How do I initially load an observable array asynchronously - javascript

This is my viewmodel:
function PitchViewModel() {
var self = this;
self.selectedPitch = ko.observable();
self.pitches = ko.computed(function () {
return $.getJSON("/api/Pitch", function (data) {
var obs = ko.mapping.fromJS([])
ko.mapping.fromJS(data, {}, obs) ;
// seems to work, but somehow observables are changed back into objects after binding
return obs();
})}, this).extend({ asyncArray: [{ Id: 1, PitchNumber: 1, Length: 0, Width: 0, HasElectricity: false, Name: "Test" }] });
// behaviours
self.selectPitch = function () {
console.log("inside selectPitch");
self.selectedPitch(this);
}
}
I'm using an async extender as shown here: asynchronous computed observables
adapted a little bit for observablearrays like so (in line 3):
var plainObservable = ko.observableArray(initialValue), currentDeferred;
In my view i do this:
var domNode = $('#content')[0];
var pitchViewModel = new PitchViewModel();
ko.applyBindings(pitchViewModel, domNode);
It seems to work fine. The binding happens asynchronously. Pretty cool so far.
However!
When (in Chrome) I put a breakpoint on
return obs();
the obs() function is an observableArray and has objects with observable properties.
But when I break on
console.log("inside selectPitch");
and inspect self.pitches() it has become a 'normal' array with objects the have 'normal' (not observable) properties.
What am I missing here?
BTW: I have tried using a an observableArray for self.pitches instead of the computable. But then the ko.applybindings happens before the initialization of the observable array, leading to binding errors.
Thanks for your help.
Frans

I have not tried to run your code but what I SUSPECT happens is that the extender uses the result of the ajax call returned as a Deferred/Promise. Your processing of the results happens in the callback function and is not used for anything afterwards.
You should not mix Deferreds and callbacks like this. Try the following instead:
self.pitches = ko.computed(function () {
return $.getJSON("/api/Pitch")
.then(function(data) {
return ko.mapping.fromJS(data);
});
}).extend({ asyncArray: [...] });

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>

retain original value with ko.mapping.fromJS

I have a value defined as ko.observableArray([]) and use ko.mapping.fromJS to append data from ajax into it. However, when i use ko.mapping.fromJS again, the new data fetch from ajax call replace self.SampleArray instead of append new data. I would like to retain the previous data. How could it be done?
function ViewModel() {
var self = this;
self.SampleArray = ko.observableArray([]);
$.ajax({
..
..
success: function() {
ko.mapping.fromJS(data, {}, self.sampleArray());
}
})
}
Try pushing it into observableArray rather replacing it
viewModel:
function ViewModel() {
var self = this;
self.sampleArray = ko.observableArray([{
'Hours': 0.5
}])
setTimeout(function () {
alert('Mock of ajax call')
var newData = ko.mapping.fromJS(data1)();
self.sampleArray.push.apply(self.sampleArray,newData)
}, 2000);
}
ko.applyBindings(new ViewModel())
Advantage of push.apply over traditional loop statements:
If your array / collection has multiple items and if you add
(array.push(item) one by one then the subscribers will be notified for
each and every push / add operation. Then the UI will have that number
of refresh. That will hurt the UI page performances.
But if you use array.push.appy , then you can still add multiple
items, but subscribers will be notified only once.
That is the difference and the advantage / usage of this
array.push.apply function.
sample working fiddle here
sample working fiddle with utils.forEach here

Pager.js: How to lazy load bindings

I am trying to figure out how to use Pager.js in conjunction with Knockout.js to lazy-load a page and bind its contents. I am trying to translate the demo example, but I am not familiar with require.js and am just getting lost.
I have spent several hours trying to reimplement the system using jQuery's getJSON instead of require and define, but the bindings are failing silently. I am having two issues:
The view model is a JSON array, so I don't know what the array is called
The code is not actually doing a getJSON request (nothing in the logs). And is failing silently.
Here is the code:
<div data-bind="page: {id: 'history', title: 'History', withOnShow: $root.getHistory }">
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.getHistory = function () {
return function (f) {
$.getJSON("#{HistoryR}", function (data) {
viewModel.history = ko.mapping.fromJS(data, {});
f(viewModel.history);
});
}
}
};
$.getJSON("#{HomeR}", function (data) {
viewModel = new ViewModel(data);
pager.extendWithPage(viewModel);
ko.applyBindings(viewModel);
pager.start();
});
I refactored the code some, to fit in with huocp's answer:
self.getExamHistory = function (f) {
$.getJSON("#{ExamHistoryR}", function (data) {
self.history = ko.mapping.fromJSON(data, {});
f(self.history);
});
}
and the getJSON call is getting triggered (and I see the response in my console), but my viewModel.history is still empty.
You did a wrong wrap of withOnShow callback function.
Remove the wrap, you should be fine :-)
self.getHistory = function (f) {
$.getJSON("#{HistoryR}", function (data) {
self.history = ko.mapping.fromJS(data); // can u try self instead of viewModel
f(self.history);
});
};
The reason the Pager.js demo page with extra wrap, is that it use withOnShow: requireVM('invention'), not withOnShow: requireVM. It uses the return value of requireVM function, not the function itself.

How to map to an Array coming from server object using Knockout Mapping plugin and have some computed?

reading this exchange in stackoverflow titled "How to map to an Array coming from server object using Knockout Mapping plugin in templates?" (sorry stackoverflow is putting me limitations on the number of links of the post)
i tried to play making use of the answer (the jsFiddle: http://jsfiddle.net/ueGAA/1)
so the exercise was to make the todo of the knockoutjs Tutorial at learn.knockoutjs.com named "Loading and saving data" but using knockout mapping.
the problem is with the kind of of viewmodel declaration of the answer, which I like, here transposed to the todo:
var viewModel =
{
tasks : ko.mapping.fromJS(data),
newTaskText: ko.observable(),
incompleteTasks: ko.computed(function() {
return ko.utils.arrayFilter(this.tasks(), function(task) { return !task.isDone() });
}),
// Operations
addTask: function() {
alert('Entering add task, count:' + this.tasks().length);
this.tasks.push(new Task({ title: this.newTaskText() }));
this.newTaskText("");
},
removeTask: function(task) { this.tasks.remove(task) }
}
The point is here: inside the declaration of ko.computed(), this is referencing window.
Normal indeed.
The correct behavior can be obtained if I declare the ko.computed() after the vewmodel variable.
this way:
viewModel.incompleteTasks=ko.computed(function() {
return ko.utils.arrayFilter(viewModel.tasks(), function(task) { return !task.isDone() });
});
I don't like it because it references statically the object viewModel in the anonymous function.
the question is: how to declare in an elegant way the incompleteTasks directly in the viewmodel declaration?
the jsFiddle is here http://jsfiddle.net/Yqg8e/
thanks
Switch from using an object literal to a constructor function for your ViewModel.
function ViewModel() {
var self = this;
self.tasks = ko.mapping.fromJS(data);
self.newTaskText = ko.observable();
self.incompleteTasks = ko.computed(function() {
return ko.utils.arrayFilter(self.tasks(), function(task) {
return !task.isDone()
});
});
self.addTask = function() {
alert('Entering add task, count:' + self.tasks().length);
self.tasks.push(new Task({ title: self.newTaskText() }));
self.newTaskText("");
};
self.removeTask = function(task) { self.tasks.remove(task) }
}
ko.applyBindings(new ViewModel());​
Note also the use of var self = this; which allows access to the this context even within inner anonymous functions.
This technique is described in the Knockout docs, in the section on Computed Observables (skip down to the section titled Managing 'this').
Here's an updated fiddle: http://jsfiddle.net/Yqg8e/1/

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