Knockoutjs model does not update when select options change - javascript

I have a computed function which doesn't update the UI when a select options change. But works fine if I add or remove a line.
This is the HTML:
<button data-bind="click: add">Add New</button>
<ul data-bind="foreach: items">
<li>
<label data-bind="text: name"></label>
<select data-bind="options: [1,2,3], value: val"> </select>
</li>
</ul>
TOTAL: <span data-bind="text: total"></span>
And this the JavaScritp:
function viewModel (initialItems) {
this.items = ko.observableArray(initialItems);
this.total = ko.computed(function () {
var total = 0;
for (var i = 0; i < this.items().length; i++)
total += this.items()[i].val;
return total;
}, this);
this.add = function() { this.items.push({name: "New", val: 1}); };
}
ko.applyBindings(new viewModel([{name: "Alpha", val: 2},
{name: "Beta", val: 3},
{name: "Gamma", val: 1}]));
And here is the fiddle: http://jsfiddle.net/waUE4/
How can I get the model update when selection change?
Thanks for your help.
Edit
Working version: http://jsfiddle.net/fCE3a/1/

The reason why the val property is not updated is that it is not declared as an Observable property.
Check out this sample code from the official KnockoutJS website, it looks like what you want to do: Cart editor example

Related

Angular.js 1.4.7 dynamic ng-options causing mutiple method calls and infdig errors

I have a select list where the content is dynamically generated from the content selected in the first select list. I cant seem to find why I'm getting these errors fired every time the first select list is changed:
Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn:
regularInterceptedExpression","newVal":42,"oldVal":37},{"msg":"fn: regularInterceptedExpression","newVal":"16","oldVal":"14"}],[{"msg":"fn: regularInterceptedExpression","newVal":47,"oldVal":42},{"msg":"fn: regularInterceptedExpression","newVal":"18","oldVal":"16"}],[{"msg":"fn: regularInterceptedExpression","newVal":52,"oldVal":47},{"msg":"fn: regularInterceptedExpression","newVal":"20","oldVal":"18"}],[{"msg":"fn: regularInterceptedExpression","newVal":57,"oldVal":52},{"msg":"fn: regularInterceptedExpression","newVal":"22","oldVal":"20"}],[{"msg":"fn: regularInterceptedExpression","newVal":62,"oldVal":57},{"msg":"fn: regularInterceptedExpression","newVal":"24","oldVal":"22"}]]
http://errors.angularjs.org/1.4.7/$rootScope/infdig?p0=10&p1=%5B%5B%7B%22ms…Expression%22%2C%22newVal%22%3A%2224%22%2C%22oldVal%22%3A%2222%22%7D%5D%5D
Plus each time the list is changed, the "makeOptions" used to generate the second list is fired several times per single change of the list.
The two lists are setup like so:
<select ng-model="selected.level1" ng-options="lvl.name as lvl.name for lvl in level1options"></select>
<select ng-model="selected.level2" ng-options="opt.value as opt.label for opt in makeOptions()"></select>
and the controller is:
app.controller('DemoController', function($scope) {
$scope.level1options = [{ name: "A", value: "A" }, { name: "B", value: "B" }, { name: "C", value: "C" }];
$scope.makeOptionsCallCount = 1;
$scope.selected = {};
$scope.makeOptions = function() {
console.log($scope.makeOptionsCallCount++);
var options = [];
for (var i = 0; i < 5; i++) {
options.push({ label: 'Value = ' + i + $scope.selected.level1, value: i + $scope.selected.level1 });
}
return options;
};
});
Here is an example of what is going weird:
http://plnkr.co/edit/mKv7nMjot5XqBj4mkvhR?p=preview
Any ideas where I've gone wrong?
UPDATE
I may have over simplified the example where I was having troubles. The solution becomes more complex because I have a list of items where for each item you can choose a set of values. I apologize, I overlooked this element when asking my original question.
I have created new example that more accurately reflects this:
http://plnkr.co/edit/I62WepJVLuuqN0YbCafD?p=preview
When you use function inside view, it execute on every digest loop.
Also angular add watcher to result this function, and if you, for example, return new array on every call - you go to infinit loop, because [] != []
In your case, you have also another error: inside function that called in view you change variable, that also showed in view. So for this variable also added watcher.
You change value in function call, watch see that variable changed, run digest again, run function, change value, see that variable changed and etc...
So, for solution, better simple save options on change first select.
var app = angular.module('demoApp', [])
app.controller('DemoController', function($scope) {
$scope.level1options = [{ name: "A", value: "A" }, { name: "B", value: "B" }, { name: "C", value: "C" }];
$scope.makeOptionsCallCount = 1;
$scope.selected = {};
$scope.makeOptions = function(lvl) {
$scope.options = [];
console.log(++$scope.makeOptionsCallCount);
for (var i = 0; i < 5; i++) {
$scope.options.push({ label: 'Value = ' + i + lvl, value: i + lvl });
}
};
});
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="DemoController">
<div>
<h2>Infinite Digest 2</h2>
<select ng-model="selected.level1" ng-options="lvl.name as lvl.name for lvl in level1options" ng-change="makeOptionsCallCount=0;makeOptions(selected.level1)"></select>
<select ng-model="selected.level2" ng-options="opt.value as opt.label for opt in options"></select>
<br/>
<br/>
<div>Level 1: {{selected.level1}}</div>
<div>Level 2: {{selected.level2}}</div>
<div>makeOptions() Called: {{makeOptionsCallCount}} times</div>
</div>
</div>
UPDATE:
In your case from comment, you can take advantage of the fact that ng-repeat create own scope, and save to it list options, created on ng-change event.
also, you can even use your function makeOptions.
To ng-change you can pass expression:
ng-change="optionsLvLs = makeOptions(option.level1)"
here in ng-repeat scope for every option, on change event would be created optionsLvLs that not pollute your option object, if this was matter.
var app = angular.module('demoApp', [])
app.controller('DemoController', function($scope) {
$scope.level1options = [{ name: "A", value: "A" }, { name: "B", value: "B" }, { name: "C", value: "C" }];
$scope.selected = {};
$scope.options = [{ name: "A" }];
$scope.makeOptions = function(level1) {
var options = [];
for (var i = 0; i < 5; i++)
{
options.push({ label: 'Value = ' + i + level1, value: i + level1 });
}
return options;
};
$scope.addQuery = function(idx) {
$scope.options.splice(idx + 1, 0, {});
};
$scope.removeQuery = function (idx) {
$scope.options.splice(idx, 1);
};
});
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-controller="DemoController" ng-app="demoApp">
<div>
<h2>Infinite Digest 2</h2>
<div ng-repeat="option in options">
<input type="button" value="+" ng-click="addQuery($index)" />
<input type="button" value="-" ng-click="removeQuery($index)" />
<select ng-model="option.level1" ng-options="lvl.name as lvl.name for lvl in level1options" ng-change="optionsLvLs = makeOptions(option.level1)"></select>
<select ng-model="option.level2" ng-options="opt.value as opt.label for opt in optionsLvLs"></select>
<br/>
<br/>
</div>
</div>
{{ options }}
</div>
Doing the following changes it won't require to call a function in order to create the data dynamically:
$scope.level1options = [{ name: "A", value: "A" }, { name: "B", value: "B" }, { name: "C", value: "C" }];
$scope.level2options = [0,1,2,3,4];
<select ng-model="selected.level1" ng-options="lvl.name as lvl.name for lvl in level1options"></select>
<select ng-model="selected.level2" ng-options="i+selected.level1 as 'Value = '+i+selected.level1 for i in level2options"></select>
Here's the plunker: http://plnkr.co/edit/GQhnjTpB0sLHoiSnchhn?p=preview

Instantiate a property from viewModel within foreach

I am iterating over an object with a knockout's foreach. Inside this foreach I render a table, and each table has a dropdown.
I need the value of the select, however the ko.observable() is not working within the foreach, because it sets each select value simultaneously. I need the individual select value of each field, not set each select to the same value.
Is there a solution to this?
<!--ko foreach: {data: thing, as: 'blah'}-->
<div data-bind="text: JSON.stringify(blah)"></div>
<select data-bind="options: $root.countries, optionsText: 'name', optionsValue: 'id', value: $root.selectedChoice, optionsCaption: 'Choose..'"></select>
<br/>
<input type="button" data-bind="click: $root.sendMe, enable: $root.selectedChoice" Value="Click Me"/>
<!--/ko-->
This is a fiddle that demonstrates with a simple example.
If you have multiple dropdowns, you're going to need multiple observables to store the selected value if you want to save individual selections. For example:
var CountryModel = function (data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
};
var ViewModel = function (data) {
var self = this;
self.things = ko.observableArray([
{ blarg: 'blarg', selectedChoice: ko.observable() },
{ means: 'means', selectedChoice: ko.observable() },
{ yes: 'yes', selectedChoice: ko.observable() }
]);
self.countries = ko.observableArray([
new CountryModel({ id: "1", name: "Russia" }),
new CountryModel({ id: "2", name: "Qatar" })
]);
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<!--ko foreach: {data: things, as: 'thing'}-->
<div data-bind="text: ko.toJSON(thing)"></div>
<select data-bind="options: $root.countries,
optionsText: 'name',
optionsValue: 'id',
value: selectedChoice,
optionsCaption: 'Choose..'">
</select>
<hr>
<!--/ko-->

Binding radio buttons to array of objects

I have my view model:
function (dataservice, Person){
var genders = ko.observableArray();
var languages = ko.observableArray();
var person = ko.observableArray();
function activate(){
dataservice.getGenders(genders);
dataservice.getGenders(languages);
}
var vm = {
genders: genders,
languages: languages,
person: person
};
}
function Person(person){
var firtstName = person.firtstName;
var familyName = person.familyName;
var genderId = person.genderId;
var languageId = person.languageId;
}
It's simplified for clarity.
Then I have my `Genders' data from server, it looks like this:
[{
$id: "1",
GenderId: 2,
GenderName: "Female",
GenderDescription: "Female",
GenderCode: "F"
}]
I also have Languages that looks like this:
[{
"$id": "1",
"LanguageId": 2,
"LanguageName": "Afar",
"LanguageDescription": "Afar",
"LanguageCode": "aa"
}]
What I am trying to achieve is to bind the genders array from my view model as a data source and use Person.GenderId as the value to be updated, in a way such that the correct radio button is initially selected. This selection depended on Person.GenderId.
I did something similar with Languages using a drop down and that works just fine:
<section data-bind="with: $root.personModel">
<select id="language"
data-bind="options: $root.languages,
optionsText: 'LanguageName',
optionsValue: 'LanguageId',
value: LanguageId,
optionsCaption: 'none'">
</select>
</section>
Now I am trying to do the same thing with radio buttons, but I don't know how to make it work. Here's what I have:
<section data-bind="with: $root.personModel">
<!-- ko foreach: $root.genders -->
<input type="radio"
name="genders"
data-bind="attr: {value: GenderId}, checked: GenderId" />
<span data-bind="text: $data.GenderName"></span>
<!-- /ko -->
</section>
If I understood things correctly, the foreach binding works like with and changes my context, so I can't reach GenderId from my exposed Person.
change
<input type="radio" name="genders" data-bind="attr: {value: GenderId}, checked: GenderId" />
to
<input type="radio" name="genders" data-bind="attr: {value: GenderId}, checked: $parent.GenderId" />
as explained here
$parent: This is the view model object in the parent context, the one immeditely outside the current context. In the root context, this is undefined.
You will need to utilize the checkdValue binding, see the lower part of the "checked" documentation.
Your code didn't quite translate to a repro, but here's my version more or less in your scenario. Suppose this bit of code:
var Person = function(person) {
var self = this;
self.name = ko.observable(person.name);
self.gender = ko.observable(person.gender);
};
var root = {
genders: [{ $id: "1", GenderId: 1, GenderName: "Female"},
{ $id: "2", GenderId: 2, GenderName: "Male"}]
};
root.personModel = new Person({name: 'john', gender: root.genders[1]});
ko.applyBindings(root);
Alongside this markup:
<section data-bind="with: $root.personModel">
<!-- ko foreach: $root.genders -->
<input type="radio"
name="genders"
data-bind="checkedValue: $data, checked: $root.personModel.gender" />
<span data-bind="text: $data.GenderName"></span><br />
<!-- /ko -->
</section>
This should work, see this fiddle.
The objects from the genders array are bound to the checkedValue of each input, and the personModel's gender is bound to checked.

knockout.js selectedOptions is not updated

I'm struggling with knockout.js selectedOptions binding.
I fill multiselect with items from observableArray A, choose some, store result in observableArray B. When item gets removed from array A, the B array is not updated.
Is this knockout issue or am I doing something wrong?
HTML code:
<h4>All items:</h4>
<div data-bind="foreach: items">
<p data-bind="text: name"></p>
<button data-bind="click: $parent.remove">Remove item</button>
</div>
<select multiple="multiple" data-bind="
options: items,
selectedOptions: selectedItems,
optionsText: 'name',
optionsCaption: 'Choose one or more...'
"></select>
<h4>Selected items:</h4>
<div data-bind="foreach: selectedItems">
<p data-bind="text: name"></p>
</div>
Javascript:
var viewModel = {
items: ko.observableArray([
{ name: "Item 1", id: "1" },
{ name: "Item 2", id: "2" },
{ name: "Item 3", id: "3" }
]),
selectedItems: ko.observableArray(),
remove: function(item) {
viewModel.items.remove(item);
}
}
ko.applyBindings(viewModel);
Here's the fiddle: http://jsfiddle.net/3FYAe/
How to reproduce:
select one or more items in the multiselect field, they appear in the list below ("Selected items")
remove one of the selected items
the selectbox is updated
4.
Expected: "Selected items" is updated
Actual: "Selected items" keeps deleted values
Answering my own question:
The trivial solution would be to remove the item from selectedItems array as well, i. e.
remove: function(item) {
viewModel.items.remove(item);
viewModel.selectedItems.remove(item);
}
Updated fiddle: http://jsfiddle.net/3FYAe/1/
However, I would like to find a nicer solution as I'm dealing with many more lists and many more dependencies; this is just a simplified example.

KnockoutJS binding to Key/Value pair

I am trying to bind to key/value pair data with KnockoutJS:
this.personal = {
"name" : "Chuck",
"country" : "USA"
};
In my HTML i use the $data binding:
<ul data-bind="foreach: personal">
<li data-bind="text: $data"></li>
</ul>
which results in:
[object Object]
[object Object]
Does anybody know how my binding should look like if I want to see this:
name: Chuck
country: USA
in other words...how I can show the property name and the property value?
EDIT: Someone pointed me at: https://github.com/jamesfoster/knockout.observableDictionary But I still hope to bind without an extra library
There is an easier way of binding to a key-value pair using Knockout.js. Say you have a key value pair that looks like the following
myItems: [
{ Name: 'Item 1', Value: 1},
{ Name: 'Item 3', Value: 3},
{ Name: 'Item 4', Value: 4}
],
Just use the following html to bind to the key value pair.
<select data-bind="options: myItems, optionsText: 'Name', optionsValue: 'Value'></select>
References:
http://knockoutjs.com/documentation/options-binding.html
Try something like this:
<ul data-bind="keyvalue: properties">
<li>
<span data-bind="text: key"></span> :
<span data-bind="text: value"></span>
</li>
</ul>
For JavaScript:
function AppViewModel() {
this.properties = { b: 'c', d: 'e' };
}
ko.bindingHandlers['keyvalue'] = {
makeTemplateValueAccessor: function(valueAccessor) {
return function() {
var values = valueAccessor();
var array = [];
for (var key in values)
array.push({key: key, value: values[key]});
return array;
};
},
'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
return ko.bindingHandlers['foreach']['init'](element, ko.bindingHandlers['keyvalue'].makeTemplateValueAccessor(valueAccessor));
},
'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
return ko.bindingHandlers['foreach']['update'](element, ko.bindingHandlers['keyvalue'].makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext);
}
};
ko.applyBindings(new AppViewModel());
Create a function in your view model that converts the object property names and values into an array of objects with key and value properties containing the aforementioned name and value.
var ExampleViewModel = function () {
var self = this;
self.personal = {
"name": "Loek",
"country": "Netherlands"
};
self.personalList = function () {
var list = [];
for (var i in self.personal) if (self.personal.hasOwnProperty(i)) {
list.push({
"key": i,
"value": self.personal[i]
});
}
return list;
};
};
Your html template should look like the following:
<ul data-bind="foreach: personalList()">
<li data-bind="text: $data.key + ': ' + $data.value"></li>
</ul>
This results in the following output:
name: Loek
country: Netherlands
Here's a fiddle with a working example.
I think you should do
<ul data-bind="foreach: personal">
<li data-bind=" text: country"></li>
<li data-bind=" text: name"></li>
</ul>​
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
// Use an array here
this.personal = [{
"name": "Loek",
"country": "Netherlands"
}];
}
// Activates knockout.js
ko.applyBindings(new AppViewModel());​
fiddle http://jsfiddle.net/Aw5hx/
P.S. i never used knockoutJS before this post so i'm no world expert.

Categories

Resources