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);
};
Related
I have a problem when implementing a nested list in Angular: the view gets updated properly but, on the other side, the code is not updated on change.
I think it will be much clearer with the code:
_this.categories = injections.map(function (category) {
return {
title: category.get('title'),
object: category,
criteria: category._criteria.map(function (oneCriteria) {
return {
object: oneCriteria,
type: oneCriteria.get("type"),
min: _this.range(oneCriteria.get("range")).min,
max: _this.range(oneCriteria.get("range")).max,
key: oneCriteria.get("key"),
value: _this.range(oneCriteria.get("range")).min,
defaultValue: _this.range(oneCriteria.get("range")).min,
selected: false
}
})
}
});
_this.category = _this.categories[0];
_this.job = {
title: '',
description: '',
salaryAmount: 0,
salaryTimeUnit: _this.salaryTimeUnits[0],
category: _this.category.object,
criteria: _this.category.criteria,
location: {latitude: 48.137004, longitude: 11.575928}
};
So and, very quick here is my HTML:
<div ng-repeat="category in controller.categories">
<input type="radio" name="group" ng-value="category.object.get('title')" id="{{category.object.get('title')}}"
ng-checked="controller.category == category" ng-click="controller.category = category">
{{category.title}}
</div>
<br>
Criteria:
<div ng-repeat="criterium in controller.category.criteria">
<div class="row vertical-align">
<div class="col-xs-9">
<span ng-click="criterium.selected = !criterium.selected"
ng-class="['list-group-item', {active:criterium.selected == true}]">{{criterium.key}}</span>
</div>
</div>
</div>
The problem is the following: the value are getting updated in the view (when you click on a radio button on the category, you see the corresponding criteria(s)). But the job is for one reason that I ignore not updated although it has the same reference as the HTML (a reference to this category.criteria).
Did I miss something?
controller.job.criteria is still just a reference to controller.categories[0]. Your code should successfully update controller.category to point at whichever category you clicked on, but that does not also update the reference in your job data structure.
What you want to do is make your ngClick event a bit more robust, i.e.:
<input type="radio" ng-click="controller.updateCategory(category)" />
and then in your js:
_this.updateCategory = function (category) {
_this.category = category;
_this.updateJob(category);
};
_this.updateJob = function (category) {
_this.job.category = category.object;
_this.job.criteria = category.criteria;
};
This will update the references in your job to match the new jazz.
I would, however, recommend leveraging ngModel and ngChange in your radios instead. Like:
<input type="radio" ng-model="controller.category" ng-value="category" ng-change="updateJob(category)" /> {{category.title}}
I have a select box which is populated with some data from my controller. When an input value changes the contents of the select box should be filtered and a default value should be assigned based on the is default property of the data object.
Is there any way this can be done using angular directives or would it need to be done as a custom filter function doing something along the lines of
angular.forEach(vm.data,function(item){
if (vm.q == item.someId && item.isDefault) {
vm.result = item.value;
}
});
My html looks something like
<div ng-app="myApp" ng-controller="ctrl as vm">
<input type="text" ng-model="vm.q">
<select ng-options="item.value as item.description for item in vm.data | filter:{someId:vm.q}" ng-model="vm.result"></select>
</div>
and my controller looks like:
(function(){
angular.module('myApp',[]);
angular
.module('myApp')
.controller('ctrl',ctrl);
function ctrl()
{
var vm = this;
vm.data = [
{
someId: '1',
description: 'test1',
value: 100,
isDefault: true
},
{
someId: '2',
description: 'test2',
value: 200,
isDefault: false
},
{
someId: '3',
description: 'test3',
value: 100,
isDefault: true
},
];
}
})();
See my plunkr demo here: http://plnkr.co/edit/RDhQWQcHFMQJvwOyHI4r?p=preview
Desired behaviour:
1) Enter 1 into text box
2) List should be filtered to 2 items
3) Select box should pre-select item 1 based on property isDefault set to true
Thanks in advance
I'd suggest you include some 3rd party library, like lodash, into your project to make working with arrays/collections that much easier.
After that you could add ng-change directive for your input.
<input type="text" ng-model="vm.q" ng-change="vm.onChange(vm.q)">
And the actual onChange function in the controller
vm.onChange = function(id) {
var item = _.findWhere(vm.data, { someId: id, isDefault: true });
vm.result = item ? item.value : null;
};
And there you have it.
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);
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.