Reputation: 395
I am starting my second attempt at creating an app with KO, so while I feel my understanding of the library is passable, my execution using it still needs work. I have a question about a complex model for a view that doesn't represent just one logical entity.
Given a complex or composite view model where properties of the main viewmodel are objects themselves:
var mainViewModel = function(data){
var self=this;
self.user = new UserModel();
self.roles = new RolesCollectionModel();
}
function UserModel(data){
var self=this;
self.Name = ko.observable(data.name);
}
function RolesCollectionModel(data){
var self=this;
self.Items = ko.observableArray(data.items);
}
It appears to me that KO only recognizes binding on the first level of properties, that there is not 'observable bubbling'. So, for me to use the data binding on the child objects, it appears I have to declare those as observables as well:
var correctViewModel = function(data){
var self=this;
self.SetUserModel= function(userData) {
this.user = ko.observable(userData);
}
self.SetRoles = function (data) {
this.roles = ko.observableArray(data);
}
}
And correspondingly in my html if I want to bind the name of the userModel property:
<input data-bind:'textInput:userModel().name'/>
My questions then are:
Am I correct in my conclusion that 'observable bubbling' does NOT occur, and this is the only way to achieve binding to properties that are several levels deep in an object graph?
Assuming I am correct in 1, the syntax above is strange to me. userModel and name are both observables, but to make my examples work, I have to reference the object as userModel().name. I would have expected to bind to userModel.name. Its confusing here, right?
** I've edited this to include the setters I would be using. The idea is that the top-level model will be composed/bound after >1 calls to an API. I did it this way because it is more natural in an OO sense, but it appears from this conversation I could just as easily set object properties explicitly after the response from the API, e.g.:
var topModel = new CorrectViewModel();
$.ajax({...}).done(function(data){
topModel.Users=(new UserModel(data));
})
Can I ask what is the more idiomatic style to use?
Upvotes: 0
Views: 860
Reputation: 39004
Answer to 1: yes, you're right, observables don't "bubble".
Answer to 2: you must use that syntax.
And now something that explains both 1 and 2 at the same time: an observable is a function, which can be invoked with a parameter to set its value, or withput a parameter to get it.
When knockout finds a binding expression, it checks if its an observable or not:
Let's examine several cases:
// 1
var vm = { name: ko.observable(); };
// 2
var vm = { hidden: ko.observable(true); };
// 3
var userModel = ko.observable({
name: ko.observable()
});
In all these cases, as explained above, the observables are functions which need to be invoked to get or set its value.
The simplest case is this: text: name
. Knockout checks that name
is an observable, so ko invokes it to get its value
A little bit more complicated: visible: !hidden()
. In this case, knockout sees that !hidden()
it's not an observable, and evaluates it as "normal JavaScript". If you wrote visible: !hidden
, knockout would also check that it's not an observable, so it would evaluate it as "normal javascript", and the result would always be false
, because hidden
, without parentheses to invoke it, is a function
, which is a JavaScript truish value, and the !
converts the truish value into false
.
When there is an observable with observable properties inside it, you must invoke the outer observable to get acces to the object inside it, which holds the observables. In the expression userModel().name
, the parentheses invoke userModel
to get the object inside it, and the refers to the name
observable. So, when Knockout checks if it's an observable, it finds it's an observable and evaluates it. If you specified userModel.name
it would be yield undefined
, becasue userModel
, without invoking it with parentheses, is a function that doesn't have a name
property.
NOTE 1: there are utility functions in knockout to discover if something is an observable or not: ko.isObservable(expr)
, and to get the value of an expression, wheter it's an observable or not: ko.unwrap(expr)
NOTE 2A: observables are implemented as function so that when they are invoked to set their value, they can notify that they have changed to all of their subscribers. The subscriptions are created atuomatically. For example, when you specify the binding text: userModel().name
, the code that has to set the text is subscribed to the name
observable, which means tha whenever name
is invoked, and its value changes, it notifies that code so that it can change the text. In fact you can also do explicit subscriptions like Aurelia
NOTE 2B: some languages, including modern flavors of JavaScript support properties. A property is read or written as if it was a simple variable, but it's able to execute some code instead of simply setting or getting the value. There are some JavaScript libraries that use this feature which makes it possible to use a simpler syntax, free of parentheses
Upvotes: 1
Reputation: 2551
Here's an example of how I usually structure deep bindings so that it effectively supports bubbling/multiple sub-viewmodels.
Suppose you are a franchise manager and you have multiple franchise stores/locations you manage.
var vm = {
activate: activate,
user: ko.observable(),
currentFranchiseLocation: ko.observable(),
};
function activate() {
vm.user(LoadYourUserHere().Result);
vm.currentFranchiseLocation(LoadTheInitialLocation().result)
}
function changeLocation() {
loadOtherLocation.done(function (data) {
vm.currentFranchiseLocation(new FranchiseLocationVM(data));
});
}
Html binding
<div data-bind="with: currentLocation">
<h3 data-bind="text:Name"></h3>
<div data-bind="text:numEmployees"></div>
</div>
The magic here is really the with
keyword. It targets an observable and on change, rebinds it's entire sub-tree. At the same time, it unwraps and aliases the contents of it's target observable to the local scope so you can do bindings such as text:Name
, rather than currentLocation().Name
Upvotes: 0