Knockout compute new values on filter - javascript

I'm building a very number-heavy app and one feature of the app is to be able to filter between the three results at the end. These are: Drugs, Disease & All (both).
I'm pretty new to Knockout so not totally sure what the best-practise way to approach the following situation would be.
I have the values filtering, my problem lies with computing the new values. The totals at the bottom have to reflect what data is currently visible in the table. Although these cell values (apart from totals) in the codepen below are static, they're actually dynamic in my app and they're a computed combination of a couple of values that are stored in sessionStorage in the app on previous pages. (This screen is a totals page).
Example:
self.total = ko.computed(function () {
return sessionStorage.getItem('1') * sessionStorage.getItem('2')
});
So to clarify; initially the computed totals at the bottom of the table are a total sum of numbers in the table cells that are multiplications of numbers from sessionStorage. The totals values then need to update every time the table is filtered to reflect the total of the visible data.
http://codepen.io/anon/pen/nlemE
I appreciate this may be complicated to get your head around and if I haven't explained it well enough, let me know and I'll clarify.

Rather than use jQuery to hide/show the rows, it would be better to use an observableArray to store your values in, and a computed function to filter that array every time the filter value changes.
Here's an updated CodePen showing how that might work: http://codepen.io/anon/pen/HAzcf?editors=101
Your data code would be an array of objects instead of r1c1, r2c1, etc. We also add a filter field so we don't have to hard code that into our HTML.
self.values = ko.observableArray([
{
c1: ko.observable(10),
c2: ko.observable(32),
c3: ko.observable(36),
filter: ko.observable('drugs')
},
{
c1: ko.observable(70),
c2: ko.observable(46),
c3: ko.observable(31),
filter: ko.observable('disease')
}
// etc..
]);
Our toggle function now simply updates an observable:
// toggle
self.filter = ko.observable('all');
self.toggleVis = function (data, event) {
self.filter(event.target.value);
};
The filtered values are updated every time the self.filter observable is updated:
self.filteredValues = ko.computed(function() {
var filter = self.filter();
return ko.utils.arrayFilter(self.values(), function(item) {
return filter == 'all' || item.filter() == filter;
});
});
And our totals properties are updated whenever the filteredValues are changed:
self.total1 = ko.computed(function () {
var total = 0,
values = self.filteredValues();
for(var i = 0; i < values.length; i++) {
total += values[i].c1();
}
return total;
});
Your HTML table code also becomes simplified to simply iterate over the filtered observable array:
<table>
<tbody>
<!-- ko foreach: filteredValues -->
<tr class="row">
<td data-bind="text:c1"></td>
<td data-bind="text:c2"></td>
<td data-bind="text:c3"></td>
</tr>
<!-- /ko -->
<tr class="totals">
<td data-bind="text:total1"></td>
<td data-bind="text:total2"></td>
<td data-bind="text:total3"></td>
</tr>
</tbody>
</table>

You want to use a Knockout Subscription on each of your observables.
self.r1c1().subscribe(function(newValue) {
// update the sum
});
Rather than add 20 subscriptions however, you might want to use a Knockout Observable Array to store/update your individual values.
var myObservableArray = ko.observableArray();
myObservableArray.push(<value for row 1>);
myObservableArray.push(<value for row 2>);
// etc
Then you could get by with a single subscription for each of your three columns.
self.myObservableArray.subscribe(function(newValue) {
// re-calculate the sum of the values
});

Related

Custom filter on 'ng-repeat' does overwrite the scope

My aim is to replace the teacher-id(f_teacher) of one outputted array with the teacher name of another array. I wrote a custom filter, that should do the job:
angular.module('core')
.filter('replaceId', function () { //filter, which replaces Id's of one array, with corresponding content of another array
return function (t_D, s_D, t_prop, s_prop) { //data of target, data of source, target property, source property
var replacment = {};
var output = [];
angular.forEach(s_D, function (item) {
replacment[item.id] = item[s_prop]; //replacment - object is filled with 'id' as key and corresponding value
});
angular.forEach(t_D, function (item) {
item[t_prop] = replacment[item[t_prop]]; //ids of target data are replaced with matching value
output.push(item);
});
return output;
}
});
I use a 'ng-repeat' like this:
<tr ng-repeat="class in $ctrl.classes | filter:$ctrl.search | replaceId:$ctrl.teachers:'f_teacher':'prename' | orderBy:sortType:sortReverse">
<td>{{class.level}}</td>
<td>{{class.classNR}}</td>
<td>{{class.f_teacher}}</td>
</tr>
But it only outputs an empty column. Now the strange thing: If I follow the steps with the debugger, it works for the first time the filter is performed. But when it is performed a second time it outputs an empty column.
I noticed that the returned object of the filter overwrites the $ctrl.classes - array, but normally this shouldn't be the case?
Here is a plnkr:
https://plnkr.co/edit/EiW59gbcLI5XmHCS6dIs?p=preview
Why is this happening?
Thank you for your time :)
The first time through your filter the code takes the f_teacher id and replaces it with the teacher name. The second time through it tries to do the same thing except now instead of getting a teachers ID in f_teacher it finds the teacher's name so it doesn't work. You could fix it by making a copy of the classes instead of modifying them directly. e.g.
angular.forEach(t_D, function (item) {
var itemCopy = angular.copy(item);
itemCopy[t_prop] = replacment[itemCopy[t_prop]];
output.push(itemCopy);
});
https://plnkr.co/edit/RDvBGITSAis3da6sWnyi?p=preview
EDIT
Original solution will trigger an infinite digest because the filter returns new instances of objects every time it runs which will cause angular to think something has changed and retrigger a digest. Could you just have a getter function that gets a teachers name instead of using a filter?
$scope.getTeacherName = function(id) {
var matchingTeachers = $scope.teachers.filter(function(teacher) {
return teacher.id == id;
})
//Should always be exactly 1 match.
return matchingTeachers[0].prename;
};
And then in the HTML you could use it like
<tr ng-repeat="class in classes">
<td>{{class.level}}</td>
<td>{{class.classNR}}</td>
<td>{{getTeacherName(class.f_teacher)}}</td>
</tr>
https://plnkr.co/edit/gtu03gQHlRIMsh9vxr1c?p=preview

How to get text box values dynamically using AngularJS

I followed the following tutorial for adding two values in Angular;
Addition of Two Numbers in AngularJS
But now I have to sum dynamic text box values inside ng-repeat.
I have the following Code
HTML Code
......
<table>
<tr>
<th>Name</th>
<th>Days</th>
</tr>
<tr ng-repeat="si in serImp">
<td>{{si.Name}}</td>
<td><input type="number" ng-model="si.Days" /></td>
</tr>
</table>
.......
<button ng-click="sum()">Calculate</button>
.......
JS code
.........
$scope.serImp = [];
$scope.serImp = data;
// here data is object contain multiple Name+Durations e.g.
/*data =
[0]
Name:"first"
Days: 1
[1]
Name:"second"
Days: 2
[2]
Name:"third"
Days: 3*/
$scope.sum = function () {
// sum code goes here
}
........
My Question is : How can I get the sum of the values/numbers from the ng-model="si.Days"
Use reduce to sum up all the days.
When you modify the values with ng-model you're manipulating the $scope.serImp array. You can then use these values to calculate the sum.
$scope.sum = function () {
return $scope.serImp.reduce(function(prv, cur) {
return prv.Days + cur.Days;
});
}
Reduce takes an array and reduces it to one value by applying one function to all of its elements. In our case just simply adding up all the days.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
I will create function that will return all Days total & will directly bind to html
{{totalDays()}}
Code
$scope.totalDays = function(){
var totalDays = 0;
angular.forEach( $scope.serImp, function(si){
if(si.Days && !isNaN(si.Days)){
totalDays += Number(si.Days);
}
})
}

How can I conditional do an ng-repeat based the number I need?

I'm pulling data from a local SQLite database and using ng-view to populate a screen. The data is repeated with ng-repeat based on the number of items returned. The page needs to have 6 items at all times. I'm able to get the total number in my scope as $scope.productCount. This number can be between 0 and 6.
What I want to do is run an additional ng-repeat for the remainder. Meaning if I only return 4 I need it to repeat twice. However, if I get 6 back I don't need it to repeat at all.
I thought about doing something like
ng-repeat="i in getNumber(6 - productCount) ng-show="productCount < 6"
and using this in my controller
$scope.getNumber = function(num) {
return new Array(num);
}
I'm not sure if 1) this is the correct way. 2) If my idea will work. I don't want to go down that rabbit hole if it's a bust.
what about using filter method.
ng-repeat='i in productCount | limitTo:4'
(not sure if I get your problem right)
You can create custom filter to repeat array items if there is less than desired items count.
MyApp.filter("arrayItemsRepeat", function () {
return function (array, count) {
var i = 0;
while(array.length < count) {
array.push(array[i]);
i++;
}
return array;
}
});
and then use the filter in template:
ng-repeat='item in items | arrayItemsRepeat:6'
<div ng-if="productCount < 6">
<div ng-repeat="data in productCount">
</div>
<div>

How can I replace an object in array that is displayed using ng-repeat?

I have an array of items that is displayed in a table using ng-repeat. When you click on an item, that item is pulled from the server and the table should then be updated with the updated item.
Function to get the updated item when clicking on an item in the table:
$scope.getUpdatedItem = function(item){
itemService.getItem(item).then(
function(updatedItem){
item = updatedItem;
},
function(error){
//Handle error
}
);
};
I'm displaying the items using:
<tr ng-repeat="item in myItems">
The problem: The item in the table is never updated.
What's the best way to update the item in the ng-repeat? Can i use "track by $index" in the ng-repeat for this? Or do I have to iterate over myItems to find the item I want to replace?
Update:
A possible solution is instead of using
item = updatedItem,
to use:
var index = $scope.myItems.indexOf(item);
$scope.myItems[index] = updateItem;
However, I feel that there should be a "cleaner" way of doing this.
There isn't a much cleaner way (then your update).
As you noticed, when you change item in your callback function you change the local reference, and not the original item in the array.
You can improve this a bit by using the $index from the ng-repeat, instead of calculating it yourself:
<div ng-click="getUpdatedItem(item, $index)"> </div>
And in your controller:
$scope.getUpdatedItem = function(item, index){
itemService.getItem(item).then(
function(updatedItem){
$scope.myItems[index] = updateItem;
},
function(error){
//Handle error
}
);
};
You can also use angular.copy instead but it's much less efficient:
function(updatedItem){
angular.copy(updateItem, item);
},
If I understand your problem properly
could something like this work?
<!-- template code -->
<table>
...
<tr ng-repeat="(index, item) in items">
<td>{{item.name}}</td>
<td>
{{item.detail}}
<button ng-if="!item.detail" ng-click="loadItem(index)">
</td>
</tr>
</table>
// Controller Code
$scope.items = [...]
$scope.loadItem = function(index){
itemService.getItemDetail($scope.items[index]).then(function(itemDetail){
$scope.items[index].detail = itemDetail;
});
};
item may start as a reference to an item in your list, but when you say:
item = updatedItem;
You reseat that binding -- you are no longer referring to the item in the list, but to the disconnected one that was returned in your promise. Either you will need to modify the item, like so:
function(updatedItem){
item.varA = updatedItem.varA
item.varB = updatedItem.varB
...
}
Or, if it gets too hairy, you might consider an item array that looks more like this:
var items = [
{ data: item1 },
{ data: item2 },
{ data: item3 }
};
At which point your update function will look like this:
function(updatedItem){
item.data = updatedItem;
},
I've just spent hours on this issue. I couldn't use the $index solution from #eladcon, as my ng-repeat also used a filter, to the index isn't correct if the rows/items are filtered.
I thought I would be able to just do this:
$filter('filter')($scope.rows, {id: 1})[0] = newItem;
but that doesn't work.
I ended up iterating the array until I found a match, and then using the $index from the iteration (not from the ng-repeat) to set the array item to the new item.
// i'm looking to replace/update where id = 1
angular.forEach($scope.rows, function(row, $index) {
if (row.id === 1) {
$scope.rows[$index] = newItem;
}
})
See here:
https://codepen.io/anon/pen/NpVwoq?editors=0011

How to update observable array element in knockoutjs?

I have following JavaScript array,
[{"unitPrice": 2499,"currency":"$","productId":1,"retailerId":1,"productName":"XX ","formattedPrice":"$ 2,499","productImage":"Images/2012_08_12_00_45_39_4539.jpg","productQuantity":"9","totalPrice":19992},
{"unitPrice": 4999,"currency":"$","productId":2,"retailerId":1,"productName":"XX","formattedPrice":"$ 4,999","productImage":"Images/2012_08_12_00_46_45_4645.jpg","productQuantity":2,"totalPrice":9998},
{"unitPrice":4555,"currency":"$","productId":1,"retailerId":1,"productName":"XXXXX","formattedPrice":"$ 4,555","productImage":"Images/2013_02_12_10_57_49_5749_9868.png","productQuantity":3,"totalPrice":13665}]
here is the relevent html,
<table>
<tbody data-bind="foreach: $root">
<tr>
<td><img width="45" height="45" alt="" data-bind="attr:{src: productImage}"/></td>
<td><span data-bind="html: productName"></span></td>
<td><span data-bind="html: formattedPrice"></span></td>
<td><input type="number" class="quantity" data-bind="value: productQuantity, attr:{'data-id': productId }" /></td>
<td><span data-bind="html: totalPrice"></span></td>
</tr>
</tbody>
</table>
Then I have created observable array as,
observableItems = ko.observableArray(items);
ko.applyBindings(observableItems);
Now I was able to get an specfic element using,
var obj = ko.utils.arrayFirst(list(), function (item) {
return item.productId === id;
});
However when I change,
item.productQuantity = 20;
But UI is not updating. Tried also,
item.productQuantity(item.productQuantity)
But getting error productQuantity is not a function
The above behavior is because only the array is an observable and not the individual elements within the array or the properties of each element.
When you do item.productQuantity = 20;, this will update the property but since it is not an observable, the UI does not get updated.
Similary, item.productQuantity(20) gives you an error because productQuantity is not an observable.
You should look at defining the object structure for each element of your array and then add elements of that type to your observable array. Once this is done, you will be able to do something like item.productQuantity(20) and the UI will update itself immediately.
EDIT Added the function provided by the OP :). This function will convert each property of the elements in an array to observables.
function convertToObservable(list)
{
var newList = [];
$.each(list, function (i, obj) {
var newObj = {};
Object.keys(obj).forEach(function (key) {
newObj[key] = ko.observable(obj[key]);
});
newList.push(newObj);
});
return newList;
}
END EDIT
If you are unable to change that piece of code, you can also do something like observableItems.valueHasMutated(). However, this is not a good thing to do as it signals to KO and your UI that the whole array has changed and the UI will render the whole array based on the bindings.
You can easily use ko.mapping plugin to convert object to become observable: http://knockoutjs.com/documentation/plugins-mapping.html
Convert regular JS entity to observable entity:
var viewModel = ko.mapping.fromJS(data);
Convert observable object to regular JS object:
var unmapped = ko.mapping.toJS(viewModel);

Categories