From what I've been able to find online I don't think it's possible to use the foreach data-bind to iterate through the properties of an observable object in knockout at this time.
If someone could help me with a solution to what I'm trying to do I'd be very thankful.
Let's say I have an array of movies objects:
var movies = [{
title: 'My First Movie',
genre: 'comedy',
year: '1984'
},
{
title: 'My Next Movie',
genre: 'horror',
year: '1988'
},
];
And what I would like to do is display this data in a table, but a different table for each genre of movie.
So I attempted something like this:
<div data-bind="foreach: movieGenre">
<table>
<tr>
<td data-bind="year"></td>
<td data-bind="title"></td>
<td data-bind="genre"></td>
</tr>
</table>
</div>
and my data source changed to look like this:
for (var i = 0; i < movies.length; ++i) {
if (typeof moviesGenres[movies.genre] === 'undefined')
moviesGenres[movies.genre] = [];
moviesGenres[movies.genre].push(movie);
}
I've tried about a dozen other solutions, and I'm starting to wonder if it's my lack of knowledge of knockout(I'm pretty green on it still), or it's just not possible the way I'd like it to be.
You can make your array "movies" an KO observable array and the array "movieGenre" a KO computed property. Have a look at this fiddle.
The code in the fiddle is given below for reader convenience;
KO View Model
function MoviesViewModel() {
var self = this;
self.movies = ko.observableArray([
{
title: 'My First Movie',
genre: 'comedy',
year: '1984'
},
{
title: 'My Next Movie',
genre: 'horror',
year: '1988'
},
{
title: 'My Other Movie',
genre: 'horror',
year: '1986'
}
]);
self.movieGenre = ko.computed(function() {
var genres = new Array();
var moviesArray = self.movies();
for (var i = 0; i < moviesArray.length; i++) {
if (genres.indexOf(moviesArray[i].genre) < 0) {
genres.push(moviesArray[i].genre);
}
}
return genres;
});
};
HTML
<div data-bind="foreach: movieGenre()">
<h3 data-bind="text: 'Genere : ' + $data"></h3>
<table border="solid">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Year</th>
</tr>
</thead>
<tbody data-bind="foreach: $parent.movies">
<!-- ko if: $data.genre === $parent -->
<tr>
<td data-bind="text: $data.title"></td>
<td data-bind="text: $data.genre"></td>
<td data-bind="text: $data.year"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
As you can see "movieGenre" is made a computed property. Whenever the observable array "movies" changes, the moveGenre is calculated and cached. However, since this is not declared as a writable computed property, you cannot bind this to your view. Hence, it's value is used in the data binding.
The approach for rendering is simply looping through the calculated "movieGenre", and nest another loop for movies. Before adding a row to the table, for the corresponding table, the movie object is evaluated with the current movieGenre. Here, the container-less control flow syntax is used. We can use the "if" binding, but that would leave an empty table row per each movie object where the genre is otherwise.
The $parent binding context is used to access the parent contexts in the nested loop.
Hope this helps.
Related
I have a requirement where I need to add index values for child rows. I have Group rows under which there will be child rows. I am ng-repeat and I am using $index for child's as shown:
HTML code:
<table ng-repeat="node in data">
<tr> <td> {{node.groupName}} </td> </tr>
<tbody ng-model="node.nodes">
<tr ng-repeat="node in node.nodes"> <td> {{$index}} </td> </tr>
</table>
But it is displaying as shown:
But I want it to display as shown:
I am new to Angular JS and not getting how to display it like this. How am I supposed to do that. Please help.
As far as I understood your question, you'd like to have something like that:
<table ng-repeat="group in data">
<thead>
<th> {{group.name}} </th>
</thead>
<tbody ng-repeat="item in group.items">
<tr>
<td>{{getIndex($parent.$index - 1, $index)}} | {{item}} </td>
</tr>
</tbody>
</table>
$scope.data = [
{name: 'Group1', items: ['a','b']},
{name: 'Group2', items: [1,2,3]},
{name: 'Group3', items: ['x', 'xx', 'xxx', 'xxxx']}
];
$scope.getIndex = function(previousGroupIndex, currentItemIndex){
if(previousGroupIndex >= 0){
var previousGroupLength = getPreviousItemsLength(previousGroupIndex);
return previousGroupLength + currentItemIndex;
}
return currentItemIndex;
};
function getPreviousItemsLength(currentIndex){
var length = 0;
for (var i = 0; i <= currentIndex; i++){
length += $scope.data[i].items.length;
}
return length;
// or even better to use Array.prototype.reduce() for that purpose
// it would be shorter and beautiful
// return $scope.data.reduce(function(previousValue, currentGroup, index){
// return index <= previousGroupIndex ? previousValue + currentGroup.items.length : previousValue;
// }, 0);
}
You need to play with $parent.$index property and use some math :) in order to achieve that.
It would look like the following:
Check out this JSFiddle to see live example.
I'm working on a project where I would like to be able to display several items side by side for comparison.
I'd like to have the comparison displayed similarly to the way in which amazon does, with each item being represented by a vertical column with all of the info about the item in that column:
I'm currently using Knockout, and have an Observable array with the items I would like to compare. I would like to use the foreach binding to do the job and my problem stems from the fact that table's have all cells for a specific row contained in the same element which makes it difficult to use:
<tr>
<td>Rating</td>
<td> 4.5 (data from item 1)</td>
<td> 3.5 (data from item 2)</td>
</tr>
<tr>
<td>price</td>
<td>$2.3 (data from item 1) </td>
<td>$3.5 (data from item 2) </td>
</td>
If I wanted each item to be contained in a row this would not be a problem.
Anyone have any recommendations for how to tackle this issue since the data for different items has to exists in different (overlapping) parts of the document.
I have bad news for you. Html tables are strictly ordered in rows, and this kind of layout for your data can be a bit frustrating.
You have two options, basically:
Option A: Transpose your data
You could introduce a specific View Model for a "Feature-Comparison" and transpose your data to that. You can then do a foreach on your tbody iterating through those "featureComparisons". In code, here's what I mean:
var inputData = [
{ name: "First product", rating: 4.5, price: "$3.50", color: "Gray" },
{ name: "Another product", rating: 3.5, price: "$2.95", color: "Yellow" }
];
function FeatComparison(comparisonName, comparedValues) {
this.comparisonName = comparisonName;
this.comparedValues = comparedValues;
}
var vm = { comparisons: ko.observableArray([]) };
// You could also explicitly send a list of comparable properties from back-end.
// For this example we iterate the first product.
for (var prop in inputData[0]) {
if (inputData[0].hasOwnProperty(prop)) {
var vals = inputData.map(function(i) { return i[prop]; });
vm.comparisons.push(new FeatComparison(prop, vals));
}
}
ko.applyBindings(vm);
td { background: #eee; padding: 5px 10px; }
tr:first-child, td:first-child { font-weight: bold; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table><tbody data-bind="foreach: comparisons">
<tr>
<td data-bind="text: comparisonName"></td>
<!-- ko foreach: comparedValues -->
<td data-bind="text: $data"></td>
<!-- /ko -->
</tr>
</tbody></table>
This option is best suited for situations where you want to make an extended comparison feature with things like comparing multiple products, saving user preferences on what features to include or not, etc.
Option B: Deal with it (for lack of a better title)
Just leave your data as is, and iterate through both objects with foreach at the same time. In code:
var inputData = [
{ name: "First product", rating: 4.5, price: "$3.50", color: "Gray" },
{ name: "Another product", rating: 3.5, price: "$2.95", color: "Yellow" }
];
function RootVm(data) {
var self = this;
this.products = ko.observableArray(data);
this.keys = ko.computed(function() {
if (self.products().length === 0) return [];
return Object.keys(self.products()[0]);
});
}
ko.applyBindings(new RootVm(inputData));
td { background: #eee; padding: 5px 10px; }
tr:first-child, td:first-child { font-weight: bold; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table><tbody data-bind="foreach: keys">
<tr>
<td data-bind="text: $data"></td>
<!-- ko foreach: $root.products -->
<td data-bind="text: $data[$parent]"></td>
<!-- /ko -->
</tr>
</tbody></table>
I'm in the process of learning Polymer. I am trying to bind an array to my UI. Each object in the array has a property that will change. I need my UI to update when the property value changes. I've defined my Polymer component as follows:
my-component.html
<dom-module id="my-component">
<template>
<h1>Hello</h1>
<h2>{{items.length}}</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr repeat="{{ item in items }}">
<td>{{ item.name }}</td>
<td>{{ item.status }}</td>
</tr>
</tbody>
</table>
<br />
<button on-click="testClick">Test</button>
</template>
<script>
// element registration
Polymer({
is: "my-component",
properties: {
items: {
type: Array,
value: function() {
return [
new Item({ name:'Tennis Balls', status:'Ordered' }),
new Item({ name:'T-Shirts', status: 'Ordered' })
];
}
}
},
testClick: function() {
for (var i=0; i<items.length; i++) {
if (items.name === 'Tennis Balls') {
items[i].status = 'Shipped';
break;
}
}
}
});
</script>
</dom-module>
The component renders. However, the bindings do not work at all.
The line with {{ items.length }} does not show a count. Its basically just an empty h2 element.
The first item gets rendered in the list. However, the second one does not.
When I click the Test button, the update to the status is not reflected on the screen.
When I look at everything, it looks correct to me. However, it is clear from the behavior that the data-binding is not setup properly. What am I doing wrong? The fact that items.length and the initial rendering of all of the items in the array has me really confused.
Polymer data binding system works like this:
If the declared property changes (for example adding a new item) then it will detect the change and update your DOM.
However Polymer won't monitor changes inside your property (For performance/compatibility reasons).
You need to notify Polymer that something inside your property changed. You can do that using the set method or notifyPath.
E.g (From the polymer data binding section)
this.set('myArray.1.name', 'Rupert');
You can also add an observer if you want to do something when your array is updated.
Polymer 1.0 properties Documentation
And I think you should also add a notify:true to your property
items: {
type: Array,
notify:true,
value: function() {
return [
new Item({ name:'Tennis Balls', status:'Ordered' }),
new Item({ name:'T-Shirts', status: 'Ordered' })
];
}
}
I'm using Knockout to bind data to a table:
I have obj:
obj=[{id:"1",productName:"laptop",tag:[promotion,blackfriday]},{id:"2",productName:"Samsung galaxy note III",tag:[samsung,galaxy]}]
HTML:
<table data-bind="foreach:list" id="listProduct">
<tr>
<td data-bind="text:productName"></td>
<td data-bind="foreach:obj.tag">
<p data-bind="text:tag"></p>
</td>
</tr>
</table>
JavaScript (to bind data):
ko.applyBindings({ list: obj }, document.getElementById('listProduct'));
But it only displays the product name and does not bind "tag" obj; I want Knockout to bind data as:
Product Name Tag
1 Laptop promotion
blackfriday
2 Samsung galaxy note III samsung
galaxy
based on the code you gave, there are a couple simple things that will fix this for you:
productName is misspelled
the foreach:obj.tag should be foreach:tag
the text:tag should be text:$data (see the docs on this)
your tags in the data given are not strings - so it won't work if they aren't defined elsewhere
JS:
var obj = [{
id: "1",
productName: "laptop",
tag: ["promotion", "blackfriday"]
}, {
id: "2",
productName: "Samsung galaxy note III",
tag: ["samsung", "galaxy"]
}];
ko.applyBindings({ list: obj }, document.getElementById('listProduct'));
HTML:
<table id="listProduct">
<tr data-bind="foreach:list">
<td data-bind="text:productName">test</td>
<td data-bind="foreach:tag">
<p data-bind="text:$data">test</p>
</td>
</tr>
</table>
The implementation that are you using is too odd, and I think is not correct. For example I don't know why are you retrieving the listProduct element and sending as argument to ko.applyBidning, this is not how knockout works.
You also have mistakes using inside object in the foreach binding, check out documentation to see the correct usage. Even you have a misspelled in the HTML with the productName value.
You have syntax errors inside both tag arrays, the values are string so you need to add quotes to each element.
I will show a better and clean way to do it fixing the issues mentioned above:
JS
function VM(){ //declare your VM constructor.
this.list = ko.observableArray([
{id: '1', productName: 'laptop', tag: ['promotion', 'blackfriday']},
{id: '2', productName: 'Samsung galaxy note III', tag: ['samsung', 'galaxy']}
]); //Add and observable array an set your data. If you don't need that this array be an observable just use a normal JS array.
}
ko.applyBindings(new VM()); //apply binding
HTML
<table data-bind="foreach: list">
<tr>
<td data-bind="text: productName"></td>
<td data-bind="foreach: tag">
<p data-bind="text: $data"></p>
</td>
</tr>
</table>
Check out this fiddle to see it working.
I have a structure of an array with a nested array. I am trying to remove an item from the nested array but I get the error that "remove is not a function".
I've recreated the problem in a simple jsFiddle - http://jsfiddle.net/rswailes/gts5g/ and also pasted the code below.
It's possible that the way I have set up the observables is not correct, but I'm stumped.
This is my html:
<script id="bookGroupTemplate" type="text/html">
<br/>
<h3><span data-bind="text: group_name"></span></h3>
<table>
<thead>
<tr>
<th>Author</th>
<th>Title</th>
<th>Genre</th>
<th></th>
</tr>
</thead>
<tbody data-bind='template: {name: "bookRowTemplate", foreach: books}'></tbody>
</table>
</script>
<script id="bookRowTemplate" type="text/html">
<tr>
<td data-bind="text: author"></td>
<td data-bind="text: title"></td>
<td data-bind="text: genre"></td>
</tr>
</script>
<h1>Books!</h1>
<div data-bind='template: {name: "bookGroupTemplate", foreach: bookGroups}'></div>
<br/><br/>
<button data-bind="click: function(){viewModel.handleButtonClick(); }">Move One From Now to Later</button>
This is the javascript:
var BookGroup = function(group_name, booksToAdd){
var self = this;
this.group_name = ko.observable(group_name);
this.books = ko.observableArray();
_.each(booksToAdd, function(book){
self.books.push(ko.observable(book));
});
}
var Book = function(author, title, genre) {
this.author = ko.observable(author);
this.title = ko.observable(title);
this.genre = ko.observable(genre);
}
var PageViewModel = function() {
var self = this;
this.bookGroups = ko.observableArray();
this.bookToUse = new Book("Robin Hobb", "Golden Fool", "Fantasy");
this.indexAction = function() {
var groups = [];
var booksArray = [];
booksArray.push(this.bookToUse);
booksArray.push(new Book("Patrick R Something", "Name Of The Wind", "Fantasy"));
booksArray.push(new Book("Someone Else", "Game Of Thrones", "Fantasy"));
groups.push(new BookGroup("To Read Now", booksArray));
booksArray = [];
booksArray.push(new Book("Terry Pratchett", "Color of Magic", "Discworld"));
booksArray.push(new Book("Terry Pratchett", "Mort", "Discworld"));
booksArray.push(new Book("Terry Pratchett", "Small Gods", "Discworld"));
groups.push(new BookGroup("To Read Later", booksArray));
this.bookGroups(groups);
};
this.handleButtonClick = function(){
console.log(this.bookGroups()[0].books().length);
this.bookGroups()[0].books().remove(this.bookToUse);
};
};
viewModel = new PageViewModel();
ko.applyBindings(viewModel);
viewModel.indexAction();
Why is remove not recognized here? Is this the right way to construct the model?
Many thanks for any advice :)
There were 2 errors:
You tried to call remove function form javascript array instead observable array.
You don't need to wrap book object with observable when put it to observableArray.
Have you tried remove on the observableArray and not on its contents, in other words :
this.handleButtonClick = function(){
console.log(this.bookGroups()[0].books().length);
this.bookGroups()[0].books.remove(this.bookToUse);
};
see Observable Arrays