SproutCore

SproutCore Guides

These guides are designed to help you write and perfect your code.

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