Knockout is slow when unchecking checkboxes on a large (1000) dataset - javascript

I am using this code, to check all checkboxes on my view.
var checked = self.includeAllInSoundscript();
var contents = self.filterContents(self.getFilters());
for (var i = 0; i < contents.length; i++) {
contents[i].includeInSoundscript(checked);
}
return true;
The checkbox
<input type="checkbox" data-bind="checked: includeInSoundscript" title="sometitle" />
This is what contents is:
(function (ko) {
ContentViewModel = function (data) {
this.orderId = data.orderId;
this.contentReferenceId = ko.observable(data.contentReferenceId);
this.includeInSoundscript = ko.observable();
});
This is the filter methods:
self.getFilters = function() {
var filterOrders = $.grep(self.orders(), function (order) {
return (order.usedInfilter());
});
var filterLocations = $.grep(self.locations(), function (location) {
return (location.usedInfilter());
});
return { orders: filterOrders, locations: filterLocations };
};
self.filterContents = function (filter) {
var filteredArray = self.contents();
if (filter.orders.length > 0) {
filteredArray = $.grep(filteredArray, function (content) {
return $.grep(filter.orders, function (order) {
return (order.orderId == content.orderId);
}).length > 0;
});
}
if (filter.locations.length > 0) {
filteredArray = $.grep(filteredArray, function (content) {
return $.grep(filter.locations, function (location) {
return $.inArray(location.location, content.orderedFrom().split('/')) != -1;
}).length > 0;
});
}
return filteredArray;
};
Checking all checkboxes is fast, but when i uncheck, it can take up to 20 seconds. Strange thing is when the filetered result is small, it still takes a bit longer, even if the filtered results is about 40, from a total set of 1000.
The checkbox is in a table, bound using data-bind="foreach: contents"
I have now removed some of the "unescessary" observables, for properties that most likely will not change, it then behaves slightly better, but still very slow, especially in firefox. The big question is, why is this behavior only on unchecking checkboxes, and not on filtering, sorting, checking, etc.
Notice: Its only unchecking the checkboxes, basically when "checked" is false, otherwise its fast.
Edit: I am only displaying 50 items at a time, but i am checking / unchecking all the filtered items. This, so that I have controll over what to post to the server.

This is what I use for this scenario. Maybe it will help you.
The checked binding can work with an array of selected items, but only supports storing strings in the array. I use a custom binding that supports storing objects in the array (like selectedOptions does):
ko.bindingHandlers.checkedInArray = {
init: function (element, valueAccessor) {
ko.utils.registerEventHandler(element, "click", function() {
var options = ko.utils.unwrapObservable(valueAccessor()),
array = options.array, // don't unwrap array because we want to update the observable array itself
value = ko.utils.unwrapObservable(options.value),
checked = element.checked;
ko.utils.addOrRemoveItem(array, value, checked);
});
},
update: function (element, valueAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor()),
array = ko.utils.unwrapObservable(options.array),
value = ko.utils.unwrapObservable(options.value);
element.checked = ko.utils.arrayIndexOf(array, value) >= 0;
}
};
The binding for each checkbox then looks like this:
<input type="checkbox" data-bind="checkedInArray: { array: $parent.selectedItems, value: $data }" />
The checkbox for selecting all items uses the normal checked binding and is attached to a writable computed observable:
this.allItemsSelected = ko.computed({
read: function() {
return this.selectedItems().length === this.items().length;
},
write: function(value) {
this.selectedItems(value ? this.items.slice(0) : [] );
},
owner: this
});
Example: http://jsfiddle.net/mbest/L3LeD/
Update: Knockout 3.0.0 introduced the checkedValue binding option that makes the above custom binding unnecessary. You can now bind the checkboxes like this:
<input type="checkbox" data-bind="checked: $parent.selectedItems, checkedValue: $data" />
Example: http://jsfiddle.net/mbest/RLLX6/

What happens to performance if you use jQuery to check/uncheck all the boxes?
$('#tableId').find('input[type=checkbox]').prop('checked', checked);
Alternatively, could you check all the boxes when you display them, rather than doing all of them in one go?
Also, you could try using the knockout.utils methods for filtering the observable arrays, I'd be interested to see if there's any performance difference there.
var filteredArray = ko.utils.arrayFilter(this.items(), function(item) {
return ko.utils.stringStartsWith(item.name().toLowerCase(), filter);
});
There is also a method for looping over an array and processing each element:
ko.utils.arrayForEach(this.items(), function(item) {
var value = parseFloat(item.priceWithTax());
if (!isNaN(value)) {
total += value;
}
});
Again, I have no idea if this will help with performance or not, though I think it's a bit better prettiness-wise!

Related

Knockoutjs arrayFilter multiple dropdowns

I have a question regarding arrayFilter in knockoutjs, how would i go about filtering my list with 2 different dropdowns which whould be related so if i have choosen 1 type of building but no area i should be shown all of that type of buildings, however if i where to choose a building option and an area option the filtering should account for that, ive been working on a prototype now for 2 days but cant figure out how to return the correct item in the arrayfilter.
http://jsfiddle.net/vGg2h/138/
Currently i made all my models and pastin in data via the viewmodel, and i got a filtered list hooked up, however i dont understand how to return the correct item back through the foreach filter and the arrayFilter, this is where it gets abit blurry.
self.filteredList = ko.computed(function () {
var filters = [];
filters.push(self.selectedBuilding());
filters.push(self.selectedArea());
var currentList = [];
ko.utils.arrayForEach(filters, function (filter) {
if (typeof filter !== "undefined") {
ko.utils.arrayFilter(self.products(), function (item) {
if (filter.id == item.areaId || filter.value == item.buildingId) {
currentList.push(item);
}
});
}
});
return currentList;
});
Thanks in advance for any answers!
You have two problems:
you are not correctly using ko.utils.arrayFilter: you have to return true or false depending on whether and item should be included in the end result or not. So you should not build your result inside the arrayFilter
you are always starting form the full list and not applying the filters one after the other, but incorrectly build the result in the arrayFilter which lead to combining your filters with OR and not with AND as you originally wanted
Your fixed code would like this:
self.filteredList = ko.computed(function () {
var filters = [];
filters.push(self.selectedBuilding());
filters.push(self.selectedArea());
var currentList = self.products();
ko.utils.arrayForEach(filters, function (filter) {
if (typeof filter !== "undefined") {
currentList = ko.utils.arrayFilter(currentList, function (item) {
if (filter.id == item.areaId || filter.value == item.buildingId) {
return true;
}
});
}
});
return currentList;
});
Demo JSFiddle
Two better see the AND filtering with reusing the same list you can rewrite your code to do the filtering in two separate steps:
self.filteredList = ko.computed(function () {
var currentList = self.products();
if (self.selectedBuilding())
{
currentList = ko.utils.arrayFilter(currentList, function(item) {
return self.selectedBuilding().value == item.buildingId;
});
}
if (self.selectedArea())
{
currentList = ko.utils.arrayFilter(currentList, function(item) {
return self.selectedArea().id == item.areaId;
});
}
return currentList;
});
In this code it is more clearer that you start from the full list and then apply the different filters one by one, further and further filtering the original list.
Demo JSFiddle
Note: if you initially want to start with an empty list (like in your original code) then you can just return an empty array if all of your filters are empty:
self.filteredList = ko.computed(function () {
if (!self.selectedBuilding() && !self.selectedArea())
return [];
//...
};
Demo JSFiddle.

Curious if angularJs has better method to calculate number of checkboxes

I need to check if any checkbox is checked. so i am doing it like
self.isButtonEnabled = function() {
var selectLineCheckboxs = document.getElementsByClassName('selectLineRadioInput'),
i = 0, checkboxLength = selectLineCheckboxs.length - 1;
for (i = 0; i <= checkboxLength; i++) {
if (selectLineCheckboxs[i].checked) {
self.selectLineChecked = true;
break;
} else {
self.selectLineChecked = false;
}
}
return self.selectLineChecked;
};
in return i get true if any checkbox is checked.
so quite simple,
Now here i am looking if we can do the same with angularJs with any better approach and i do not want to use watch() function in angular.
I can help with your some code to convert it to look like in Angular way.
use angular.element (provided by jQLite to get element) as instead of document.getElementsByClassName
You could use $filter while checking attribute is checked or not
CODE
self.isButtonEnabled = function() {
var selectLineCheckboxs = angular.element('.selectLineRadioInput');
var checkedValues = $filter('filter')(selectLineCheckboxs, { 'checked': true }); //do filtering and contains check value
self.selectLineChecked = checkedValues.length > 0 ? true : false;
return self.selectLineChecked;
};
Note: You should add $filter dependency on your controller before using $filter
Update
I'd suggest you to create your own custom filter that could be usable in multiple purposes, or dynamically check property value is true or not. I know your code is as same as you ask in answer, but I putted some of your code as reusable component, which can dynamically work for any property value to check is true or not.
Filter
.filter('isPropertyTrue', function () {
return function (elements, property) {
var returnArray = [];
angular.forEach(elements, function (val, index) {
if (val[property]) returnArray.push(val)
});
return returnArray;
}
});
Code
$scope.isButtonEnabled = function () {
var selectLineCheckboxs = document.getElementsByClassName('selectLineRadioInput');
var checkedValues = $filter('isPropertyTrue')(selectLineCheckboxs, 'checked');
self.selectLineChecked = checkedValues.length > 0 ? true : false;
return self.selectLineChecked;
};
JSFiddle
Hope this could help you, Thanks.
i have been using it this way with ng-bind variable in scope $scope.Items .and the binded variable can be used to see what all items are checked.
angular.forEach($scope.Items,function(key,value)
{
if(key.Selected)
{
counter++;
}
});
Here is a JSFiddle to illustrate the same

Knockout.js: computed observable not updating as expected

Edit: Added code for function populateDropdown and function isSystemCorrect (see bottom)
Edit 2 I have narrowed it down a bit and the problem seems to arise in the arrayFilter function in the computed observable. This returns an empty array, no matter what I try. I have checked that self.testsuites() looks ok right before filtering, but the filtering still fails.
I have a problem with my computed observable, filteredTestsuites.
As you can see from the screendump, the testsuites observable is populated correctly, but the computed observable remains empty. I have also tried choosing another option than "Payment" from the dropdown menu, to see if this will trigger the observable, it did not.
I would think the computed observable would be updated every time self.testsuites() or self.dropdownSelected() was changed, but it doesnt seem to trigger on neither of them.
What am I doing wrong here?
I simply want to make the computed observable filter the testsuites after the chosen dropdown option, every time either of them change.
function ViewModel() {
var self = this;
// The item currently selected from a dropdown menu
self.dropdownSelected = ko.observable("Payment");
// This will contain all testsuites from all dropdown options
self.testsuites = ko.mapping.fromJS('');
// This will contain only testsuites from the chosen dropdown option
self.filteredTestsuites = ko.computed(function () {
return ko.utils.arrayFilter(self.testsuites(), function (testsuite) {
return (isSystemCorrect(testsuite.System(), self.dropdownSelected()));
});
}, self);
// Function for populating the testsuites observableArray
self.cacheTestsuites = function (data) {
self.testsuites(ko.mapping.fromJS(data));
};
self.populateDropdown = function(testsuiteArray) {
for (var i = 0, len = testsuiteArray().length; i < len; ++i) {
var firstNodeInSystem = testsuiteArray()[i].System().split("/")[0];
var allreadyExists = ko.utils.arrayFirst(self.dropdownOptions(), function(option) {
return (option.Name === firstNodeInSystem);
});
if (!allreadyExists) {
self.dropdownOptions.push({ Name: firstNodeInSystem });
}
}
};
}
$(document).ready(function () {
$.getJSON("/api/TestSuites", function (data) {
vm.cacheTestsuites(data);
vm.populateDropdown(vm.testsuites());
ko.applyBindings(vm);
});
}
Function isSystemCorrect:
function isSystemCorrect(system, partialSystem) {
// Check if partialSystem is contained within system. Must be at beginning of system and go
// on to the end or until a "/" character.
return ((system.indexOf(partialSystem) == 0) && (((system[partialSystem.length] == "/")) || (system[partialSystem.length] == null)));
}
As suggested in a comment - rewrite the cacheTestsuites method:
self.testsuites = ko.observableArray();
self.filteredTestsuites = ko.computed(function () {
return ko.utils.arrayFilter(self.testsuites(), function (testsuite) {
return (isSystemCorrect(testsuite.System(), self.dropdownSelected()));
});
});
self.cacheTestsuites = function (data) {
var a = ko.mapping.fromJS(data);
self.testsuites(a());
};
The only thing different here is the unwrapping of the observableArray from the mapping function.

knockout sortable with computed observable not working

jsfiddle example. Like the title says I am trying to use a computed observable along with rniemeyer knockout sortable example. I keep getting
the write method needs to be implemented
This error is viewable in the developer console.
I have a write method implement on my ko.computed but it still errors out. What I am I doing wrong?
html and javascript below
<div id="main">
<h3>Tasks</h3>
<div class="container" data-bind="sortable: tasks">
<div class="item">
<span data-bind="visible: !$root.isTaskSelected($data)">
</span>
<span data-bind="visibleAndSelect: $root.isTaskSelected($data)">
<input data-bind="value: name, event: { blur: $root.clearTask }" />
</span>
</div>
</div>
</div>
var Task = function(first,last) {
var self = this;
self.firstName = ko.observable(first);
self.lastName = ko.observable(last);
self.TestName = ko.computed({
read: function (){
return self.firstName() + " " + self.lastName();
},
write: function (item) {
console.log(item);
}
});
return self;
}
var ViewModel = function() {
var self = this;
self.testTasks = ko.observableArray([
new Task("test","one"),
new Task("test","two"),
new Task("test","three")
]);
self.tasks = ko.computed({
read: function() { return self.testTasks();},
write: function(item) {console.log(item);}
});
self.selectedTask = ko.observable();
self.clearTask = function(data, event) {
if (data === self.selectedTask()) {
self.selectedTask(null);
}
if (data.name() === "") {
self.tasks.remove(data);
}
};
self.addTask = function() {
var task = new Task("new");
self.selectedTask(task);
self.tasks.push(task);
};
self.isTaskSelected = function(task) {
return task === self.selectedTask();
};
};
//control visibility, give element focus, and select the contents (in order)
ko.bindingHandlers.visibleAndSelect = {
update: function(element, valueAccessor) {
ko.bindingHandlers.visible.update(element, valueAccessor);
if (valueAccessor()) {
setTimeout(function() {
$(element).find("input").focus().select();
}, 0); //new tasks are not in DOM yet
}
}
};
ko.applyBindings(new ViewModel());
As the very author of this plugin says here, you can't use a computed observable; the sortable plugin depends on an actual observable array.
Which makes sense when you think about it: the plugin is actually manipulating the various indexes of the array as you re-sort the elements.
Here's a "writableComputedArray" if you want the best of both worlds. If you add/remove from the array, and a subsequent re-compute of the observable performs the same add/remove, subscribers will not get notified the second time. However, it's your responsibility to make sure that there are no discrepancies between the computation of the array and what actually gets added/removed. You can accomplish this by making the necessary changes in the sortable binding's afterMove event.
ko.writeableComputedArray = function (evaluatorFunction) {
// We use this to get notified when the evaluator function recalculates the array.
var computed = ko.computed(evaluatorFunction);
// This is what gets returned to the caller and they can subscribe to
var observableArray = ko.observableArray(computed());
// When the computed changes, make the same changes to the observable array.
computed.subscribe(function (newArray) {
// Add any new values
newArray.forEach(function (value) {
var i = observableArray.indexOf(value);
if (i == -1) {
// It's a new value, push it
observableArray.unshift(value);
}
});
// Remove any old ones. Loop backwards since we're removing items from it.
for (var valueIndex = observableArray().length - 1; valueIndex >= 0; valueIndex--) {
var value = observableArray()[valueIndex];
var i = newArray.indexOf(value);
if (i == -1) {
// It's an old value, remove it
observableArray.remove(value);
}
}
});
return observableArray;
};

How to define a custom binding who use previous value to determine class in Knockout?

I need to bind a table with knockout, and I would like the table cell to get a different css class if the new value is higher or lower of the previous.
I have in mind different possibilities, such as storing the previous value in the bindingContext and have a function which returns the right class, but is it possible to add a custom binding handler which receives the previous value and the new value?
Although Jeff's and SÅ‚awomir's answers would work, I found an alternative that doesn't need any change to the view model nor relies on altering the DOM element object.
function subscribeToPreviousValue(observable, fn) {
observable.subscribe(fn, this, 'beforeChange');
}
ko.bindingHandlers['bindingWithPrevValue'] = {
init: function (element, valueAccessor) {
var observable = valueAccessor();
var current = observable();
console.log('initial value is', current);
subscribeToPreviousValue(observable, function (previous) {
console.log('value changed from', previous, 'to', current);
});
}
};
Naturally, that will only work if the bound property is an observable.
I looked into knockout source and I suppose that you can't access previous value inside update method of bindingHandler but you can store it inside element
ko.bindingHandlers['bindingWithPrevValue'] = {
update: function (element, valueAccessor) {
var prevValue = $(element).data('prevValue');
var currentValue = valueAccessor();
$(element).data('prevValue', currentValue());
// compare prevValue with currentValue and do what you want
}
};
What you could do is create an extender to extend the observables that you wish to track the previous values of. You could then inspect the previous value to do as you wish.
Just pass in the name of the property that will hold the previous value.
ko.extenders.previousValue = function (target, propertyName) {
var previousValue = ko.observable(null);
target[propertyName] = ko.computed(previousValue);
target.subscribe(function (oldValue) {
previousValue(oldValue);
}, target, 'beforeChange');
return target;
};
Then to use it:
function ViewModel() {
this.value = ko.observable('foo').extend({ previousValue: 'previousValue' });
}
var vm = new ViewModel();
console.log(vm.value()); // 'foo'
console.log(vm.value.previousValue()); // null
vm.value('bar');
console.log(vm.value()); // 'bar'
console.log(vm.value.previousValue()); // 'foo'
In your case, you could probably use something like this:
function TableCell(value) {
this.value = ko.observable(value).extend({ previousValue: 'previousValue' });
this.cssClass = ko.computed(function () {
// I'm assuming numbers
var current = Number(this.value()),
previous = Number(this.value.previousValue());
if (current < previous)
return 'lower';
else if (current > previous)
return 'higher';
}, this);
}

Categories

Resources