I have an issue with Knockout.js . What I try to do is filter a select field. I have the following html:
<select data-bind="options: GenreModel, optionsText: 'name', value: $root.selectedGenre"></select>
<ul data-bind="foreach: Model">
<span data-bind="text: $root.selectedGenre.id"></span>
<li data-bind="text: name, visible: genre == $root.selectedGenre.id"></li>
</ul>
And the js:
var ViewModel = function (){
self.selectedGenre = ko.observable();
self.Model = ko.observableArray([{
name: "Test",
genre: "Pop"
}
]);
self.GenreModel = ko.observableArray([
{
name: "Pop",
id: "Pop"
},
{
name: "Alle",
id: "All"
}
]);
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
JSFiddle: http://jsfiddle.net/CeJA7/1/
So my problem is now that the select list does not update the binding on the span inside the ul and I don't know why...
The value binding should update the property selectedGenre whenever the select value changes, shouldn't it?
Any ideas are welcome.
There are a lot of issues in your code:
1) self is not a magical variable like this. It's something people use to cope with variable scoping. Whenever you see self somewhere in a JavaScript function be sure there's a var self = this; somewhere before.
2) KnockoutJS observables are not plain variables. They are functions (selectedGenre = ko.observable()). ko.observable() returns a function. If you read the very first lines of documentation regarding observables you should understand that access to the actual value is encapsulated in this retured function. This is by design and due to limitations in what JavaScript can and cannot do as a language.
3) By definition, in HTML, <ul> elements can only contain <li> elements, not <span> or anything else.
Applying the above fixes leads to this working updated sample:
HTML:
<select data-bind="options: GenreModel, optionsText: 'name', value: selectedGenre"></select>
<span data-bind="text: $root.selectedGenre().id"></span>
<ul data-bind="foreach: Model">
<li data-bind="text: name, visible: genre == $root.selectedGenre().name"></li>
</ul>
JavaScript:
var ViewModel = function (){
var self = this;
self.selectedGenre = ko.observable();
self.Model = ko.observableArray([
{
name: "Test",
genre: "Pop"
}
]);
self.GenreModel = ko.observableArray([
{
name: "Pop",
id: "Pop"
},
{
name: "Alle",
id: "All"
}
]);
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
Related
I'm new to Knockout. Basically I need to get an array of items (pairs of price and quantity) from controller and then display it on my view. But before i display it, I want to use knockout to do some computation (calculate the subtotal) in the viewmodel, then pass the new array to the view. I know I can bind the original array to my view. But how can i pass this array to my viewmodel?
You would not pass from a view to a viewmodel, it's the other way around. Controller passes data to a viewmodel, which is bound to a view. I digress.
There are several different techniques but a common one is to map the data into observable values. Knockout has a helper method arrayMap which will help convert items in the array into observables. An example below:
var Item = function(data) {
var self = this;
self.Name = ko.observable(data.Name);
self.Price = ko.observable(data.Price);
self.Qty = ko.observable(data.Qty);
self.Total = ko.pureComputed(function() { return self.Price() * self.Qty();});
}
var viewModel = function() {
var self =this;
// list of items
self.Data = ko.observableArray([]);
// simulate ajax call to fetch data
self.Load = function() {
var data = [
{ Name: "A", Price: 12.34, Qty: 1},
{ Name: "B", Price: 23.45, Qty: 2 },
{ Name: "C", Price: 1234.56, Qty: 3 }
];
var mappedData = ko.utils.arrayMap(data, function(item) {
return new Item(item);
});
this.Data(mappedData);
}
}
var vm = new viewModel();
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: Data">
<li>
Name: <span data-bind="text: Name"></span>
Quantity: <input type="text" data-bind="value: Qty" style="width: 100px;" />
Price: <input type="text" data-bind="value: Price" style="width: 100px;" />
Total: <span data-bind="text: Total"></span>
</li>
</ul>
<p>Click the button to simulate a load from API
<button data-bind="click: Load">Load</button></p>
[ Please see updates at the bottom ]
I'm trying to make knockout depended selects, it's intended to make a "product" selection by these attributes, for example a product can have "size" and "material", if I selected "size", a knockout script make a request to the backend and retrieves which "material" available for the selected size, in other words, if an attribute is selected, other attributes are filtered out to show only available values ("all sizes": 1,2,3,4,5; "aluminium": 1,4).
Attributes list are completely dynamic, there are about 80 attributes which can be linked to the products in arbitrary way.
Are there any "best practices" for this situation?
I am trying to solve it with code like this, without success yet:
var ViewModel = function(data) {
var self = this;
self.data = data;
self.attributes = ko.observableArray();
self.data.forEach(function(item, i, a) {
// I passed .self to catch it later
// in products as view_model.attributes().
self.attributes.push(new VariableProduct(item, self));
})
};
var VariableProduct = function(item, view_model) {
var self = this;
self.attribute_name = ko.observable(item.name);
self.attribute_value = ko.observable('--');
// list of attribute values
self.attribute_values = ko.computed(function() {
var result = {};
view_model.attributes().forEach(function(attribute, i, a) {
// here I try to filter each attributes lists by values
// it doesn't work well
if (attribute.attribute_name() != self.attribute_name() && self.attribute_value() != '--') {
result = attribute.attribute_values().filter(
function(value) {
return value.indexOf(self.attribute_value()) >= 0;
});
}
});
return result;
});
};
UPDATE 1:
With Dnyanesh's reference to ko.subscribe(), i've achived these results, isn't ok yet, but a progress:
http://jsfiddle.net/xwild/65eq14p3/
UPDATE 2:
At the end it was solved with knockout.reactor and knockout.mapping plugins.
Related stackoverflow question with details and the answer.
For dependent select I think you can use subscribe in following manner
var vm = {
sizes: ko.observableArray([
{ name: 'size 1', id: 1},
{ name: 'size 2', id: 2},
{ name: 'size 3', id: 3},
{ name: 'size 4', id: 4}
]),
selectedSize : ko.observable(0),
};
vm.selectedSize.subscribe(function(newValue){
alert('Selected Size is ---> ' + newValue )
// Here at this point call your ajax or backend method and bind the values which are coming form
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<select data-bind="
options: sizes,
optionsText: 'name',
optionsValue: 'id',
value: selectedSize,
optionsCaption: 'Choose Size...'"">
</select>
<select data-bind="
options: material,
optionsText: 'name',
optionsValue: 'id',
value: selectedMaterial,
optionsCaption: 'Choose Material...'"">
</select>
I know I am talking about only part of solution to your problem but, I think you need to divide your data object to bind it to various controls.
Got a bit of a conundrum with a knockout observable array being shared across multiple view models.
Basically, I have a layout as follows
Transport
... textbox fields, etc
Selected Passengers:
<!-- ko foreach: allPassengers -->
<input type="checkbox" />
<!-- /ko -->
<button>Add Transport</button>
Holiday
... textbox fields, etc
Selected Passengers:
<!-- ko foreach: allPassengers -->
<input type="checkbox" />
<!-- /ko -->
<button>Add Holiday</button>
Now the selected passengers for each section is being generated from ONE observable array, idea being if a passenger is deleted/altered everything should fall into place automagically.
So something like this
function page() {
// in actuality this passengers array is a computed observable obtained from the passengers section which is not shown here.
this.allPassengers = ko.observableArray([
{
Id: 1,
name = ko.observable('name'),
checked = ko.observable(false)
},
{
.
.
]);
}
function transport() {
// pageVM is a page object
this.allPassengers = pageVM.allPassengers;
this.transportItems = ko.observableArray();
this.addTransport = function() {
this.transportItems.push({
.
.
selectedPassengers: [...]
.
.
});
};
}
function holiday() {
// pageVM is a page object
this.allPassengers = pageVM.allPassengers;
this.holidayItems = ko.observableArray();
this.addHoliday = function() {
this.holidayItems.push({
.
.
selectedPassengers: [...]
.
.
});
};
}
However, when add transport/holiday is clicked, I need a way to determine which checkboxs where checked so I can add the selected passengers.
I have tried to add a checked = ko.observable(false) property to the passenger item in parent.allPassengers, but the problem with this approach is if a checkbox is checked in the transport section it will also check it in the holiday section since it is using the same observable array.
Any ideas??
Edit:
example fiddle
The checked binding works with observable arrays too. So you can simply bind to $parent.selectedPassengers and specify the value attribute to be the passenger id, like this:
<input type="checkbox" data-bind="attr: { value: id },
checked: $parent.selectedPassengers" />
In each view model you need to have a selectedPassengers observable array used for binding to the checkbox. And the add function should look like this:
function transport(pageVM) {
....
this.selectedPassengers = ko.observableArray([]);
....
this.addTransport = function() {
this.selectedItems.push({
....
selectedPassengers: this.selectedPassengers()
});
};
};
Working Fiddle
You can use a ko.computed to return the selected passengers (and here's a fiddle):
var ViewModel = function () {
this.allPassengers = ko.observableArray([
{ name: 'John', selected: ko.observable(false) },
{ name: 'Jane', selected: ko.observable(false) },
{ name: 'Mark', selected: ko.observable(false) }
]);
this.selectedPassengers = ko.computed(function () {
return ko.utils.arrayFilter(this.allPassengers(), function (item) {
return item.selected();
});
}, this);
};
I have a drop down list that is bound to a SelectedFormat value. The lists options are populated from external source on load and matches the view models data.Format object base on id.
Take a look at the js fiddle
Can anyone tell me why the model updates but the UI is not updating with the correct Format.Name
Thanks.
HTML:
<div data-bind="text:data.Format.Name"></div>
<select data-bind="
options:Controls,
optionsText: 'Name',
value: data.SelectedFormat"></select>
Model:
var jsonData = {
Id: "abc-123",
Name: "Chicken Cheese",
Format: {
Id: 2,
Name: 'Medium',
Other: 'Bar'
}
};
var self = this;
self = ko.mapping.fromJS(data);
self.SelectedFormat = ko.observable(
//return the first match based on id
$.grep(vm.Controls,function(item){
return item.Id === self.Format.Id();
})[0]
);
//when changed update the actual object that will be sent back to server
self.SelectedFormat.subscribe(function (d) {
this.Format = d;
},self);
In your code, you have Format and SelectedFormat. The former isn't an observable and so can't trigger updates. You have to use SelectedFormat instead.
<div data-bind="text:data.SelectedFormat().Name"></div>
Example: http://jsfiddle.net/QrvJN/9/
The Knockout mapping plugin documentation has a section entitled "Uniquely identifying objects using “keys”". This describes how to update part of an object and then only update that part of the display rather than completely replacing the display of all properties of a partially-modified object. That all works splendidly in their simple example, which I have slightly modified here to make my question more clear. My modifications were to:
Replace the object with a corrected name after a 2 second delay.
Highlight the unchanging part of the display, so you can see that it is actually not replaced when the update happens.
1. Simple object (jsFiddle)
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<script>
var data = {
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
But what isn't clear to me is how I would achieve the same behavior for a more complex nested data structure. In the following example, I took the above code and wrapped the data in a list. I would like this to behave the same as above, but it doesn't. The whole display is redone because of the change in one property. You can see this because, unlike the above example, the highlighting is lost after the data is updated.
2. More complex nested object (jsFiddle)
<!-- ko foreach: parents -->
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<!-- /ko -->
<script>
var data = {
parents: [
{
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
parents: [
{
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
So basically what I'm asking is, how can I make the second example work like the first, given the more nested data structure? You can assume that ids are unique for each child (so if I added another parent besides Scott, his children would start with id=2, etc.).
Interesting observation there and nice write-up. It appears to work if you define a key on the parent as well as the child. Try this fiddle:
http://jsfiddle.net/8QJe7/6/
It defines instantiable view model functions for the parents and children, where the parent constructor does its child mappings.