Knockout computed items of observable array - javascript

I have the following ViewModel:
var myViewModel = {
categories: ko.observableArray([
{ name: ko.observable('Fruit'),
items: ko.observableArray([
ko.observable('Apple'),
ko.observable('Orange'),
ko.observable('Banana'),
]),
},
]),
};
I want to add a computed value for each item of this array:
$.each(myViewModel.categories(), function(index, cat){
cat.num = ko.computed(function(){
return cat.items().length;
}, cat);
});
This is my HTML:
<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>
</li>
</ul>
<span data-bind="text: category.num"></span>
</li>
</ul>
And it works just great! Here is the result:
Fruit: Apple
Fruit: Orange
Fruit: Banana
3
But now let's say my data has changed and I need to reflect those changes:
myViewModel.categories([
{
name: "Fruit",
items: ["Apple", "Banana"],
},
{
name: "Veg",
items: ["Potato", "Carridge"],
}
]);
And it doesn't work - I don't see num at the end of each list. Could you please help me to fix this? Here is the codepen

Of course it does not work, because you are replacing the entire content of the observableArray. This code of yours:
$.each(myViewModel.categories(), function(index, cat){
cat.num = ko.computed(function(){
return cat.items().length;
}, cat);
});
adds the num property to the items that exist in the array at the time of the call. By giving a new backing array to your observableArray by doing this:
myViewModel.categories([
{
name: "Fruit",
items: ["Apple", "Banana"],
},
{
name: "Veg",
items: ["Potato", "Carridge"],
}
])
you, of course, lose the old array and thus the computed observables which are defined on the items and not the observable array. Knockout of course has no way to know how and whether it should add the computed property to every item in the new array.
Solution:
You can either do the same property addition when you replace the backing array of your observableArray like this:
myViewModel.categories([
{
name: "Fruit",
items: ["Apple", "Banana"],
},
{
name: "Veg",
items: ["Potato", "Carridge"],
}
]);
$.each(myViewModel.categories(), function(index, cat){
cat.num = ko.computed(function(){
return cat.items().length;
}, cat);
});
or better yet, you could instantiate the objects in the array with the property already defined. Note: you don't have to write the function over and over again for each item, you could, for instance, define a constructor for the array items:
function Category(name, items) {
var self = this;
this.name = name;
this.items = ko.observableArray(items);
this.num = ko.computed(function() {
return self.items().length;
});
};
and then, this is how you create a new one:
var newItem = new Category('Category 1', [ 'Apple', 'Banana']);
and insert it into the observable array:
myViewModel.categories.push(newItem);
this adds one item and keeps the existing ones, or, as you did, you could supply a whole new array as so:
myViewModel.categories([
new Category('Fruit', ['Apple', 'Banana' ]),
new Category('Veg', ['Potato', 'Carridge' ])
]);

Related

How do I incorporate the index of an array while defining a property of said array within a class in JavaScript?

Sorry if the title makes no sense.. let me explain
Say I have the following 2d array.. the first array representing ice cream and the second representing milkshakes
menu = [ ['vanilla', 'chocolate', 'almond'],
['vanilla', 'pineapple', 'strawberry'] ]
Now I create a class that takes this array as input
class cafe{
constructor(menu){
this.iceCreams = menu[0]
this.milkshakes = menu[1]
}
}
Now I want to define a property called 'price' for each flavor of milkshake.
this.milkshakes[n].price = < a function that computes price based on value of n >
so that i can access them like this :
cafe.milkshakes[0].price
So how do I incorporate the index 'n' of the array while defining the property
I haven't tried anything bcos I dont know how to even approach this ☹️
You can do it in your constructor.
You can grab the names, and call map function on it and do whatever you want. Please check the following example. There, calculatePrice is a function that takes the index and returns the price based on the index.
class Cafe {
constructor (menu) {
this.iceCreams = menu[0].map((flavor, index) => {
return {
flavor,
price: calculatePrice(index)
}
});
this.milkshakes = menu[1].map((flavor, index) => {
return {
flavor,
price: calculatePrice(index)
}
});
}
This is a minimal answer.
UPDATE:
For a detailed and improved answer: https://codesandbox.io/s/cafe-example-wxp2c4
So, in the milkshakes array you need each item as an object data structure, not a string.
menu = [ ['vanilla', 'chocolate', 'almond'],
[{ flavor: 'vanilla' }, { flavor: 'pineapple' }, { flavor: 'strawberry' }] ]
and then you can loop through and set the price, something like this.
menu.milkshakes.forEach((item, index) => item.price = index))
you can use objects:
menu = [
[
{
name: "vanilla",
price: 200,
},
{
name: "chocolate",
price: 200,
},
{
name: "almond",
price: 200,
},
],
[
{
name: "vanilla",
price: 200,
},
{
name: "pineapple",
price: 200,
},
{
name: "strawberry",
price: 200,
},
],
];
and then:
class cafe{
constructor(menu){
this.iceCreams = menu[0]
this.milkshakes = menu[1]
}
}
now iceCreams and milshakes have the property price and name
example:
iceCreams[n].price
iceCreams[n].name

Add class to ng-repeat object in angular based on another array

I am trying to add a class to an object within an ng-repeat if a property of that object (name) exists within another array.
Basically, a user can flag an object in the items ng-repeat as correct or incorrect which creates a "judgement" object within the judgements array. Onload of the page, I want to be able to compare the two arrays to add/remove a class based on if the most recent judgement of the object is incorrect or correct.
Based on my fiddle below, item1 and item3 should have a class of "incorrect". How could I accomplish this?
I tried using inArray (see http://jsfiddle.net/arunpjohny/wnnWu/) but could not figure out how to get it to work with specific properties rather than the entire array.
fiddle: http://jsfiddle.net/bTyAa/2/
function itemCtrl($scope) {
$scope.items = [{
itemname: "item1"
}, {
itemname: "item2"
}, {
itemname: "item3"
}];
$scope.judgements = [{
judgementResult: "incorrect",
date: "2016-02-01T11:03:16-0500",
item: {
itemname: "item1"
}
}, {
judgementResult: "correct",
date: "2016-01-06T11:03:16-0500",
item: {
itemname: "item1"
}
}, {
judgementResult: "incorrect",
date: "2016-01-04T11:03:16-0500",
item: {
itemname: "item3"
}
}]
}
<div ng-app>
<div ng-controller="itemCtrl">
<ul>
<li ng-repeat="item in items" class="item"> <span>{{item.itemname}}</span>
</li>
</ul>
</div>
</div>
Try to define new function to get proper class for your item
http://jsfiddle.net/bTyAa/8/
$scope.getJudgementsClass = function(itemName) {
var matched = $scope.judgements.filter(function(el) {
return el.item.itemname === itemName;
}).sort(function(a,b){
// Turn your strings into dates, and then subtract them
// to get a value that is either negative, positive, or zero.
return new Date(b.date) - new Date(a.date);
});
if (matched.length == 0)
{
return "";
}
console.log(itemName);
return matched[0].judgementResult;
}
why not doing the NG-repeat on the second array since you already have the information in the first array as item in the second array.
and just use the judgement Result as the class ?
function itemCtrl($scope) {
$scope.items = [{
itemname: "item1"
}, {
itemname: "item2"
}, {
itemname: "item3"
}];
$scope.judgements = [{
judgementResult: "incorrect",
date: "2016-02-01T11:03:16-0500",
item: {
itemname: "item1"
}
}, {
judgementResult: "correct",
date: "2016-01-06T11:03:16-0500",
item: {
itemname: "item1"
}
}, {
judgementResult: "incorrect",
date: "2016-01-04T11:03:16-0500",
item: {
itemname: "item3"
}
}]
}
<div ng-app>
<div ng-controller="itemCtrl">
<ul>
<li ng-repeat="obj in judgements" ng-class="obj.judgementResult"> <span>{{obj.item.itemname}}</span>
</li>
</ul>
</div>
</div>
You cant iterate through $scope.judgements array on each ng-repeat iteration. Something like:
$scope.isItemInCorrect = function(itemname){
console.log($scope.judgements);
for(var i = $scope.judgements.length; i--;) {
if ($scope.judgements[i].item.itemname == itemname
&& $scope.judgements[i].judgementResult == 'incorrect') {
return true;
}
}
}
<li ng-repeat="item in items" ng-class="{'incorrect' : isItemInCorrect(item.itemname)}" class="item"> <span>{{item.itemname}}</span>
http://jsfiddle.net/n0eb82j3/7/

Angularjs map array to another array

I have two arrays, Users and Employments like so:
Users = [{id:1, name: "ryan"}, {id:2, name:"Julie"}]
Employments = [{user_id: 1, title: "manager"}, {user_id: 2, title: "Professor"}]
I'd like to display the Employments array in an ng-repeat like so:
<li ng-repeat="employment in Employments">
{{employment.user.name}}
</li>
How do I map the Users array to the Employments array?
If you want the employee name to get displayed based on id, the simplest way is just pass that id to a function and return the name, like as shown below
Working Demo
html
<div ng-app='myApp' ng-controller="ArrayController">
<li ng-repeat="employment in Employments">{{getEmployeeName(employment.user_id)}}
</li>
</div>
script
var app = angular.module('myApp', []);
app.controller('ArrayController', function ($scope) {
$scope.Users = [{
id: 1,
name: "ryan"
}, {
id: 2,
name: "Julie"
}];
$scope.Employments = [{
user_id: 1,
title: "manager"
}, {
user_id: 2,
title: "Professor"
}];
$scope.getEmployeeName = function (empId) {
for (var i = 0; i < $scope.Users.length; i++) {
if ($scope.Users[i].id === empId) {
return $scope.Users[i].name;
}
};
};
});
UPDATE 2
If you want to embed the User array in the Employments array, try the following stuff
$scope.Users = [{id: 1, name: "ryan"}, {id: 2, name: "Julie"}];
$scope.Employments = [{user_id: 1, title: "manager"},
{user_id: 2, title: "Professor"}
];
code for flattening Employments array by adding User properties
angular.forEach($scope.Users, function (user, userIndex) {
angular.forEach($scope.Employments, function (employee, employeeIndex) {
if (employee.user_id === user.id) {
employee.name = user.name;
}
});
});
Output
$scope.Employments = [ { user_id: 1, title: "manager", name: "ryan" },
{ user_id: 2, title: "Professor", name: "Julie" }
]
Working Demo
UPDATE 3
Code for making a nested employee structure like as shown below from $scope.Users and $scope.Employments
$scope.employees = [];
angular.forEach($scope.Employments, function (employee, employeeIndex) {
var employeeObject = {};
employeeObject.title = employee.title;
angular.forEach($scope.Users, function (user, userIndex) {
if (employee.user_id === user.id) {
employeeObject.user = user;
}
});
$scope.employees.push(employeeObject);
});
Output
[ { title: "manager", user: { "id": 1, "name": "ryan" } },
{ title: "Professor", user: { "id": 2, "name": "Julie" } }
]
Working Demo
If you wanted to match up the two following arrays purely with a template you could take the following arrays
Users = [{id:1, name: "ryan"}, {id:2, name:"Julie"}]
Employments = [{user_id: 1, title: "manager"}, {user_id: 2, title: "Professor"}]
And nest a repeat like:
<li ng-repeat="employment in Employments">
<div ng-repeat="user in Users" ng-if="user.id === employment.user_id" >
{{user.name}}:{{employment.title}}
</div>
</li>
Two more nice little thing to do to avoid any risk of getting those brackets showing on a slow page load is to use the ng-bind and prefix the attributes with data so its with the html spec
<li data-ng-repeat="employment in Employments">
<div data-ng-repeat="user in Users" data-ng-if="user.id === employment.user_id" >
<span data-ng-bind="user.name"></span>:<span data-ng-bind="employment.title"></span>
</div>
</li>
I know you didn't have the need for anything but the name, but figured a quick example of using the outer loop in the inner still could be helpful. Also this would be the case for ng-init if you needed to reference the the $index of the outer ng-repeat from the inner, but that might be more than you're looking for here.
Plunker
This sorts the users names into the employments array:
var sortUsers = function() {
var i = 0;
for (i; i < $scope.users.length; i++) {
console.log($scope.users[i].id)
for(var z = 0; z < $scope.employments.length; z++) {
if($scope.employments[z].user_id === $scope.users[i].id) {
$scope.employments[z].name = $scope.users[i].name;
}
}
}
}
HTML:
<ul>
<li ng-repeat="employment in employments">
{{employment.name}}
</li>
</ul>
I dealt similar problem yesterday. If you want to use js, have to loop twice.
I recommend to use the best way is to select in one query by join table if data come from single database.
You select User by one query, and Employment for another query in database. Then, twice ng-repeat to re-arrange. Here is my solution.
select users.*, employments.title from `users` inner join `employments` where users.id = employments.user_id;
Hope be be helpful.

How to watch for a model's deep changes, from templates?

I have a model on my scope, which is an object of objects. I have seen this, but I want to do this from the template as I have a filter defined on it.
var App = angular.module('app', []);
App.controller('myCtrl', function($scope) {
$scope.items = {
{ name: 'Cricket bat', cost: '2500', quantity: 0},
{ name: 'Football', cost: '1100', quantity: 0}
};
$scope.cartItems = {}; // This holds the items. I want quantity of each item separately so it's not an array.
I have defined a filter getPrice which calculates the price for the items in users cart.
And I have in the template:
{{ cartItems | getPrice }}
Is it possible to have the template update after any of the nested object value (ie. quantity of one of the items from the cart) changes? If yes, how?
Move the quantity field to the cartItem object
$scope.items = {
{ name: 'Cricket bat', cost: '2500'},
{ name: 'Football', cost: '1100'}
};
$scope.cartItems = {
{ name: 'Cricket bat', quantity: 3},
{ name: 'Football', quantity: 6}
};
Then update your getprice filter based on the above json. This should take care of your cost in the cart getting updated when a price of an item changes.
Seems like your strategy is not good overall.
An "object of objects" as you mean it { {id:1}, {id:2} } is just not valid javascript, and will generate an error. Choose an array of objects [ {id:1}, {id:2} ] or a true object { 1: {id:1}, 2: {id:2} }
#Blackhole is right, you don't need to watch anything, interpolation is part of angular.js core and does all that tricky watch stuff for you. Defining a cartItems variable, and interpoling it (with brackets {}) in a template, filtering it or not, will keep the interpolated value in sync with the variable value without you doing anything more. It's even two-ways bound (if you change model, variable will also change). You can verify the sync by removing temporiraly your filter : try to put {cartItems} in your template and update your cart, you'll see the template updated.
This model format would probably be more appropriate
$scope.items = [
{ name: 'Cricket bat', cost: '2500' },
{ name: 'Football', cost: '1100' }
];
$scope.cartItems = [
{ item: { name: 'Cricket bat', cost: '2500' }, quantity: 3 },
];

Populating arrays after their definition

I have a ul containing li's which contain names of different recipe ingredients for a recipe page. I'm trying to get those ingredients and store them into a JavaScript array within an object. I already know the title of the recipe so I put that right into the object property title, but I don't know how many ingredients there will be for each recipe. Here is what I have:
var recipeobj = {
title: $('h3.title').val(),
ingredients: [
ingredient,
optional
]
}
$.each($('ul.ingredients > li > h4'), function (index, ingredient) {
recipeobj.ingredients[index].ingredient = $(ingredient).html();
recipeobj.ingredients[index].optional = false;
})
If I try to do console.log(recipeobj.ingredients) I just get the error Uncaught ReferenceError: ingredient is not defined
No doubt this is simple, I just rarely need to use arrays in JavaScript so have little experience with them.
Open your console and run it
var recipeobj = {
title: $('h3.title').html(),
// ingredients is empty for now
ingredients: []
};
$.each($('ul.ingredients > li > h4'), function(index, ingredient) {
// Get the name
var name = $(ingredient).html(),
// Find out if it is 'optional'(using a class here)
optional = $(ingredient).hasClass('optional');
// Push a new ingredient into the array
recipeobj.ingredients.push({ name: name, optional: optional });
});
console.log(recipeobj);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3 class="title">Pork and beans</h3>
<ul class="ingredients">
<li>
<h4>Pork</h4>
</li>
<li>
<h4>Beans</h4>
</li>
<li>
<h4 class="optional">Salt*</h4>
</li>
</ul>
This should output:
{
"title": "Pork and beans",
"ingredients": [
{ name : "Pork", optional : false },
{ name : "Beans", optional : false },
{ name : "Salt*", optional : true}
]
}
var rObj = {
title: $('h3.title').val(),
ingredients: [
'source cream',
'cheese',
'chopped meat'
],
optional: true
};
accessing
var rItem = rObj.ingredients[1];
or you want
var rObj = {
title: $('h3.title').val(),
ingredients: {
ingredient_list: ['one','two','three'],
optional: true
}
};
accessing
var rItem = rObj.ingredients.ingredient_list[1];
The structure you are attempting to use looks like the structure should be like
var rObj = {
title: $('h3.title').val(),
things: [{
ingredient: 'source cream',
optional: true
},
{
ingredient: 'cheese',
optional: false
}]
};
accessing
var ingred = rObj.things[1].ingredient;
var rObj = {
title: $('h3.title').val(),
ingredients : []
};
you can add ingredients:
$.each($('ul.ingredients > li > h4'), function (index, ingredient) {
rObj.ingredients.push({ingredient: $(ingredient).html(), optional :false})
})

Categories

Resources