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>
Related
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: [...] });
I've been trying for some time to save a ko viewmodel to the browser history and return it on a popstate event. Currently no errors are being thrown, but nothing is being changed on the popstate. The basic flow of what I'm trying goes like this:
var storeViewModel = function (){
return ko.toJSON(viewModel);
};
function viewModel() {
var self = this;
self.records = ko.observableArray();
// add an object to the array and save view model
self.addRecord = function () {
self.records.push(new record());
// call the storeViewModel function push viewModel to history
history.pushState(storeViewModel(), "");
}
// set view model to stored view model object on forward / back navigation navigation
window.onpopstate = function (event) {
self = ko.utils.parseJson(event.state);
}
}
ko.applyBindings(new viewModel());
I've read the mozilla documentation for this several times. Everything seems to make sense, but I am having trouble in implementation. Thanks for any help!
You wouldn't save the viewmodel, but the data it contains.
The most convenient way to do that is by employing automatic mapping. Knockout has the [mapping plugin][1] for this; it allows you to easily turn raw data into a working viewmodel, and a working viewmodel back into raw data.
By default the mapping plugin maps all properties of the raw data to observable or observableArray, respectively, but that can be fine-tuned in the mapping definition (see documentation).
This basically works like this:
ko.mapping.fromJS(data, {/* mapping definition */}, self);
and back like this:
ko.mapping.toJS(self);
I'd recommend setting up all your viewmodels so they can bootstrap themselves from raw data:
function Record(data) {
var self = this;
// init
ko.mapping.fromJS(data, Record.mapping, self);
}
Record.mapping = {
// mapping definition for Record objects, kept separately as a constructor
// property to keep it out of the individual Record objects
};
and
function RecordPlayer(data) {
var self = this;
self.records = ko.observableArray();
self.init(data);
self.state = ko.pureComputed(function () {
return ko.mapping.toJS(self);
});
}
RecordPlayer.mapping = {
// mapping rules for ViewModel objects
records: {
create: function (options) {
return new Record(options.data);
}
}
};
RecordPlayer.prototype.init = function (data) {
// extend a default data layout with the actual data
data = ko.utils.extend({
records: []
}, data);
ko.mapping.fromJS(data, ViewModel.mapping, this);
};
RecordPlayer.prototype.addRecord = function () {
this.records.push(new Record());
};
The mapping plugin keeps track of all the properties it mapped in the .fromJS step and only returns those in the .toJS() step. Any other properties, like computeds, will be ignored.
That's also the reason for the ko.utils.extend - to establish the baseline set of properties that you want the mapping plugin to handle.
The state computed now changes every time the state-relevant data changes, due to knockout's built-in dependency tracking.
Now what's left is handling the page load event:
// initialize the viewmodel
var player = new RecordPlayer(/* data e.g. from Ajax */);
// subscribe to VM state changes (except for changes due to popState)
var popStateActive = false;
player.state.subscribe(function (data) {
if (popStateActive) return;
history.pushState(ko.toJSON(data), "");
});
// subscribe to window state changes
player.utils.registerEventHandler(window, "popstate", function (event) {
popStateActive = true;
player.init( ko.utils.parseJson(event.state) );
popStateActive = false;
});
// and run it
ko.applyBindings(player);
You can expand and run the code snippet below to see it in action.
function Record(data) {
var self = this;
// init
ko.mapping.fromJS(data, Record.mapping, self);
}
Record.mapping = {
// mapping definition for Record objects, kept separately as a constructor
// property to keep it out of the individual Record objects
};
function RecordPlayer(data) {
var self = this;
self.records = ko.observableArray();
self.init(data);
self.state = ko.pureComputed(function() {
return ko.mapping.toJS(self);
});
}
RecordPlayer.mapping = {
// mapping rules for RecordPlayer objects
records: {
create: function(options) {
return new Record(options.data);
}
}
};
RecordPlayer.prototype.init = function(data) {
// extend a default data layout with the actual data
data = ko.utils.extend({
records: []
}, data);
ko.mapping.fromJS(data, RecordPlayer.mapping, this);
};
RecordPlayer.prototype.addRecord = function() {
this.records.push(new Record());
};
RecordPlayer.prototype.pushState = function() {
history.pushState(this.state(), "");
};
// initialize the viewmodel
var player = new RecordPlayer( /* optional: data e.g. from Ajax */ );
var popStateActive = false;
// subscribe to VM state changes (except for changes due to popState)
player.state.subscribe(function(data) {
if (popStateActive) return;
history.pushState(ko.toJSON(data), "");
});
// subscribe to window state changes
ko.utils.registerEventHandler(window, "popstate", function(event) {
popStateActive = true;
player.init(ko.utils.parseJson(event.state));
popStateActive = false;
});
// and run it
ko.applyBindings(player);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<p>Records:
<span data-bind="foreach: records">
<span>(o)</span>
</span>
</p>
<p>There are <span data-bind="text: records().length"></span> records in the player.</p>
<button data-bind="click: addRecord">Add record</button>
<button data-bind="click: function () { history.back(); }">Undo (<code>history.back()<code>)</button>
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/
So here is an example of my app in jsfiddle: http://jsfiddle.net/GWXpn/1/
The problem is click event isn't being fired at all. I am not getting any JS errors in the console.
First, I wanted to display an unordered list with couple if items, each item should be clickable. This is what I did:
var FooModel = Backbone.Model.extend({});
var ListView = Backbone.View.extend({
tagName: 'ul', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render'); // every function that uses 'this' as the current object should be in here
},
render: function() {
for (var i = 0; i < 5; i++) {
var view = new SingleView({
model: new FooModel()
});
$(this.el).append(view.render().el);
}
return this; // for chainable calls, like .render().el
}
});
var SingleView = Backbone.View.extend({
tagName: 'li', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render', 'click'); // every function that uses 'this' as the current object should be in here
},
events: {
"click": "click"
},
click: function(ev) {
console.log("aaa");
alert(333);
},
render: function() {
$(this.el).append("aaa");
return this; // for chainable calls, like .render().el
}
});
I wanted to divide my app in to multiple modules (header, body, footer) so I created an abstract model and extended my modules from it:
var AbstractModule = Backbone.Model.extend({
getContent: function () {
return "TODO";
},
render: function () {
return $('<div></div>').append(this.getContent());
}
});
var HeaderModule = AbstractModule.extend({
id: "header-module",
});
var BodyModule = AbstractModule.extend({
id: "body-module",
getContent: function () {
var listView = new ListView();
return $("<div/>").append($(listView.render().el).clone()).html();
}
});
var ModuleCollection = Backbone.Collection.extend({
model: AbstractModule,
});
Then I just created my main view and rendered all its subviews:
var AppView = Backbone.View.extend({
el: $('#hello'),
initialize: function (modules) {
this.moduleCollection = new ModuleCollection();
for (var i = 0; i < modules.length; i++) {
this.moduleCollection.add(new modules[i]);
}
},
render: function () {
var self = this;
_(this.moduleCollection.models).each(function (module) { // in case collection is not empty
$(self.el).append(module.render());
}, this);
}
});
var appView = new AppView([HeaderModule, BodyModule]);
appView.render();
Any ideas why?
You have two bugs in one line:
return $("<div/>").append($(listView.render().el).clone()).html();
First of all, clone doesn't copy the events unless you explicitly ask for them:
Normally, any event handlers bound to the original element are not copied to the clone. The optional withDataAndEvents parameter allows us to change this behavior, and to instead make copies of all of the event handlers as well, bound to the new copy of the element.
[...]
As of jQuery 1.5, withDataAndEvents can be optionally enhanced with deepWithDataAndEvents to copy the events and data for all children of the cloned element.
You're cloning the <ul> here so you'll want to set both of those flags to true.
Also, html returns a string and strings don't have events so you're doubling down on your event killing.
I don't understand why you're cloning anything at all, you should just return the el and be done with it:
return listView.render().el;
If you insist on cloning, then you'd want something like this:
return $(listView.render().el).clone(true, true);
but that's just pointless busy work.
BTW, 'title' and 'Title' are different model attributes so you'll want to say:
console.log(this.model.get("title") + " clicked");
instead of
console.log(this.model.get("Title") + " clicked");
Also, Backbone collections have a lot of Underscore methods mixed in so don't mess with a collection's models directly, where you're currently saying:
_(this.moduleCollection.models).each(...)
just say:
this.moduleCollection.each(...)
And as Loamhoof mentions, 0.3.3 is ancient history, please upgrade to newer versions of Backbone, Underscore, and jQuery. You should also read the change logs so that you can use newer features (such as this.$el instead of $(this.el), fewer _.bindAll calls, listenTo, ...).
Partially Corrected Demo (including updated libraries): http://jsfiddle.net/ambiguous/e4Pba/
I also ripped out the alert call, that's a hateful debugging technique that can cause a huge mess if you get into accidental infinite loops and such, console.log is much friendlier.
I have:
userAccess object:
var userAccess = new (
function() {
this.userLogedIn = false;
}
);
I have modelview, binded to UI
var modelview = new (
function(){
this.itemVisible =
function(data) {
if(data.id === "ID2")
return userAccess.userLogedIn;
return true;
};
this.items = [{id:"ID1", text:"text1"}, {id:"ID2", text:"text2"}];
}
);
on UI, inside foreach binding I have:
<span data-bind="text: text, visible:$parent.itemVisible($data)"> </span>
so the visibility of the span element is binded to modelview's function.
The function determines a visibility of the current item based on its ID and value of userAccess.
Problem:
The two way binding doesn't work in this scenario. For example if I make userAccess.userLogedIn = true the element "ID2" doesn't become visible.
This is because of lack of observable, but I can not, seems to me, fit an observable in this pattern.
I know also that I can update binding manually, but would like to avoid this, if this is possible.
I have feeling that I'm missing something obvious here.
Complete source on CodePen
You should probably refactor your whole setup to use observables. Otherwise, the usage of knockout does not make much sense due to the lack of automated view updates (as you noticed).
var userAccess = new (
function() {
// It is likely that this value will change, so make it an observable!
this.userLogedIn = ko.observable(false);
}
);
// Create a "class" for the items in the list be able to encapsulate behavior /
// properties such as "is this item visible"?
var Item = function(id, text) {
var self = this;
self.id = id; // <-- will most likely never change (?) => not an observable
self.text = ko.observable(text);
// Use a "computed observable" for things that require more sophisticated logic
self.visible = ko.computed(function() {
if (self.id === "ID2") {
return userAccess.userLogedIn(); // <-- observable = () required!
} else {
return true;
}
});
};
var modelview = new (
function() {
this.items = ko.observableArray([
new Item("ID1", "text1"), new Item("ID2", "text2")
]);
}
);
and in the HTML
<span data-bind="text: text, visible: visible"> </span>
Example: http://jsfiddle.net/a89VL/