Computed Properties, Observers and Bindings
The guide covers some of the core concepts of properties, bindings and key-value observing in SproutCore. By referring to this guide, you will be able to:
- Be familiar with Computed Properties, Observers and Bindings.
- Add Computed Properties, Observers and Bindings to your Classes.
- Be familiar with absolute, relative and chained property paths.
1 Key-Value Observing
One of the core tenets of a SproutCore application is leveraging SproutCore’s Key-Value Observing (KVO) system. KVO is a powerful feature that keeps code clean, fast and maintainable. Rather than writing fallible code to manually keep your application in sync between models, views and other objects, we simply define and bind the proper properties and let SproutCore magically update the state of our application as these properties change.
1.1 get() and set()
For KVO to work properly, SproutCore implements getters and setters to track changes to objects. This is why it’s important to always use get() and set() for properties that might use observers, bindings, or be computed properties. Failure to do so will cause your app to get out of sync and not update properly. While this may seem like a nuisance for those used to accessing everything via dot notation, the benefits of KVO are substantial and you’ll quickly get used to using get() and set() such that you’ll forget you ever had to worry about it.
The following example shows KVO compliant access to an SC.Object object.
var obj = SC.Object.create({ name: 'Jim' });
obj.get('name'); // Jim
obj.set('name', 'Bob');
obj.get('name'); // Bob
Those of you who have worked in languages like Java may be familiar with having to write property accessors. Instead of having to define accessors for every property, get() and set() serve as universal accessors.
1.2 Property Paths
SproutCore also introduces the concept of property paths, which will play a role in the next important sections. A property path is a String that points to a nested property. For instance, "MyApp.userController.firstName" refers to the firstName property of the MyApp.userController object. You can also have relative paths which refer to paths relative to the current object. For instance, within the context of MyApp, you could use the path ".userController.firstName" to point to the same location. As you can see, the relative property begins with a period, which is always the case for relative property paths.
To work with relative property paths you can use the getPath() and setPath() functions. These work identically to get() and set() except that they expect a path as the first argument.
2 Computed Properties
Often you have properties that depend on the value of several other properties. These are known as computed properties and they are an extremely useful means to keep your code contextually correct without resorting to manual property manipulation. Computed properties are defined as functions with a call to property() using a list of the dependent property names as arguments.
In the next example, we will revisit an earlier version of MyApp.Person, to make the fullName() function into a computed property.
MyApp.Person = SC.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
return [this.get('firstName'), this.get('lastName')].compact().join(' ');
}.property('firstName', 'lastName')
});
var person = MyApp.Person.create({
firstName: 'Peter',
lastName: 'Wagenet'
});
person.get('fullName'); // Peter Wagenet
person.set('lastName', 'Smith');
person.get('fullName'); // Peter Smith
As you can see, you are able to use get() with computed properties in the same way you would use it with regular properties.
This may not seem like much of an improvement over the previous fullName() function, but it is or at least it will be. On the one hand, by making fullName into a property it can now be observed and bound to, an important detail which is described later, but on the other hand SproutCore provides a function to make computed properties even more efficient for our use right now. This is where the cacheable() extension comes in. By making our computed properties cacheable, they don’t need to be computed unless their dependent properties change.
The next example highlights how multiple requests of the same property don’t involve re-calculations. Notice the addition of cacheable() to the computed property.
MyApp.Person = SC.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
console.log('Calculating fullName...');
return [this.get('firstName'), this.get('lastName')].compact().join(' ');
}.property('firstName', 'lastName').cacheable()
});
var person = MyApp.Person.create({
firstName: 'Peter',
lastName: 'Wagenet'
});
person.get('fullName');
> 'Calculating fullName...' // Peter Wagenet
person.get('fullName'); // Peter Wagenet
person.set('lastName', 'Smith');
person.get('fullName');
> 'Calculating fullName...' // Peter Smith
Computed properties are “lazily” computed. That is, their functions are not run until the property is requested. If the computed property is never requested, the function will never need be run.
Setting computed properties is slightly more complicated, but still easy to grasp. Each call to set() passes key and value arguments to the computed property function, while a call to get() on the same computed property will only pass the key and the value will be undefined. Therefore, the existence of value tells us whether the property is being got or set.
Here is an example of creating a computed property that is also writable with set().
MyApp.Capitalizer = SC.Object.extend({
capitalizedValue: function(key, value) {
if (value !== undefined) {
this._capitalizedValue = value.toUpperCase();
console.log('Set capitalizedValue to ' + this._capitalizedValue);
}
return this._capitalizedValue;
}.property()
});
var cap = MyApp.Capitalizer.create();
cap.set('capitalizedValue', 'abc');
> 'Set capitalizedValue to ABC'
cap.get('capitalizedValue'); // 'ABC'
The key argument is ignored, which is generally the case with computed properties.
3 Observers
Closely related to the concept of properties is that of observers. Observers do exactly what their name suggests, they observe properties, watching for changes. The most basic observer looks like this,
var obj = SC.Object.create({
value: null,
valueDidUpdate: function(){
alert('New Value: ' + this.get('value'));
}.observes('value')
});
obj.set('value', 'Test'); // alert('New Value: Test');
As you can see observes() is added to the function and when the matching property is updated, the function is run. You can even pass multiple properties or property paths into an observer.
The following example uses an observer on a property and a computed property. This is actually an example of an improper use of observes(), because fullName is dependent on firstName and lastName and therefore also appears to change whenever one of those properties changes. Therefore, it probably doesn’t make sense to observe both fullName and firstName, but it helps us to really understand how computed properties and observers work.
MyApp.Person = SC.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
console.log('Calculating fullName...');
return [this.get('firstName'), this.get('lastName')].compact().join(' ');
}.property('firstName', 'lastName').cacheable(),
nameDidChange: function() {
console.log('firstName or fullName changed!');
}.observes('firstName','fullName')
});
person = MyApp.Person.create({
firstName: 'Emma',
lastName: 'Goldman'
});
person.set('lastName','Berkman');
> 'firstName or fullName changed!' // because fullName will have changed
person.set('firstName','Alexander');
> 'firstName or fullName changed!' // because firstName changed
> 'firstName or fullName changed!' // because fullName will have changed
Note that fullName properly notifies changing as firstName and lastName change, but it is still not re-computed in this example because we didn’t actually get() fullName anywhere.
Using set() to repeatedly set the same value will not result in the observer firing repeatedly. It will only fire when the value changes.
3.1 Observer Notification
It is important to realize that observers have to be notified of property changes. The set() method and property() extension handles this for us automatically, which is one of the reasons get() and set() should be used to access properties. However, there are rare cases where we know that an observed property has changed without having updated the KVO system. In these cases you can use notifyPropertyChange() to tell observers that the property has been updated (i.e. this.notifyPropertyChange('value')). However, if you find this is the case, check your code to see that you are using set() and that your computed properties have the correct dependent properties defined.
Another case that is less rare is that you may find that you are updating a number of properties at once. If you have a lot of observers that depend on these properties, you may find them getting called more often than necessary. In this case, you can use beginPropertyChanges() and endPropertyChanges() to wrap your property change calls. This will cause all change notifications to happen once endPropertyChanges is called and will prevent unnecessary duplicate notifications.
3.2 Chained Property Paths
Observers are able to use a special type of property path called a chained property path. When using an observer (or abinding as we will see), usually the actual observer is only added to the second to last object in the property path. Therefore, if you add an observer for the path "MyApp.usersController.mainUser.name", SproutCore finds the object at "MyApp.usersController.mainUser" and adds the observer to its name property. In this case, nothing is observing MyApp.usersController to see if its mainUser property changes.
For example,
MyApp.usersController = SC.Object.create({
mainUser: SC.Object.create({
name: 'Joe'
})
});
MyApp.observerObject = SC.Object.create({
userNameDidChange: function() {
console.log(this.getPath('MyApp.usersController.mainUser.name'));
}.observes('MyApp.usersController.mainUser.name')
});
MyApp.usersController.setPath('mainUser.name', 'Jim');
> 'Jim'
MyApp.usersController.set('mainUser', SC.Object.create({ name: 'Bob' }));
MyApp.usersController.setPath('mainUser.name', 'Doug');
As you can see, when we replace mainUser the observer does not fire. This is because we only had an observer on the original mainUser object, which has been replaced.
What we want to do is watch for changes to usersController.mainUser and for changes to mainUser.name. This is where chained property paths come in. To let SproutCore know that we want observers on both, we use a chained property path like MyApp.usersController*mainUser.name.
The asterisk (*) in the property path indicates that we want SproutCore to observe changes to all properties following the asterisk. In this case, that is both mainUser and name.
Here’s an updated version of the previous example with a chained observer,
MyApp.observerObject = SC.Object.create({
userNameDidChange: function() {
console.log(this.getPath('MyApp.usersController.mainUser.name'));
}.observes('MyApp.usersController*mainUser.name') // Chained observer
});
MyApp.usersController.setPath('mainUser.name', 'Jim');
> 'Jim'
MyApp.usersController.set('mainUser', SC.Object.create({ name: 'Bob' }));
> 'Bob'
The observer will now fire if MyApp.usersController.mainUser.name or MyApp.usersController.mainUser changes.
So why don’t we always use chained observers? Observers are “expensive”, they take time to set up and they have to run each time their properties change and often times we don’t have changes in all levels. In the previous example, we don’t care about changes to MyApp.usersController, because we are never going to replace it. If the same were true for mainUser, we wouldn’t want to observe it either. Therefore, it is advisable in practice to use chained observers as little as possible, in order to protect performance.
4 Bindings
When you combine properties and observers, you end up with bindings. Bindings serve to link two properties together and if you have experience with other application development frameworks, you will likely recognize their importance.
Here is an example which binds a property on an SC.View object to a property on a controller object. SC.View is the main SproutCore view class and it is a common pattern to bind views to controllers, so that as properties change on the controller, the view updates automatically. This example uses the short form of creating a binding, by simply suffixing Binding to the property name.
MyApp.userController = SC.Object.create({
name: null
});
MyApp.mainView = SC.View.create({
userBinding: 'MyApp.userController.name'
});
MyApp.userController.set('name', 'Joe');
// The runloop must run
console.log(MyApp.mainView.get('user'));
> 'Joe'
MyApp.mainView.set('user', 'Jim');
// The runloop must run
console.log(MyApp.userController.get('name'));
> 'Jim'
In this example, we used the absolute path to the property. Bindings support relative property paths and chained property paths as well.
As you can see in the example, when you update the value on one side of the relationship, it is automatically updated on the other side and vice versa. By default bindings are bi-directional, but you can also set them to only go in one direction by using the long form SC.Binding.oneWay() setup instead. Making bindings unidirectional increases the performance of the binding and is recommended.
The long form for a bi-directional binding is SC.Binding.from().
For example, if we used userBinding: SC.Binding.oneWay('MyApp.userController.name'), changes to MyApp.userController.name would update MyApp.mainView.user, but setting MyApp.mainView.user will not update MyApp.userController.name.
MyApp.userController = SC.Object.create({
name: null
});
MyApp.mainView = SC.View.create({
userBinding: SC.Binding.oneWay('MyApp.userController.name')
});
MyApp.userController.set('name', 'Joe');
// The runloop must run
console.log(MyApp.mainView.get('user'));
> 'Joe'
MyApp.mainView.set('user', 'Jim');
// The runloop must run
console.log(MyApp.userController.get('name'));
> 'Joe'
One very important difference between bindings and observers is that observers update almost immediately, while bindings update once at the end of each Run Loop. By deferring binding updates, it offers considerable improvements in performance, but can be confusing if you are expecting an update to propagate immediately. See ‘The Run Loop’ for more details.
4.1 Bindings and Chained Property Paths
Since bindings are based off of observers behind the scenes, the same principles of chained observers applies. This means that if you want to bind a chained property path, you will want to use asterisks in your path as appropriate.
For example,
MyApp.usersController = SC.Object.create({
mainUser: SC.Object.create({
name: 'Joe'
})
});
MyApp.userNameView = SC.LabelView.create({
valueBinding: 'MyApp.usersController*mainUser.name'
});
MyApp.usersController.setPath('mainUser.name', 'Harriet');
// The run loop must run
console.log(MyApp.userNameView.get('value'));
> 'Harriet'
MyApp.usersController.set('mainUser', SC.Object.create({ name: 'Eunice' }));
// The run loop must run
console.log(MyApp.userNameView.get('value'));
> 'Eunice'
Since we used an asterisk in the property path, the binding will be updated when either mainUser or mainUser.name changes.


Chapters