In the following code, a product (represented with productVM) has an observable property (productName) containing its name in two languages (english and french).
Once a cartItem is added, and a product is selected, I want its displayed name to be updated when the button "change language" is clicked (e.g., if "Door" is selected, and "change language" is then clicked, the displayed name should be the french version (which is simply the english word plus a french-ish suffix "eux")).
But it doesn't work: The options do change, but the selected option is changed to the caption option.
What needs to be changed/added to fix it?
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.availableProducts = ko.observableArray([]);
self.language = ko.observable();
self.init = function () {
self.initProducts();
self.language("english");
}
self.initProducts = function () {
self.availableProducts.push(
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
);
}
self.getProducts = function () {
return self.availableProducts;
}
self.getProductName = function (product) {
if (product != undefined) {
return self.language() == "english" ?
product.productName().english : product.productName().french;
}
}
self.getProductColours = function (selectedProductName) {
selectedProductName = selectedProductName();
// if not caption
if (selectedProductName) {
var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
return (self.language() == "english" ? product.productName().english : product.productName().french) == selectedProductName;
});
return matched.availableColours;
}
}
self.addCartItem = function (a, b, c, d) {
self.cartItems.push(new cartItemVM());
}
self.changeLanguage = function () {
self.language() == "english" ?
self.language("french") :
self.language("english");
}
}
self.productVM = function (name, availableColours) {
var self = this;
self.productName = ko.observable({
english: name,
french: name + "eux",
});
self.availableColours = ko.observableArray(availableColours);
}
self.cartItemVM = function () {
var self = this;
self.cartItemName = ko.observable();
self.cartItemColour = ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $parent.getProducts(),
optionsText: function (item) { return $parent.getProductName(item); },
optionsValue: function (item) { return $parent.getProductName(item); },
optionsCaption: 'Choose a product',
value: cartItemName"
>
</select>
</div>
<div>
<select data-bind="options: $parent.getProductColours(cartItemName),
optionsText: $data,
optionsCaption: 'Choose a colour',
value: cartItemColour,
visible: cartItemName() != undefined"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'change language', click: changeLanguage" />
</div>
</div>
Your problem occurs when you change the options of your select. During the change, your value bound observable, cartItemName, contains the English string. For example: Door. As soon as you change the language, there is not a single option that returns Door for its optionsValue expression, thereby clearing the value altogether.
The best solution is to store a reference to your actual viewmodel, rather than just its string name. This does require you to move some other bits & pieces around, since you're manually updating quite a bit.
The starting point of the change:
// Remove
self.cartItemName = ko.observable();
// Add
self.cartItem = ko.observable();
// Change
<select data-bind="...
value: cartItem
" />
In a working snippet, with some other changes to make my work easier:
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.language = ko.observable("english");
self.availableProducts = ko.observableArray([
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
]);
self.productNameFor = function(product) {
return product.productName()[self.language()];
};
self.addCartItem = function (a, b, c, d) {
self.cartItems.push(new cartItemVM());
}
self.changeLanguage = function () {
self.language() == "english" ?
self.language("french") :
self.language("english");
}
}
self.productVM = function (name, availableColours) {
var self = this;
self.productName = ko.observable({
english: name,
french: name + "eux",
});
self.availableColours = ko.observableArray(availableColours);
}
self.cartItemVM = function () {
var self = this;
self.cartItem = ko.observable();
self.cartItemColour = ko.observable();
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $root.availableProducts,
optionsText: $root.productNameFor,
optionsCaption: 'Choose a product',
value: cartItem"
>
</select>
</div>
<div data-bind="with: cartItem">
<select data-bind="options: availableColours,
optionsCaption: 'Choose a colour',
value: $parent.cartItemColour"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'change language', click: changeLanguage" />
</div>
</div>
Related
I have two connected dropdowns that I'm using with Knockout. Both of them I'm populating with a simple Value,Text pair that I load from an ajax call.
When I go to change the value however I always get the previous value from the selected list. So if the default RegionCode value is "-1" the first RegionCode value that will be passed to the loadLocationList when I change it to another region will be "-1" even if the actual selected value is different that what I get by inspecting the element or through JQuery
Model
define([
'util',
'locationService',
'jquery',
'knockout',
'knockoutmapping',
function(util, svc, $, ko, mapped) {
var LocationViewModel = function(regionCodes, regionCode, locationCodes, locationCode, currentYear) {
var self = this;
self.CurrentYear = currentYear;
self.RegionCode = ko.observable(regionCode)
self.RegionCodes = ko.observableArray(regionCodes)
self.LocationCode = ko.observable(locationCode)
self.LocationCodes = ko.observableArray(locationCodes)
self.loadLocationList = function() {
self.LocationCodes([]);
var locationListCallback = function(data){
for (var i = 0; i < data.length; i++) {
self.LocationCodes.push(new SelectListPair(data[i].Value, data[i].Text));
}
}
svc.getLocationsInRegion(self.CurrentYear, self.RegionCode, true, locationListCallback);
}
}
var SelectListPair = function (value, text) {
this.Value = ko.observable(value);
this.Text = ko.observable(text);
}
return function summaryViewModel() {
var self = this;
self.LocationSummaryViewModel = ko.observable();
var initViewModel = function() {
$.ajax({
url: 'Url here',
success: function(vm) {
var locationVM = vm.LocationSummaryViewModel;
var selectListArray = locationVM.LocationList;
var selectList = [];
for (var i = 0; i < selectListArray.length; i++) {
var SelectListPair = {
Value: ko.observable(selectListArray[i].Value),
Text: ko.observable(selectListArray[i].Text),
};
selectList.push(SelectListPair);
}
var location = [new SelectListPair("-1", "Please select a location")];
self.LocationSummaryViewModel(new LocationViewModel(vm.CurrentYear, selectList, "-1", location, "-1"));
},
});
}
}
self.initViewModel();
});
View
<!-- ko with: LocationSummaryViewModel() -->
<div class="panel panel-default" data-bind="visible: Visible()">
<div class="panel-heading">
<div class="row">
<div class="col-md-4">
Location Summary
</div>
<div class="col-md-4">
<select class="form-control" data-bind="
options: RegionCodes(),
disable: RegionCodes().length === 1,
optionsText: 'Text',
optionsValue: 'Value',
event: {change: loadLocationList },
value: RegionCode">
</select>
</div>
<div class="col-md-4">
<select class="form-control" data-bind="
options: LocationCodes(),
disable: LocationCodes().length === 1,
optionsText: 'Text',
optionsValue: 'Value',
value: LocationCode">
</select>
</div>
</div>
</div>
<div class="panel-body">
Neat content
</div>
<div class="panel-footer">
</div>
</div>
<!-- /ko -->
Don't bind to the change event on a select. Subscribe to the value variable instead. In Knockout, the idea is always to represent the view in the model, and then use the view exclusively.
I want to write a computed so I can choose between text input and text from a select drop down, but somewhere down the route I´m stuck.
I have an '' collecting my text string
From a '' I can choose among 'Projects'.
Then I have a checkbox which decides whether the text from the selected project should override my input-textstring.
If the is empty. the selected project.title should set it.
This is my code:
HTML:
<input data-bind="textInput: $root.toDo" placeholder="What to do?" /><br/><br/>
<select data-bind="options: $root.Projects, optionsCaption: '< choose project >', optionsText: 'title', value: $root.selected"></select><br/>
<input id="useProjectTitle" type="checkbox" value="toDoUseProjectTitle" data-bind="checked: $root.toDoUseProjectTitle" />
<label for="useProjectTitle">Use project title as action</label>
<div data-bind="with: $root.toDo">
<label>I prefer:</label>
<ul >
<li >
Project: <span data-bind="text: $root.toDoProjectAction"></span><br/> <!-- Project title-->
To do: <span data-bind="text: $root.toDo"></span> <!-- toDo -->
</li>
</ul>
</div>
And my javascript:
Project = function(data){
var self = this;
self.id = data.id;
self.title = ko.observable(data.title);
};
var viewModel = function () {
var self = this;
self.Projects = ko.observableArray();
// data
self.Projects.push(new Project({
id: 1,
title: 'Friday night live'
}));
self.Projects.push(new Project({
id: 2,
title: 'Saturday morning gym'
}));
self.selected = ko.observable();
self.toDoUseProjectTitle = ko.observable(false);
self.toDoProjectAction = ko.computed(function () {
var title;
var project = self.selected();
if (project) {
title = project.title();
}
return title;
});
self.toDo = ko.computed({
read: function (value) {
if (self.selected()) { // not 'undefined' or null
if (self.toDoUseProjectTitle() || value === null) {
value = self.selected().title();
}
}
return value;
},
write: function (value) {
return value;
},
owner: self
});
};
ko.applyBindings(new viewModel());
Fiddle: http://jsfiddle.net/AsleG/srwr37k0/
Where do I go wrong?
I'm not sure I fully understand your desired behavior, but I have modified your Fiddle to use an extra variable and to correct your writable computed. It could be rearranged to work without a writable, but I didn't. :)
self.handEntered = ko.observable('');
self.toDo = ko.computed({
read: function () {
var value = self.handEntered();
if (self.selected()) { // not 'undefined' or null
if (self.toDoUseProjectTitle() || value === null) {
value = self.selected().title();
}
}
return value;
},
write: function (value) {
self.handEntered(value);
},
owner: self
});
http://jsfiddle.net/srwr37k0/14/
So far, my code binds data to choices object, and then add field input dynamically when clicking "add course" button. Then, uses $http.get to return json data from the server and binds it to select option. Although when I choose an option, it only selects applied option.
HTML:
<div ng-app="timeTable" ng-controller="addCoursesCtrl">
<button ng-click="addNewCourse()">Add New Course</button>
<fieldset ng-repeat="choice in choices">
<select ng-model="choice.type"
ng-options="s.value as s.name for s in coursetoAdd">
<option value="{{s.value}}">{{s.value}}</option>
</select>
<input ng-model="choice.course">
</fieldset>
<button ng-click="convertAndSend()">Submit</button>
</div>
Javascript:
var timeTable = angular.module("timeTable",[]);
timeTable.controller("addCoursesCtrl", function ($scope,$http) {
$scope.choices = [{ course: '', type: '' }];
$scope.coursetoAdd = $scope.choices;
$http.get("/Semster/getSuggtedCourses").then(function (response) {
$scope.coursetoAdd = response.data;
});
$scope.addNewCourse = function () {
var newITemNo = $scope.choices.length + 1;
$scope.choices.push({ course: '', type: '' });
};
$scope.convertAndSend = function () {
var asJson = angular.toJson($scope.choices);
console.log(asJson);
$http.post('/Semster/Add', asJson);
};
});
in my MVC application im creating multiple Dropdowns by following:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
optionsValue: 'id',
value: selectedItem(1),
event: { change: selectionChange }"></select>
the findgroup(x) and the selectedItem(x) are global functions in my ViewModel while those are for all the dropdowns the same.
the selectedItem(x) should return the currently selected Option of the dropdown. selectedItem(x) is a function to return a computed knockout observable.
Now im facing the Problem that the selectionChange Event is fired twice. See this fiddle for an example: http://jsfiddle.net/LGveR/20/
In this example, if you Change the value of the Dropdown box, you can see that the selectionCahnge Event is fired twice.
When i leave the value: selectedItem(x) out (and thus no computed function in the code) it doesnt: see: http://jsfiddle.net/LGveR/21/
I think that second time the Event is being fired Comes from the fact that in the computed function selectedItem(x) the observable
grp.selectedItem(grp.findItemByValue(value));
is setted.
How to prevent that the Setting of this observable leads to a "Change" Event ?
TIA,
Paul
HTML:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
optionsValue: 'id',
value: selectedItem(1),
event: { change: selectionChange }"></select> <span data-bind="text: 'aantal: ' + findGroup(1).items().length"></span>
<br /> <span data-bind="text: 'Group Selected Country: ' + findGroup(1).selectedItem().country"></span>
<br /> <span data-bind="text: 'Computed Selected Country: ' + selectedItem(1)().country"></span>
<br /> <span data-bind="text: 'after select: ' + counter()"></span>
<br />
Javascript:
var group = function (id) {
this.id = id;
this.items = ko.observableArray() || {};
this.selectedItem = ko.observable();
this.addItem = function (data) {
this.items.push(data);
};
this.findItemByValue = function (id) {
return ko.utils.arrayFirst(this.items(), function (item) {
return item.id === id;
});
}
};
var grpItem = function (id, country) {
this.id = id;
this.country = country;
};
var ViewModel = function () {
this.groups = ko.observableArray() || {};
this.counter = ko.observable(0);
this.selectionChange = function (data, event, selector, item) {
this.counter(this.counter() + 1);
};
this.addGrp = function (data) {
this.groups.push(data);
};
this.findGroup = function (groupId) {
var ret = ko.utils.arrayFirst(this.groups(), function (c) {
return c.id === groupId;
});
return ret;
};
this.selectedItem = function (groupId) {
var grp = this.findGroup(groupId);
return ko.computed({
read: function () {
return this.findGroup(groupId).selectedItem();
},
write: function (value) {
grp.selectedItem(grp.findItemByValue(value));
}
}, this);
};
};
var vm = new ViewModel();
var p = new group(1);
var a = new grpItem(1, 'holland');
var b = new grpItem(2, 'germany');
var c = new grpItem(3, 'brasil');
p.addItem(a);
p.addItem(b);
p.addItem(c);
vm.addGrp(p);
ko.applyBindings(vm);
You're doing a couple odd things in your code which results in the computed being recomputed a bunch of times. Basically, you're setting the computed value by setting an observable with a function that relies on that observable, which recomputes your computed (or something crazy like that, see http://jsfiddle.net/LGveR/25/ to see how many times read and write are being called). There are a couple simple ways you can simplify and remove this issue:
Remove the optionsValue from your select data-bind. This will set
the value to the entire item in the observable array (instead of
just the id). You can then simplify the computed write function.
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
value: selectedItem(1),
event: { change: selectionChange }"></select>
and
this.selectedItem = function (groupId) {
var grp = this.findGroup(groupId);
return ko.computed({
read: function () {
return grp.selectedItem();
},
write: function (value) {
grp.selectedItem(value);
}
}, this);
};
see http://jsfiddle.net/LGveR/23/
Alternatively, you could remove the selectedItem on the viewmodel
entirely, and remove the optionsValue (as in #1). Then, you only need the group observable with the following html:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
value: findGroup(1).selectedItem,
event: { change: selectionChange }"></select>
<span data-bind="text: 'aantal: ' + findGroup(1).items().length"></span>
<br />
<span data-bind="text: 'Group Selected Country: ' + findGroup(1).selectedItem().country"></span>
...
See http://jsfiddle.net/LGveR/24/
I have an array within an array, for example I have the following objects:
{ruleGroups: [{
rules: [{
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}]
}]
};
How can I edit the array within the array?
I was able to improve the issue but still have problems with adding row when adding group, the new group works but the old groups run dead:
A working example is found here (http://jsfiddle.net/abarbaneld/UaKQn/41/)
Javascript:
var dataFields = function() {
var fields = [];
fields.push("datafield1");
fields.push("datafield2");
return fields;
};
var operators = function() {
var operator = [];
operator.push("Plus");
operator.push("Minus");
operator.push("Times");
operator.push("Divided By");
return operator;
};
var booleanOperators = function() {
var operator = [];
operator.push("Equal");
operator.push("Not Equal");
operator.push("Greater Than");
operator.push("Less Than");
operator.push("Contains");
operator.push("Etc...");
return operator;
};
var ruleObj = function () {
return {
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}
};
var ruleGroup = function() {
return rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg.rules);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
};
var ViewModel = function() {
var self = this;
self.datafields = ko.observableArray(dataFields());
self.operators = ko.observableArray(operators());
self.booleanOperators = ko.observableArray(booleanOperators());
self.groupRules = ko.observableArray([new ruleGroup()]);
self.addGroup = function() {
self.groupRules.push(new ruleGroup());
};
self.removeGroup = function() {
if(self.groupRules().length > 1){
self.groupRules.remove(this);
}
};
self.save = function() {
console.log('Saving Object', ko.toJS(self.groupRules));
};
};
ko.applyBindings(new ViewModel());
HTML
<div data-bind="foreach: { data: groupRules, as: 'groupRule' }" style="padding:10px;">
<div>
<div data-bind="foreach: { data: rules, as: 'rule' }" style="padding:10px;">
<div>
<select data-bind="options: $root.datafields(), value: rule.dataField1, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.operators(), value: rule.operator, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.datafields(), value: rule.dataField2, optionsCaption: 'Choose...',visible: operator"></select>
<select data-bind="options: $root.booleanOperators(), value: rule.boolean, optionsCaption: 'Choose...'"></select>
<input data-bind="value: rule.duration" />
<span data-bind="click: groupRule.addRow">Add</span>
<span data-bind="click: groupRule.removeRow">Remove</span>
</div>
</div>
<span data-bind="click: $parent.addGroup">[Add Group] </span>
<span data-bind="click: $parent.removeGroup">[Remove Group]</span>
</div>
</div>
<div>
<span data-bind="click:save">[Save]</span>
</div>
I was able to fix the issue by rearranging the function of ruleGroup to:
var ruleGroup = function() {
var rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
return rg;
};
Not exactly sure why this made a difference but I think its due to now a new var is being created and referenced.
Working JSFiddle is found here http://jsfiddle.net/abarbaneld/UaKQn/