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 or property paths 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 a binding 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.
4.2 - Binding Transforms
By default, Sproutcore creates simple bi-directional bindings. A change on one property will be relayed to the other side and vice versa with no changes in between. Sometimes, this behavior isn't exactly what you want. For example, what if you have an object that expects a Boolean on one side and another object that emits a Number on the other side?
Sproutcore provides an way to implement custom bindings through binding transforms. Transforms convert values that change on one side of the bindings into another value before it is applied to the other side.
Helper | Description |
---|---|
oneWay() | Forces binding to only relay changes in one direction. Changes on the "from" side (the path you name as the value) will be relayed to the "to" side (the property you name before "Binding"). Changes in the other direction will not be relayed. |
bool() | Forces all values to become booleans. null, undefined, 0, and false all become false. All other values become true |
single() | Forces all values to a non-Enumerable value. Arrays with one item in them will be converted to that item only. Arrays with multiple items will be converted to a placeholder value (default SC.MULTIPLE_PLACEHOLDER). |
multiple() | Forces all values to an Array-like value. null or undefined becomes an empty array. Single objects will be wrapped in an array. Arrays and enumerables pass through unchanged. |
notEmpty() | Does not allow empty values. null, undefined, empty strings and empty arrays are converted to a placeholder value (default SC.EMPTY_PLACEHOLDER) |
notNull() | Does not allow null values. This is a more restricted version of notEmpty() |
not() | Inverts the transform. This is equivalent to bool() but the resulting boolean will be inverted. |
isNull() | Transforms the value to YES if the original value is null or undefined and NO otherwise. Useful for enabled/disabling UI when you have no content |
noError() | Ensures no Error objects are allowed through. Errors will be converted to null. |
Transforms can be chained together. You can use this facility to construct just the type of binding you want. For example, if you have an object that expects single values only, no errors and no empty values, you could code:
userBinding: SC.Binding.from('MyApp.userController.name').single().noError().notEmpty()
As another example, if you only need a one way binding that is of a boolean type, you could code:
isVisibleBinding: SC.Binding.from('MyApp.userController.isBusy').oneWay().bool()
In addition to using the built-in transforms listed above, you can also add your own transforms. A transform is simply a function that accepts an untransformed value and returns the transformed value. It has the following signature.
function(value, isForward) {
// do transform
return transformedValue;
}
The first parameter passed to a transform function will be the untransformed value.
If isForward is YES, then the value being passed came from the "from" side of the binding (i.e. the "Binding.path" you named). If isForward is NO, then the value came from the "to" side (i.e. the property you named with "propertyBinding"). You can vary your transform behavior if you are based on the direction of the change.
Your transform function should transform the value as needed and return it. If you don't want to perform any kind of transform, then return the value as it was passed in.
For example, to make a string upper case:
userBinding: SC.Binding.from('MyApp.userController.name')
.transform(function(value, isForward) {
return (value && value.toString) ? value.toString().toUpperCase() : '';
})
If you want use a transform over and over again, you can define a new method on the SC.Binding object like so:
SC.Binding.upperCaseString = function() {
return this.transform(function(value, isForward) {
return (value && value.toString) ? value.toString().toUpperCase() : '';
});
};
...
userBinding: SC.Binding.from('MyApp.userController.name').upperCaseString()
4.3 - Manually Creating and Disconnecting Bindings
Most of the time you will setup bindings using the class definition method described above. Occasionally, however, you may want to setup and teardown bindings yourself.
To create a binding just use the bind() method defined on SC.Observable and inherited by SC.Object. The first parameter you pass should be the property name you want to bind to. The second is the same thing you would set as the value of a binding property:
myView.bind('isVisible', SC.Binding.from('MyApp.mainController.title').bool());
// is the same as
myView = SC.View.create({
isVisibleBinding: SC.Binding.from('MyApp.mainController.title').bool()
});
Note that when you manually create a binding, the BindingDefaults property is not consulted. You must configure the binding exactly as you need.
If you have created a binding and need to disconnect it for some reason, you can call the disconnect() method on the binding itself.
If you manually create a binding using bind(), the actual binding object is returned from the bind() call.
If you create a binding by defining it on your class, you can find the binding instance on the same property on an object instance:
var binding = myView.bind('isVisible', SC.Binding.from('MyApp.mainController.title').bool());
// is the same as
myView = SC.View.create({
isVisibleBinding: SC.Binding.from('MyApp.mainController.title').bool()
});
var binding = myView.get('isVisibleBinding');
// disconnect!
binding.disconnect();
You can reconnect a binding that has been disconnected using the connect() method as well.
5 - Changelog
- January 12, 2011: initial partial version by Peter Wagenet
- January 19, 2011: further updates by Peter Wagenet
- January 24, 2011: added section on "Bindings and Chained Property Paths" by Peter Wagenet
- March 2, 2011: fixed paragraph formmatting by Topher Fangio
- March 2, 2011: fixed grammar, clarified phrasing, added examples to the Observer section by Jason Gignac
- April 13, 2011: "Binding Transforms" and "Manually Creating and Disconnecting Bindings" sections copied from wiki and edited by Vibul Imtarnasan
- July 19, 2013: added Changelog by Topher Fangio
- September 11, 2013: converted to Markdown format for DocPad guides by Topher Fangio