This question already has answers here:
Knockout JS - How to correctly bind an observableArray
(2 answers)
Closed 9 years ago.
I have been looking into an Knockout for dynamic data-bind and I have a situation where I need an observable array to contain multiple observable objects.
This is my code:
<ul data-bind="foreach: { data: categories, as: 'category' }">
<li>
<ul data-bind="foreach: { data: items, as: 'item' }">
<li>
<span data-bind="text: category.name"></span>:
<span data-bind="text: item"></span>
<input type="text" data-bind="value: item"/>
</li>
</ul>
</li>
</ul>
$(document).ready(function () {
ko.applyBindings(viewModel);
});
var viewModel = {
categories: ko.observableArray([
{ name: 'Fruit', items: [ko.observable('Apple'), ko.observable('Orange'), ko.observable('Banana')] },
{ name: 'Vegetables', items: [ko.observable('Celery'), ko.observable('Corn'), ko.observable('Spinach')] }
])
};
When working with oject observables usually I could modify a value of an input text box and that value is set to the entire page where that property was used to be displayed.
In my current example I tried to do the same with my input box , but after I modified the values in the text box the span did not to the curent value.
How can I make the observable objects inside the observableArray behave as they would have if they were stand alone observable objects?
when i encounter these issues, i like to break them down into sub vms, which allow me better control over what is happening at each level of context that im in. for your issues above, i would do something like this:
var produceVM = function (data) {
var self = this;
self.item = ko.observable(data);
}
var categoryVM = function (data) {
var self = this;
self.name = ko.observable(data.name);
self.items = ko.observableArray();
var items = ko.utils.arrayMap(data.items, function (item) {
return new produceVM(item);
});
self.items(items);
}
var viewModel = function (data) {
var self = this;
self.categories = ko.observableArray();
var categories = ko.utils.arrayMap(data, function (category) {
return new categoryVM(category);
});
self.categories(categories);
}
var data = [
{ name: 'Fruit', items: [ 'Apple', 'Orange', 'Banana' ] },
{ name: 'Vegetables', items: ['Celery', 'Corn', 'Spinach' ]}
];
ko.applyBindings(new viewModel(data));
I believe the ko mapping plugin achieves something similar to this without having to write all of the above code. you could pass it the data model and it would construct the observables for each item.
As #nemesv pointed, answer lies right under the corner.
Simply wrap every array item into object and it will work flawlessly.
This is how your view model should look, and here is a working jsfiddle
var viewModel = {
categories: ko.observableArray([{
name: 'Fruit',
items: [
{name: ko.observable('Apple')},
{name: ko.observable('Orange')},
{name: ko.observable('Banana')}
]
}, {
name: 'Vegetables',
items: [
{name: ko.observable('Celery')},
{name: ko.observable('Corn')},
{name: ko.observable('Spinach')}
]
}])
};
I have to say from my own experience, that usually you'll have objects inside array anyway, not claiming that there isn't other use cases, just saying that it is very useful to have objects in array and have ability to change them dynamically and not to worry about anything else.
Related
I'm creating a list GUI which can be re-arranged by drag and drop. Data comes from an existing API as an array of objects such as:
var dataFromApi = [
{name: 'jack', label: 'Test element'},
{name: 'john', label: 'Test element 2'}
];
There are many more properties per object, but there is no unique ID field for any object. The nature of data means that duplicate objects are permitted.
I used a React class to render these as a list:
var List = React.createClass({
getDefaultProps: function() {
return {
data: []
};
},
getInitialState: function() {
return {
data: this.props.data
};
},
render: function() {
return (
<ul>
{
this.state.data.map(function(item, idx) {
return (
<li>{item.name}</li>
);
})
}
</ul>
);
}
});
I tried adding some drag-and-drop code, to allow the list to be re-ordered and the state to be updated accordingly. However, to re-order I need each <li key={something?}> element to have a unique key attribute which isn't the index.
What would be the best way add a unique key to each element, but not have this key in the data if I ever needed to send the changes up to a parent element - such as for saving the data to the server.
try doing both, <li key=idx+item> think that would solve your problem
Tried almost everything, setting the key as a combination of item properties and index, but nothing seemed to work if I wanted to allow re-arranging of child elements.
In the end I generated a random key for each object just after an API call which worked and allowed items to be re-arranged.
I am trying to acheive something similar to SQL table join,
in the most elegant (functional) way, preferably with underscore.js,
so no for loops please.
I need to merge objects from two different arrays, matched upon a common identifier.
For example, given:
var basic = [{
id: '1',
name: 'someName',
},
{...} ]
var ext= [{
id: '1',
job: 'someJob',
},
{...} ]
Result should be:
var combined = [{
id: '1',
name: 'someName',
job: 'someJob',
},
{...} ]
Thanks!
Map, findWhere and extend should do the trick:
var combined = _.map(basic, function(base){
return _.extend(base, _.findWhere(ext, { id: base.id} ));
});
Edit:
If performance is an issue create a hash of the extended values:
var extHash = _.reduce(ext, function(memo, extended, key){
memo[extended.id] = extended;
return memo;
}, {});
and use like so:
var combined = _.map(basic, function(base){
return _.extend(base, extHash[base.id]);
});
Fiddle
NO LOOP : http://jsfiddle.net/abdennour/h3hQt/2/
basic=basic.sort(function(a,b){return a.id-b.id});
ext=ext.sort(function(a,b){return a.id-b.id});
var combined=basic.map(function(e,i){return inherits(e,ext[i]) });
Known that ,inherits are as following:
function inherits(base, extension)
{
for ( var property in base )
{
extension[property] = base[property];
}
return extension ;
}
Here is the sample plunker http://embed.plnkr.co/bJFmT0WlRfqUgrCxZRT6
To start with: I am grouping a collection by a certain key - in my example yob.
I have two options -
I can write a custom function that does the job (which I want to do as I can add custom logic)
I can use _.groupBy provided by lodash/underscore.js
So I decided to try both the methods - using lodash I group the collection by a key and output is displayed (see plunkr)
When I use the custom method, studentsByYear in this case, somehow the array becomes empty before being displayed. I have console logged my output before returning the array and the array has the desired output ..
So my question is why my grouping method does not work? Am I missing something obvious in angular? Is it that I have to do a deep copy of objects before I return them, if yes please explain?
<div ng-controller="myController">
<h2> Using Lodash </h2>
<ul ng-repeat="(yob, students) in myModel.studentsByYobLodash">
<h3>{{ yob }}</h3>
<div ng-repeat="s in students">
<p> {{s.name}} </p>
</div>
</ul>
<h2>Not using Lodash </h2>
<ul ng-repeat="(yob, students) in myModel.studentsByYob">
<h3>{{ yob }}</h3>
<div ng-repeat="s in students">
<p> {{s.name}} </p>
</div>
</ul>
</div>
script
var app = angular.module("myApp", []);
app.factory('studentsFactory', [function () {
var students = [{
name: 'Tony',
yob: '1987'
},{
name: 'Rachel',
yob: '1988'
}, {
name: 'Eric',
yob: '1988'
}, {
name: 'Jon',
yob: '1988'
}, {
name: 'Tim',
yob: '1989'
}, {
name: 'Bing',
yob: '1987'
}, {
name: 'Valerie',
yob: '1988'
}, {
name: 'Brandon',
yob: '1987'
}, {
name: 'Sam',
yob: '1987'
}]
return {
getStudents: function () {
return students;
}
}
}])
app.controller('myController', ['$scope', 'studentsFactory', function ($scope, studentsFactory) {
$scope.myModel = [];
$scope.myModel.students = studentsFactory.getStudents();
$scope.myModel.studentsByYobLodash = studentsByYearUsingLodash($scope.myModel.students)
$scope.myModel.studentsByYob = studentsByYear($scope.myModel.students);
function studentsByYearUsingLodash (students) {
return _.groupBy(students, 'yob');
}
function studentsByYear(students) {
var arr = [];
angular.forEach(students, function (student) {
var key = student.yob;
_.has(arr, key) ? arr[key].push(student) : (arr[key] = [student]);
})
return arr;
}
}])
With the structure you have you are using key, value with object iteration in ng-repeat. So by sending myModel.studentsByYob as an array will eventually return an array with holes, because you will end up having, for instance, myModel.studentsByYob[0..] as undefined as they don't exist and the array object instead has property 1987, 1988 etc which points to the array of students and if you check the browser console you will see the exact same error pointed by the ng-repeat code because of multiple undefined keys returned. So just change:
var arr = [];
to
var arr = {};
Plunkr
The issue is when you create the arr in studentsByYear():
var arr = [];
should be
var arr = {};
Angular iterators treat arrays and objects differently, so when iterating on a non-zero-indexed array, using (key, value) will always result in an unset key. Since Angular thinks undefined == undefined, it results in a duplicate key error.
Incidentally: you theoretically could get away with this error exactly once, so if your yob's were:
[1, 2, 3, 4...] instead of [1987, ...]
you would not have had an error, just an empty "0" at the top of your list.
http://plnkr.co/edit/VPiJSjOqPNFeunc7LUqJ?p=preview
But once you have 2 out-of-sequence indices
[2, 3, 4...] // 0 and 1 are missing
then you will again get the error, because 0 == undefined and 1 == undefined, therefore 0 == 1 and it's a duplicate error.
This should be really simple.
I have an associative observable array with a name and a boolean value.
this.items = ko.observableArray([
{ name: "name1", boolVal: true },
{ name: "name2", boolVal: true },
]);
Then a simple function to change boolVal.
this.changeValue = function (item) {
item.boolVal = false;
};
When I call the changeValue function, boolVal does change (see console.log(data) in my jsfiddle) but the view doesn't update. The value on the screen remains "true". I must be making an incorrect assumption regarding how KnockoutJS works.
JS Fiddle Link
In order to the KO update UI you need to have observable properties:
this.items = ko.observableArray([
{ name: "name1", boolVal: ko.observable(true) },
{ name: "name2", boolVal: ko.observable(true) },
]);
And set it with:
this.changeValue = function (item) {
item.boolVal(false);
};
The ko.observableArray only tracks item addition and removal. So it won't notify the UI if one of its items changed. For that you need to have ko.observable on the items.
Demo JSFiddle.
I receive a complex JSON from the server. Let it be next:
var data = [{
name: "name1",
items:[
{
name:"name11",
subItems:[{
name:"name111",
children[
{id:1,name:"child1111",status:"good"},
{id:2,name:"child1112",status:"bad"},
{id:3,name:"child1113",status:"good"}
]},
{
name:"name112",
children[
{id:4,name:"child1121",status:"good"}]
}]
},
{
name:"name12",
subItems:[{
name:"name121",
children[
{id:5,name:"child1211",status:"bad"}]
}]
}]
},
{
name: "name2",
items:[
{
name:"name21",
subItems:[{
name:"name111",
children[
{id:7,name:"child2111",status:"good"}
]}]
}]
}];
So I have the list of objects each one contains name and items properties. Items is property of the similar list of objects each one contains name and subItems properties. subItems property same to previous and has name and children properties. children is list of my entities. I use mapping for filling my ViewModel.
First of all I can't image how to set as key id in my entity. I am wondering how to "dive" to it. Moreover, I need to extend my entity. Add the compute property like next example:
computProp: ko.computed(function() {return name+status;})
I don't want to create js classes, because I don't see benefits of mapping on this case. I can implement manual mapping in this case. It would be more clear for me.
So any idea, suggesting or critics are welcome.
PS: I have searched/read similar topics
You must explicit declare the children viewmodel to get this behaviour, but you still benefit from getting all the mapping done
http://jsfiddle.net/uXMhA/
ChildViewModel = function(data) {
ko.mapping.fromJS(data, {}, this);
this.computProp = ko.computed(function() {
return this.name() + this.status();
}, this);
};