• If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • You already know Dokkio is an AI-powered assistant to organize & manage your digital files & messages. Very soon, Dokkio will support Outlook as well as One Drive. Check it out today!

View
 

Runtime-Bindings

Page history last edited by Drew Hart 13 years, 4 months ago

Bindings connect two properties in your application so that when one properties changes, the other one will be updated automatically also.  You can design your application as small, manageable pieces and then connect them with bindings.  Each component can be designed and tested separately, saving you both time and code.  Bindings also make it easier to create variations of your application that work on different platforms or serve different purposes.

 

Bindings build on the Key Value Coding (KVC) and Key Value Observing (KVO) features of the Runtime.  Go read those sections first if you haven't yet before continuing with this chapter.

 

Creating a Basic Binding

 

The most basic way to bind two values in SproutCore is to simply define the bindings when you create an object instance or define a subclass.  To define a binding this way, just add a property when you extend() or create() a class ending in the word "Binding".  For example:

 

MyApp.userController = SC.ObjectController.create();

 

MyApp.detailController = SC.ObjectController.create({

  contentBinding: "MyApp.userController.person",

 

  // example observer

  personDidChange: function() {

    console.log('content did change!');

  }.observes('content')

 

});

 

MyApp.userController.set('person', foo);

==> "content did change!"

 

When the detailController object in this example is created, a binding will be created automatically from the "person" property on the MyApp.userController object to the "content" property on MyApp.detailController.  Through this binding, any change you make to the "person" property will be automatically copied to the "content" property and vice versa.

 

Thanks to this binding, you could now design the userController and the detailController separately.  Each object only needs to know about its own properties with bindings connecting them in between.

 

Chained and Relative Bindings

 

Bindings work by adding observers to the specified properties, and only to those properties.  For example, along with adding an observer to this object's content attribute, the following binding

 

contentBinding: "MyApp.userController.person.firstName"

 

also adds an observer to the firstName attribute of the model which MyApp.userController.person points to.  This may be what you want, but if, for example, your userController's person attribute simply moves to point to a new record, no firstNames will have changed and so no observers will fire.  If you want to bind your content to person's firstName and have it change when the controller's person attribute changes to point to a new record, you need to use a chained binding.  Just add an asterisk:

 

contentBinding: "MyApp.userController*person.firstName"

 

This will add observers to both the record's firstName and to the controller's choice of record (its people attribute), properly binding your content to the firstName attribute of the selected "person" object.

 

Likewise, you can create a relative path binding by starting with a period or asterisk:

 

firstNameBinding: "*person.firstName"

 

is an easy way to expose a property deep in your object hierarchy at a top level.  The above example will make the firstName property equal to the person.firstName property of the same object.

 

Binding Timing and Run Loops

 

Unlike observers, bindings do not fire immediately when a property changes.  Instead they wait until the end of the current run loop to synchronize.  This usually will not directly impact your code other than making your app faster.  However, it is important to keep this in mind when using bindings in your application.   Don't design your application expecting bindings to fire immediately like observers.

 

Additionally, because bindings are deferred into run loops, this means that they will not fire immediately when you make changes on the command line in the web browser unless you first call SC.RunLoop.begin() and then SC.RunLoop.end().  Any event handlers must also execute within a runloop.  If you use the built-in SproutCore system for handling events, this will not be a problem.  If you add your own event listeners then you must make sure you always begin and end a runloop in order for bindings to fire.

 

Binding Transforms

 

By default, SproutCore creates simple two-way 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?

 

You could write special computed properties to convert this code yourself, but Bindings provide an easier way to handle this 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.  For example, here is how you could define a binding that only relays changes one way and converts all values to Booleans:

 

isVisibleBinding: SC.Binding.from("MyApp.perferencesController.isVisible").oneWay().bool()

 

If you want to configure a binding this way, you should always begin with the from() helper first.  This just wraps your string in a new Binding object that can be configured.  In fact, when you name a string only in a binding, it is equivalent to using from() alone:

 

isVisibleBinding: "MyApp.preferencesController.isVisible"

 

is the same as:

 

isVisibleBinding: SC.Binding.from("MyApp.preferencesController.isVisible")

 

Runtime comes with some of the most commonly needed transforms built in as shown in the table below:

 

Helper  Behavior 
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 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 do:

 

contentBinding: SC.Binding.from("MyApp.contentController.contact").single().noError().notEmpty()

 

Configuring Binding Defaults

 

When you are designing a class and you expect to accept bindings, sometimes it is useful to be able to set some default configurations for all bindings made to a property.  For example, if your object has an isVisible property and that property must always be a Boolean, you don't want to have to ensure any other component that creates a binding uses the bool() transform.

 

Instead, you should configure a binding default.  Binding defaults are setup just like bindings except instead of ending your property name in "Binding" end it in "BindingDefault".  Also, you shouldn't actually provide a path - those will come when real bindings are setup.

 

MyApp.VisibleView = SC.View.extend({

 

  isVisibleBindingDefault: SC.Binding.bool()

});

 

var view = MyApp.VisibleView.create({

  isVisibleBinding: "MyApp.mainController.showView" // uses bool() transform

});

 

In  the example above, VisibleView configures the default binding for isVisible using the bool() transform.  When an actual instance of the view is created later, since no explicit binding configuration is provided, the default binding settings will be used instead.

 

Writing Custom Transforms

 

In addition to using the built-in transforms for bindings, you can also add your own custom 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 simply 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, here is how you might implement a transform that forces all values into an upper case string:

 

function(value, isForward) {

  return (value && value.toString) ? value.toString().toUpperCase() : '';

}

 

To add a custom transform to a Binding, just use the transform() helper, like so:

 

titleBinding: SC.Binding.from("MyApp.mainController.title").transform(function(value, isForward) {

  return (value && value.toString) ? value.toString().toUpperCase() : '';

})

 

This is an easy way to add a one-off transform.  If you want to add a new named transform to use over and over again, just define a new method on the SC.Binding object like so:

 

SC.Binding.string = function() {

  return this.transform(function(value, isForward) {

    return (value && value.toString) ? value.toString().toUpperCase() : '';

  });

};

 

--

titleBinding: SC.Binding.form('MyApp.mainController.title').string()

 

Manually Creating 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.

 

Disconnecting Bindings

 

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.form('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.

 

Moving On 

 

Learn about Enumerables, Arrays, and Sets »

or Back to Runtime Programming Guide Home »

Comments (2)

two-bit-fool said

at 8:18 pm on Nov 15, 2009

It may be useful to explain: "Unlike observers, bindings do not fire immediately when a property changes." Why the difference between the two?

Tim Evans said

at 10:31 am on Nov 1, 2010

It also might be useful to say that bindings do not get attached immediately. They are deferred until the run loop is completed.

You don't have permission to comment on this page.