Knockout computed vs. subscription, timing issues - javascript

Just found out, that in KnockoutJS subscription functions are evaluated before dependent computables and need someone who can commit that, because I can't find anything about Knockouts timing in the docs or discussion forums.
That means: If I have a model like this...
var itemModel = function (i) {
var self = this;
self.Id = ko.observable(i.Id);
self.Title = ko.observable(i.Title);
self.State = ko.observable(i.State);
};
var appModel = function () {
var self = this;
self.Items = ko.observableArray() // <-- some code initializes an Array of itemModels here
self.indexOfSelectedItem = ko.observable();
self.selectedItem = ko.computed(function () {
if (self.indexOfSelectedItem() === undefined) {
return null;
}
return self.Items()[self.indexOfSelectedItem()];
});
};
where I want to keep track of the selected array item with an observable index field, and I subscribe to this index field like this...
appModel.indexOfSelectedItem.subscribe(function () {
// Do something with appModel.selectedItem()
alert(ko.toJSON(appModel.selectedItem()));
}
...the subscription function is evaluated before the computed is reevaluated with the new index value, so I will get the selectedItem() that corresponds to the last selected Index and not the actual selected Index.
Two questions:
Is that right?
Then why should I make use of ko.computed() if a simple function gets me the current selected Item every time I call it, while ko.computed gets evaluated at anytime where everything is done already and I dont need it anymore?

By default all computeds in Knockout are evaluated in an eager fashion, not lazily (i.e., not when you first access them).
As soon as one dependency changes, all all subscriptions are notified and all connected computeds are re-evaluated. You can change that behavior to "lazy" by specifying the deferEvaluation option in a computed observable, but you cannot do this for a subscription.
Hoewever, I think there is no need to depend on the index of the selected item. In fact, that's even bad design because you are not really intested in the numerical value of the index, but rather in the item it represents.
You could reverse the dependencies by creating a writeable computed observable that gives you the index of the currently selected item (for diplay purposes) and allows to change it as well (for convenience).
function AppModel() {
var self = this;
self.Items = ko.observableArray();
self.selectedItem = ko.observable();
self.indexOfSelectedItem = ko.computed({
read: function () {
var i,
allItems = self.Items(),
selectedItem = self.selectedItem();
for (i = 0; i < allItems.length; i++) {
if (allItems[i] === selectedItem) {
return i;
}
}
return -1;
},
write: function (i) {
var allItems = self.Items();
self.selectedItem(allItems[i]);
}
});
}
Knockout favors storing/handling the actual values instead of just indexes to values, so it would probably not be difficult to make the necessary changes to your view. Just make everything that previously wrote to indexOfSelectedItem now write to selectedItem directly. Dependencies on selectedItem will continue to work normally.
In a well-designed Knockout application you will rarely ever have the need to handle the index of an array item. I'd recommend removing the write part of the computed once everything works.
See: http://jsfiddle.net/4hLLn/

Related

Knockout computed property fires on loading

I'm starting with knockout and my computed observable seems to fire always when the viewmodel is instantiated and i don't know why.
I've reduced the problem to the absurd just for testing: the computed property just prints a message in the console and it is not binded to any element at the DOM. Here it is:
(function() {
function HomeViewModel() {
var self = this;
(...)
self.FullName = ko.computed(function () {
console.log("INSIDE");
});
(...)
};
ko.applyBindings(new HomeViewModel());
})();
How can it be avoided?
Update:
Here is the full code of the ViewModel just for your better understanding:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.FilterByName = ko.observable('');
self.FilterByLastName = ko.observable('');
self.FilteredTeachers = ko.observableArray([]);
self.FilteredStudents = ko.observableArray([]);
self.FilteredUsersComputed = ko.computed(function () {
var filteredTeachers = self.teachers().filter(function (user) {
return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
);
});
self.FilteredTeachers(filteredTeachers);
var filteredStudents = self.students().filter(function (user) {
return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
);
});
self.FilteredStudents(filteredStudents);
$("#LLAdminBodyMain").fadeIn();
}).extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.FilteredUsersComputed.subscribe(function () {
setTimeout(function () { $("#LLAdminBodyMain").fadeOut(); }, 200);
}, null, "beforeChange");
$.getJSON("/api/User/Teacher", function (data) {
self.teachers(data);
});
$.getJSON("/api/User/Student", function (data) {
self.students(data);
});
}
ko.applyBindings(new HomeViewModel());
})();
I need it to not be executed on load because on load the self.students and self.teachers arrays are not jet populated.
NOTE: Just want to highlight that in both codes (the absurd and full), the computed property is executed on loading (or when the ViewModel is first instantiated).
There are two main mistakes in your approach.
You have a separate observable for filtered users. That's not necessary. The ko.computed will fill that role, there is no need to store the computed results anywhere. (Computeds are cached, they store their own values internally. Calling a computed repeatedly does not re-calculate its value.)
You are interacting with the DOM from your view model. This should generally be avoided as it couples the viewmodel to the view. The viewmodel should be able operate without any knowledge of how it is rendered.
Minor points / improvement suggestions:
Don't rate-limit your filter result. Rate-limit the observable that contains the filter string.
Don't call your computed properties ...Computed - that's of no concern to your view, there is no reason to point it out. For all practical purposes inside your view, computeds and observables are exactly the same thing.
If teachers and students are the same thing, i.e. user objects to be displayed in the same list, why have them in two separate lists? Would it not make more sense to have a single list in your viewmodel, so you don't need to filter twice?
Observables are functions. This means
$.getJSON("...", function (data) { someObservable(data) });
can be shortened to
$.getJSON("...", someObservable);.
Here is a better viewmodel:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.filterByName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.filterByLastName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
function filterUsers(userList) {
var name = self.filterByName().toUpperCase(),
lastName = self.filterByLastName().toUpperCase(),
allUsers = userList();
if (!name && !lastName) return allUsers;
return allUsers.filter(function (user) {
return (!name || user.name.toUpperCase().includes(name)) &&
(!lastName || user.lastName.toUpperCase().includes(lastName));
});
}
self.filteredTeachers = ko.computed(function () {
return filterUsers(self.teachers);
});
self.filteredStudents = ko.computed(function () {
return filterUsers(self.students);
});
self.filteredUsers = ko.computed(function () {
return self.filteredTeachers().concat(self.filteredStudents());
// maybe sort the result?
});
$.getJSON("/api/User/Teacher", self.teachers);
$.getJSON("/api/User/Student", self.students);
}
With this it does not matter anymore that the computeds are calculated immediately. You can bind your view to filteredTeachers, filteredStudents or filteredUsers and the view will always reflect the state of affairs.
When it comes to making user interface elements react to viewmodel state changes, whether the reaction is "change HTML" or "fade in/fade out" makes no difference. It's not the viewmodel's job. It is always the task of bindings.
If there is no "stock" binding that does what you want, make a new one. This one is straight from the examples in the documentation:
// Here's a custom Knockout binding that makes elements shown/hidden via jQuery's fadeIn()/fadeOut() methods
// Could be stored in a separate utility library
ko.bindingHandlers.fadeVisible = {
init: function(element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function(element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
It fades in/out the bound element depending the bound value. It's practical that the empty array [] evaluates to false, so you can do this in the view:
<div data-bind="fadeVisible: filteredUsers">
<!-- show filteredUsers... --->
</div>
A custom binding that fades an element before and after the bound value changes would look like follows.
We subscribe to value during the binding's init phase.
There is no update phase in the binding, everything it needs to do is accomplished by the subscriptions.
When the DOM element goes away (for example, because a higher-up if or foreach binding triggers) then our binding cleans up the subscriptions, too.
Let's call it fadeDuringChange:
ko.bindingHandlers.fadeDuringChange = {
init: function(element, valueAccessor) {
var value = valueAccessor();
var beforeChangeSubscription = value.subscribe(function () {
$(element).delay(200).fadeOut();
}, null, "beforeChange");
var afterChangeSubscription = value.subscribe(function () {
$(element).fadeIn();
});
// dispose of subscriptions when the DOM node goes away
// see http://knockoutjs.com/documentation/custom-bindings-disposal.html
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
// see http://knockoutjs.com/documentation/observables.html#explicitly-subscribing-to-observables
beforeChangeSubscription.dispose();
afterChangeSubscription.dispose();
});
}
};
Usage is the same as above:
<div data-bind="fadeDuringChange: filteredUsers">
<!-- show filteredUsers... --->
</div>

KnockoutJS how to implement a computed observable for each item of an array?

I need to have a computed observable for each element of an array. The resolution of this computed observable needs to rely on other properties existing in the context of each array element.
Please check the following example case:
This is the result of a KnockoutJS foreach binding to a nested array, having a group = category, and items nested in each category.
In my scenario, and initial array has "plain" properties in its elements, as if retrieved from a backend, to later be mapped into another array with the same structure, but with the quantity property deliberately set to a knockout observable. In this mapping process where I set quantity to be a knockout observable, I also need to set another computed observable property to represent the item total (price property x quantity property). This item total computed observable has to happen for each element of the array, and has to depend on the content of price and quantity property in the context of each array element.
Please check the following screen shot, imagine a bar menu where quantity is entered on the input tag bound to observable array elements, and upon changing each quantity, a computed observable bound to a td tag should reflect the item total calculation.
Please also check the JS Fiddle for this
As you will notice in the JS Fiddle, this part in the array mapping function to implement the totalComputed property is obviously wrong, it does not work.
totalComputed: ko.computed(function() {
return element.items[indexInArrayItems].quantity * element.items[indexInArrayItems].price;
})
Can you help, preferrably making my JS Fiddle work?
You need to take advantage of knockout's power by creating observable variables and observableArray then have dependencies in your computed function in order to have your computed gets updated automatically every time any of its dependencies change.
Here is based on your JS Fiddle example : https://jsfiddle.net/rnhkv840/22/
var MainViewModel = function(){
var self = this;
self.dataSource = ko.observableArray([]);
//probably inside your ajax success
self.dataSource($.map(dataFromBackend, function (item) {
return new CategoryViewModel(item);
}));
}
var CategoryViewModel = function(data){
var self = this;
self.Items = ko.observableArray($.map(data.items, function (item) {
return new ItemViewModel(item);
}));
self.category = ko.observable(data.category);
}
var ItemViewModel = function(data){
var self = this;
self.description = ko.observable(data.description);
self.price = ko.observable(data.price);
self.quantity = ko.observable(data.quantity);
self.totalComputed = ko.computed(function () {
return self.price() * self.quantity();
});
}
var vm = new MainViewModel();
ko.applyBindings(vm);

Determine if observable has changed in computed

I am implementing a cache function in a computed observable.
Is there any way to invalidate the cache below if the items collection differs since the last call?
I have seen examples of dirty checking where a serialized version of the observable is used to determine if the collection has changed, but it's too expensive for me, since there may be hundreds of items.
var itemCache;
var manipulatedItems = ko.pureComputed(function(){
var items = someObervable();
if(!itemCache /* || someObervable.hasChangedSinceLastCall */) {
itemCache = heavyWork(items);
}
return itemCache;
});
var heavyWork = function(items){
// do some heavy computing with items
return alteredItems;
};
In my viewmodel:
myViewModel.itemList = ko.pureComputed(function(){
var result = manipulatedItems();
return result;
});
Since computed observables always cache the last value, there's no reason to store it separately. In fact, storing it separately can cause trouble with getting the latest data in your application.
var manipulatedItems = ko.pureComputed(function () {
var items = someObervable();
return heavyWork(items);
});
var heavyWork = function (items) {
// do some heavy computing with items
return alteredItems;
};
Yes, but you need to use .subscribe and keep the relevant moments in vars inside your own closure. There is no "last-modified-moment" property on observables or inside ko utils to be found.
In your repro, you could do something like this:
var lastChange = new Date();
var lastCacheRebuild = null;
var itemCache;
someObervable.subscribe(function(newObsValue) { lastChange = new Date(); });
var manipulatedItems = ko.pureComputed(function(){
var items = someObervable();
if(!itemCache || !lastCacheRebuild || lastChange > lastCacheRebuild) {
lastCacheRebuild = new Date();
itemCache = heavyWork(items);
}
return itemCache;
});
As far as the repro is concerned you could even put the items = someObservable() bit inside the if block.
PS. This is not recursive, i.e. the subscription is only on someObservable itself, not on the observable properties of things inside that observable. You'd have to manually craft that, which is specific to the structure of someObservable.

Knockout arrayForEach undefined property

I'm having trouble trying to get a number from each item in a knockout observable array and add the numbers together and assign it to another computed variable. Here's what I have right now...
Semesters: ko.observableArray([
{
semesterName: "Fall",
semesterCode: "300",
PlannedCourses: ko.observableArray([]),
totalCredits: ko.computed(function(){
var total = 0;
ko.utils.arrayForEach(this.PlannedCourses, function (course) {
total += course.MinHours();
});
return total;
}),
},
...
What I'm trying to do is, in the totalCredits variable, I'm trying to iterate through the PlannedCourses array and get the MinHours variable for each item and add them together in the total variable. Then I return it to the totalCredits item in the Semesters array. The issue I'm having is getting the PlannedCourses variable in the ko.utils.arrayForEach part. I'm getting an undefined on it and I'm not sure why. I think it's a simple syntax error but I can't see what's wrong.
The PlannedCourses observable array is a dynamic object that is getting the list of PlannedCourses properly. It's defined in the context of itself but I'm not passing it to the totalCredits computed function properly.
I hope this is clear enough. Thank you for your help!
Note: All the rest of the code is working as intended. The only part that isn't working is the totalCredits computed function. I'm not sure if anything within the ko.utils.arrayForEach is working as I haven't been able to get that far.
You're going to need to change the way you populate your Semesters observable array to use a constructor function in order to get a reference to the correct scope for this:
function semester(name, code) {
this.Name = name;
this.Code = code;
this.PlannedCourses = ko.observableArray([]);
this.totalCredits = ko.computed(function(){
var total = 0;
ko.utils.arrayForEach(this.PlannedCourses(), function (course) {
//Note the change to "this.PlannedCourses()" above to get the underlying array
total += course.MinHours();
});
return total;
}, this); //now we can pass "this" as the context for the computed
}
See how we can now pass in an object to the second argument for ko.computed to use as the context for this in the inner function. For more information, see the knockout docs: Managing 'this'.
You then create new instances of semester when populating your array:
Semesters: ko.observableArray([
new semester("Fall", "300"),
new semester(...)
]);
This approach also means you have a consistent way of creating your semester objects (the computed is only defined once for one thing), rather than possibly incorporating typos etc in any repetition you may originally have had.
As others already mentioned your this is not what you think it is. In your case the context should be passed to the computed as follows:
totalCredits: ko.computed(function() {
// Computation goes here..
}, this)
Another approach could be to store the correct this to some local variable during the object creation (ex. var self = this; and then use self instead of this).
However, ko.utils.arrayForEach doesn't work with observable arrays but works on pure JavaScript arrays, so you should unwrap the observable array to access the elements of the underlying array:
ko.utils.arrayForEach(this.PlannedCourses(), function(course) {
// ...
});
// Or
ko.utils.arrayForEach(ko.unwrap(this.PlannedCourses), function(course) {
// ...
});
The scope (this) isn't what you think it is.
See http://knockoutjs.com/documentation/computedObservables.html
try adding your context, like the following:
Semesters: ko.observableArray([
{
semesterName: "Fall",
semesterCode: "300",
PlannedCourses: ko.observableArray([]),
totalCredits: ko.computed(function(){
var total = 0;
ko.utils.arrayForEach(this.PlannedCourses, function (course) {
total += course.MinHours();
});
return total;
}, this), // new context passed in here
},
...
Doing this passes in the context of the array item itself into your computed function.
Edit:
you may need to access the Semesters object inside you loop, and add some way to reference the current item:
Semesters: ko.observableArray([
{
semesterName: "Fall",
semesterCode: "300",
PlannedCourses: ko.observableArray([]),
totalCredits: ko.computed(function(){
var total = 0;
for( var i = 0, len = Semesters().length; i < len; i++ ) {
// check current array item, possibly add an id?
if( Semesters()[i].semesterName === "Fall" &&
Semesters()[i].semesterCode === "300" ) {
ko.utils.arrayForEach(Semesters()[i].PlannedCourses, function (course) {
total += course.MinHours();
});
break; // done searching
}
}
return total;
})
},

Creating methods on the fly

Hi I'm trying to author a jQuery plugin and I need to have methods accessible to elements after they are initialized as that kind of object, e.g.:
$('.list').list({some options}); //This initializes .list as a list
//now I want it to have certain methods like:
$('.list').find('List item'); //does some logic that I need
I tried with
$.fn.list = function (options) {
return this.each(function() {
// some code here
this.find = function(test) {
//function logic
}
}
}
and several other different attempts, I just can't figure out how to do it.
EDIT:
I'll try to explain this better.
I'm trying to turn a table into a list, basically like a list on a computer with column headers and sortable items and everything inbetween. You initiate the table with a command like
$(this).list({
data: [{id: 1, name:'My First List Item', date:'2010/06/26'}, {id:2, name:'Second', date:'2010/05/20'}]
});
.list will make the <tbody> sortable and do a few other initial tasks, then add the following methods to the element:
.findItem(condition) will allow you to find a certain item by a condition (like findItem('name == "Second"')
.list(condition) will list all items that match a given condition
.sort(key) will sort all items by a given key
etc.
What's the best way to go about doing this?
If you want these methods to be available on any jQuery object, you will have to add each one of them to jQuery's prototype. The reason is every time you call $(".list") a fresh new object is created, and any methods you attached to a previous such object will get lost.
Assign each method to jQuery's prototype as:
jQuery.fn.extend({
list: function() { .. },
findItem: function() { .. },
sort: function() { .. }
});
The list method here is special as it can be invoked on two occasions. First, when initializing the list, and second when finding particular items by a condition. You would have to differentiate between these two cases somehow - either by argument type, or some other parameter.
You can also use the data API to throw an exception if these methods are called for an object that has not been initialized with the list plugin. When ('xyz').list({ .. }) is first called, store some state variable in the data cache for that object. When any of the other methods - "list", "findItem", or "sort" are later invoked, check if the object contains that state variable in its data cache.
A better approach would be to namespace your plugin so that list() will return the extended object. The three extended methods can be called on its return value. The interface would be like:
$('selector').list({ ... });
$('selector').list().findOne(..);
$('selector').list().findAll(..);
$('selector').list().sort();
Or save a reference to the returned object the first time, and call methods on it directly.
var myList = $('selector').list({ ... });
myList.findOne(..);
myList.findAll(..);
myList.sort();
I found this solution here:
http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
This seems to do exactly what I need.
(function($) {
var TaskList = function(element, options)
{
var $elem = $(element);
var options = $.extend({
tasks: [],
folders: []
}, options || {});
this.changed = false;
this.selected = {};
$elem.sortable({
revert: true,
opacity: 0.5
});
this.findTask = function(test, look) {
var results = [];
for (var i = 0,l = options.tasks.length; i < l; i++)
{
var t = options['tasks'][i];
if (eval(test))
{
results.push(options.tasks[i]);
}
}
return results;
}
var debug = function(msg) {
if (window.console) {
console.log(msg);
}
}
}
$.fn.taskList = function(options)
{
return this.each(function() {
var element = $(this);
if (element.data('taskList')) { return; }
var taskList = new TaskList(this, options);
element.data('taskList', taskList);
});
}
})(jQuery);
Then I have
$('.task-list-table').taskList({
tasks: eval('(<?php echo mysql_real_escape_string(json_encode($tasks)); ?>)'),
folders: eval('(<?php echo mysql_real_escape_string(json_encode($folders)); ?>)')
});
var taskList = $('.task-list-table').data('taskList');
and I can use taskList.findTask(condition);
And since the constructor has $elem I can also edit the jQuery instance for methods like list(condition) etc. This works perfectly.
this.each isn't needed. This should do:
$.fn.list = function (options) {
this.find = function(test) {
//function logic
};
return this;
};
Note that you'd be overwriting jQuery's native find method, and doing so isn't recommended.
Also, for what it's worth, I don't think this is a good idea. jQuery instances are assumed to only have methods inherited from jQuery's prototype object, and as such I feel what you want to do would not be consistent with the generally accepted jQuery-plugin behaviour -- i.e. return the this object (the jQuery instance) unchanged.

Categories

Resources