Class: SC.Binding
A binding simply connects the properties of two objects so that whenever the value of one property changes, the other property will be changed also. You do not usually work with Binding objects directly but instead describe bindings in your class definition using something like:
valueBinding: "MyApp.someController.title"
This will create a binding from "MyApp.someController.title" to the "value" property of your object instance automatically. Now the two values will be kept in sync.
One-Way Bindings
By default, bindings are set up as two-way. In cases where you only need the
binding to function in one direction, for example if a value from a controller
is bound into a read-only LabelView, then for performance reasons you should
use a one-way binding. To do this, call the very useful oneWay
helper:
valueBinding: SC.Binding.oneWay('MyApp.someController.title')
or:
valueBinding: SC.Binding.from('MyApp.someController.title').oneWay()
This way if the value of MyApp.someController.title
changes, your object's
value
will also update. Since value
will never update on its own, this will
avoid the setup time required to plumb the binding in the other direction,
nearly doubling performance for this binding.
Transforms
In addition to synchronizing values, bindings can also perform some basic transforms on values. These transforms can help to make sure the data fed into one object always meets the expectations of that object, regardless of what the other object outputs.
To customize a binding, you can use one of the many helper methods defined
on SC.Binding.
For example:
valueBinding: SC.Binding.single("MyApp.someController.title")
This will create a binding just like the example above, except that now the
binding will convert the value of MyApp.someController.title
to a single
object (removing any arrays) before applying it to the "value" property of
your object.
You can also chain helper methods to build custom bindings like so:
valueBinding: SC.Binding.single("MyApp.someController.title").notEmpty(null,"(EMPTY)")
This will force the value of MyApp.someController.title
to be a single value
and then check to see if the value is "empty" (null, undefined, empty array,
or an empty string). If it is empty, the value will be set to the string
"(EMPTY)".
The following transform helper methods are included: noError
, single
, notEmpty
,
notNull
, multiple
, bool
, not
, isNull
, and
(two values only), or
(two
values only), and equalTo
. See each method's documentation for a full description.
(Note that transforms are only applied in the forward direction (the 'to' side); values are applied untransformed to the 'from' side. If the 'from' object has validation needs, it should own and apply them itself, for example via a read/write calculated property.)
Adding Custom Transforms
In addition to using the standard helpers provided by SproutCore, you can also defined your own custom transform functions which will be used to convert the value. To do this, just define your transform function and add it to the binding with the transform() helper. The following example will not allow Integers less than ten. Note that it checks the value of the bindings and allows all other values to pass:
valueBinding: SC.Binding.transform(function (value, binding) {
return ((SC.typeOf(value) === SC.T_NUMBER) && (value < 10)) ? 10 : value;
}).from("MyApp.someController.value")
If you would like to instead use this transform on a number of bindings,
you can also optionally add your own helper method to SC.Binding.
This
method should simply return the value of this.transform(). The example
below adds a new helper called notLessThan
() which will limit the value to
be not less than the passed minimum:
SC.Binding.notLessThan = function (minValue) {
return this.transform(function (value, binding) {
return ((SC.typeOf(value) === SC.T_NUMBER) && (value < minValue)) ? minValue : value;
});
};
You could specify this in your core.js file, for example. Then anywhere in your application you can use it to define bindings like so:
valueBinding: SC.Binding.from("MyApp.someController.value").notLessThan(10)
Also, remember that helpers are chained so you can use your helper along with any other helpers. The example below will create a one way binding that does not allow empty values or values less than 10:
valueBinding: SC.Binding.oneWay("MyApp.someController.value").notEmpty().notLessThan(10)
Note that the built in helper methods all allow you to pass a "from" property path so you don't have to use the from() helper to set the path. You can do the same thing with your own helper methods if you like, but it is not required.
Creating Custom Binding Templates
Another way you can customize bindings is to create a binding template. A template is simply a binding that is already partially or completely configured. You can specify this template anywhere in your app and then use it instead of designating your own custom bindings. This is a bit faster on app startup but it is mostly useful in making your code less verbose.
For example, let's say you will be frequently creating one way, not empty bindings that allow values greater than 10 throughout your app. You could create a binding template in your core.js like this:
MyApp.LimitBinding = SC.Binding.oneWay().notEmpty().notLessThan(10);
Then anywhere you want to use this binding, just refer to the template like so:
valueBinding: MyApp.LimitBinding.beget("MyApp.someController.value")
Note that when you use binding templates, it is very important that you always start by using beget() to extend the template. If you do not do this, you will end up using the same binding instance throughout your app which will lead to erratic behavior.
How to Manually Activate a Binding
All of the examples above show you how to configure a custom binding, but
the result of these customizations will be a binding template, not a fully
active binding. The binding will actually become active only when you
instantiate the object the binding belongs to. It is useful however, to
understand what actually happens when the binding is activated. (Of course
you should always use the highest-level APIs available, even if you understand
how it works underneath; unless you have specific needs, you should rely on
the convenience fooBinding
format.)
For a binding to function it must have at least a "from" property and a "to" property. The from property path points to the object/key that you want to bind from while the to path points to the object/key you want to bind to.
When you define a custom binding, you are usually describing the property you want to bind from (such as "MyApp.someController.value" in the examples above). When your object is created, it will automatically assign the value you want to bind "to" based on the name of your binding key. In the examples above, during init, SproutCore objects will effectively call something like this on your binding:
binding = this.valueBinding.beget().to("value", this);
This creates a new binding instance based on the template you provide, and sets the to path to the "value" property of the new object. Now that the binding is fully configured with a "from" and a "to", it simply needs to be connected to become active. This is done through the connect() method:
binding.connect();
Now that the binding is connected, it will observe both the from and to side and relay changes.
If you ever needed to do so (you almost never will, but it is useful to understand this anyway), you could manually create an active binding by doing the following:
SC.Binding.from("MyApp.someController.value")
.to("MyApp.anotherObject.value")
.connect();
You could also use the bind() helper method provided by SC.Object.
(This is
the same method used by SC.Object.init()
to setup your bindings):
MyApp.anotherObject.bind("value", "MyApp.someController.value");
Both of these code fragments have the same effect as doing the most friendly form of binding creation like so:
MyApp.anotherObject = SC.Object.create({
valueBinding: "MyApp.someController.value",
// OTHER CODE FOR THIS OBJECT...
});
SproutCore's built in binding creation method make it easy to automatically
create bindings for you. If you need further documentation on SC.Binding's
inner
workings, see the private method documentation in the source code.
Defined in: binding.js
- Since:
- SproutCore 1.0
Field Summary
Class Methods
Instance Methods
- and(the)
- bool(fromPath)
- connect()
- disconnect()
- equalTo(fromPath, equalValue)
- from(propertyPath, root)
- integer(fromPathOrRadix, radix)
- isNull(fromPath)
- mix(the, mixFunction)
- multiple(fromPath)
- noError(fromPath, aFlag)
- not(fromPath)
- notEmpty(fromPath, placeholder)
- notNull(fromPath, placeholder)
- oneWay(fromPath, aFlag)
- or(the)
- resetTransforms()
- single(fromPath, placeholder)
- string(fromPath)
- sync()
- to(propertyPath, root)
- transform(transformFunc)
Field Detail
SC.Binding.isBindingExtend SC.Binding
with properites that make it easier to detect bindings
in the inspector
Defined in: binding.js.
Class Method Detail
Adds a transform to format the DateTime value to a String value according to the passed format string.
valueBinding: SC.Binding.dateTime('%Y-%m-%d %H:%M:%S')
.from('MyApp.myController.myDateTime');
Defined in: datetime.js.
- Parameters:
- format String
- format string
- Returns:
- SC.Binding
- this
Defined in: binding.js.
Defined in: binding.js.
- Parameters:
- coder
Instance Method Detail
Adds a transform that returns the logical 'AND' of all the values at the provided paths. This is
a quick and useful way to bind a Boolean
property to two or more other Boolean
properties.
For example, imagine that we wanted to only enable a deletion button when an item in a list
is selected and the current user is allowed to delete items. If these two values are set
on controllers respectively at MyApp.itemsController.hasSelection
and
MyApp.userController.canDelete
. We could do the following,
deleteButton: SC.ButtonView.design({
// Action & target for the button.
action: 'deleteSelectedItem',
target: MyApp.statechart,
// Whether the list has a selection or not.
listHasSelectionBinding: SC.Binding.oneWay('MyApp.itemsController.hasSelection'),
// Whether the user can delete items or not.
userCanDeleteBinding: SC.Binding.oneWay('MyApp.userController.canDelete'),
// Note: Only enable when the list has a selection and the user is allowed!
isEnabled: function () {
return this.get('listHasSelection') && this.get('userCanDelete');
}.property('listHasSelection', 'userCanDelete').cacheable()
})
However, this would be much simpler to write by using the and
binding transform like so,
deleteButton: SC.ButtonView.design({
// Action & target for the button.
action: 'deleteSelectedItem',
target: MyApp.statechart,
// Note: Only enable when the list has a selection and the user is allowed!
isEnabledBinding: SC.Binding.and('MyApp.itemsController.hasSelection', 'MyApp.userController.canDelete')
})
Note:* the transform acts strictly as a one-way binding, working only in the one direction.
- Parameters:
- the String...
- property paths of source values that will be provided to the AND transform.
Adds a transform to convert the value to a bool value. If the value is
an array it will return YES
if array is not empty. If the value is a string
it will return YES if the string is not empty.
- Parameters:
- fromPath String Optional
- Returns:
- SC.Binding
- this
Attempts to connect this binding instance so that it can receive and relay changes. This method will raise an exception if you have not set the from/to properties yet.
- Returns:
- SC.Binding
- this
Disconnects the binding instance. Changes will no longer be relayed. You will not usually need to call this method.
- Returns:
- SC.Binding
- this
Adds a transform that will return YES
if the value is equal to equalValue
, NO otherwise.
isVisibleBinding
: SC.Binding.oneWay("MyApp.someController.title").equalTo(comparisonValue)
Or:
isVisibleBinding
: SC.Binding.equalTo("MyApp.someController.title"
, comparisonValue
)
- Parameters:
- fromPath String
- from path or null
- equalValue Object
- the value to compare with
- Returns:
- SC.Binding
- this
This will set "from" property path to the specified value. It will not attempt to resolve this property path to an actual object/property tuple until you connect the binding.
The binding will search for the property path starting at the root level
unless you specify an alternate root object as the second parameter to this
method. Alternatively, you can begin your property path with either "." or
"*", which will use the root object of the to side be default. This special
behavior is used to support the high-level API provided by SC.Object.
- Parameters:
- propertyPath String|Tuple
- A property path or tuple
- root Object Optional
- root object to use when resolving the path.
- Returns:
- SC.Binding
- this
Adds a transform that will always return an integer Number value. Null and undefined values will
return 0 while String values will be transformed using the parseInt
method (according to the
radix) and Boolean values will be 1 or 0 if true or false accordingly. Other edge cases like NaN
or other non-Numbers will also return 0.
Example results,
- null => 0
- undefined => 0
- '123' => 123
- true => 1
- {} => 0
- Parameters:
- fromPathOrRadix String
- from path or the radix for the parsing or null for 10
- radix String
- the radix for the parsing or null for 10
- Returns:
- SC.Binding
- this
- Parameters:
- fromPath String Optional
- Returns:
- SC.Binding
- this
Adds a transform that aggregates through a given function the values at the provided paths. The given function is called whenever any of the values are updated. This is a quick way to aggregate multiple properties into a single property value.
For example, to concatenate two properties 'MyApp.groupController.name' and 'MyApp.userController.fullName', we could do the following,
currentGroupUserLabel: SC.LabelView.extend({
// The group name (may be null).
groupNameBinding: SC.Binding.oneWay('MyApp.groupController.name'),
// The user full name (may be null).
userFullNameBinding: SC.Binding.oneWay('MyApp.userController.fullName'),
// Ex. Returns one of "", "Selected Group", or "Selected Group: Selected User"
value: function () {
var groupName = this.get('groupName'),
userFullName = this.get('userFullName');
if (SC.none(userFullName)) {
if (SC.none(groupName)) {
return ''; // No group and no user.
} else {
return groupName; // Just a group.
}
} else {
return '%@: %@'.fmt(groupName, userFullName); // Group and user.
}
}.property('groupName', 'userFullName').cacheable()
})
However, this is simpler (ex. 86 fewer characters) to write by using the mix
binding transform like so,
currentGroupUserLabel: SC.LabelView.extend({
// Ex. Returns one of "", "Selected Group", or "Selected Group: Selected User"
valueBinding: SC.Binding.mix(
'MyApp.groupController.name', // The group name (may be null).
'MyApp.userController.fullName', // The user full name (may be null).
// Aggregate function. The arguments match the bound property values above.
function (groupName, userFullName) {
if (SC.none(userFullName)) {
if (SC.none(groupName)) {
return ''; // No group and no user.
} else {
return groupName; // Just a group.
}
} else {
return '%@: %@'.fmt(groupName, userFullName); // Group and user.
}
})
})
Note:* the number of parameters of mixFunction
should match the number of paths provided.
Note:* the transform acts strictly as a one-way binding, working only in the one direction.
- Parameters:
- the String...
- paths of source values that will be provided to the aggregate function.
- mixFunction Function
- the function that aggregates the values
Adds a transform that will convert the passed value to an array. If the value is null or undefined, it will be converted to an empty array.
- Parameters:
- fromPath String Optional
- Returns:
- SC.Binding
- this
Specifies that the binding should not return error objects. If the value of a binding is an Error object, it will be transformed to a null value instead.
Note that this is not a transform function since it will be called at the end of the transform chain.
- Parameters:
- fromPath String Optional
- from path to connect.
- aFlag Boolean Optional
- Pass NO to allow error objects again.
- Returns:
- SC.Binding
- this
Adds a transform to convert the value to the inverse of a bool value. This uses the same transform as bool() but inverts it.
- Parameters:
- fromPath String Optional
- Returns:
- SC.Binding
- this
Adds a transform that will return the placeholder value if the value is
null, undefined, an empty array or an empty string. See also notNull
().
- Parameters:
- fromPath String
- from path or null
- placeholder Object Optional
- Returns:
- SC.Binding
- this
Adds a transform that will return the placeholder value if the value is
null or undefined. Otherwise it will pass through untouched. See also notEmpty
().
- Parameters:
- fromPath String
- from path or null
- placeholder Object Optional
- Returns:
- SC.Binding
- this
Configures the binding as one way. A one-way binding will relay changes on the "from" side to the "to" side, but not the other way around. This means that if you change the "to" side directly, the "from" side will not be updated, and may have a different value.
- Parameters:
- fromPath String Optional
- from path to connect.
- aFlag Boolean Optional
- Pass NO to set the binding back to two-way
- Returns:
- SC.Binding
- this
Adds a transform that returns the logical 'OR' of all the values at the provided paths. This is
a quick and useful way to bind a Boolean
property to two or more other Boolean
properties.
For example, imagine that we wanted to show a button when one or both of two values are present.
If these two values are set on controllers respectively at MyApp.profileController.hasDisplayName
and
MyApp.profileController.hasFullName
. We could do the following,
saveButton: SC.ButtonView.design({
// Action & target for the button.
action: 'saveProfile',
target: MyApp.statechart,
// Whether the profile has a displayName or not.
profileHasDisplayNameBinding: SC.Binding.oneWay('MyApp.profileController.hasDisplayName'),
// Whether the profile has a fullName or not.
profileHasFullNameBinding: SC.Binding.oneWay('MyApp.profileController.hasFullName'),
// Note: Only show when the profile has a displayName or a fullName or both!
isVisible: function () {
return this.get('profileHasDisplayName') || this.get('profileHasFullName');
}.property('profileHasDisplayName', 'profileHasFullName').cacheable()
})
However, this would be much simpler to write by using the or
binding transform like so,
saveButton: SC.ButtonView.design({
// Action & target for the button.
action: 'saveProfile',
target: MyApp.statechart,
// Note: Only show when the profile has a displayName or a fullName or both!
isVisibleBinding: SC.Binding.or('MyApp.profileController.hasDisplayName', 'MyApp.profileController.hasFullName')
})
Note:* the transform acts strictly as a one-way binding, working only in the one direction.
- Parameters:
- the String...
- paths of source values that will be provided to the OR sequence.
Resets the transforms for the binding. After calling this method the binding will no longer transform values. You can then add new transforms as needed.
- Returns:
- SC.Binding
- this
Adds a transform to the chain that will allow only single values to pass. This will allow single values, nulls, and error values to pass through. If you pass an array, it will be mapped as so:
[] => null
[a] => a
[a,b,c] => Multiple Placeholder
You can pass in an optional multiple placeholder or it will use the default.
Note that this transform will only happen on forwarded valued. Reverse values are send unchanged.
- Parameters:
- fromPath String
- from path or null
- placeholder Object Optional
- placeholder value.
- Returns:
- SC.Binding
- this
Adds a transform that will always return a String value. Null and undefined values will return
an empty string while all other non-String values will be transformed using the toString
method.
Example results,
- null => ''
- undefined => ''
- 123 => '123'
- true => 'true'
- {} => '[object Object]' (i.e. x = {}; return x.toString())
- Parameters:
- fromPath String
- from path or null
- Returns:
- SC.Binding
- this
Calling this method on a binding will cause it to check the value of the from side of the binding matches the current expected value of the binding. If not, it will relay the change as if the from side's value has just changed.
This method is useful when you are dynamically connecting bindings to a network of objects that may have already been initialized. Otherwise you should not need to call this method.
This will set the "to" property path to the specified value. It will not attempt to reoslve this property path to an actual object/property tuple until you connect the binding.
If you are using the convenience format fooBinding
, for example
isVisibleBinding
, you do not need to call this method, as the to
property
path will be generated for you when its object is created.
- Parameters:
- propertyPath String|Tuple
- A property path or tuple
- root Object Optional
- root object to use when resolving the path.
- Returns:
- SC.Binding
- this
Adds the specified transform function to the array of transform functions.
The function you pass must have the following signature:
function (value) {};
or:
function (value, binding) {};
It must return either the transformed value or an error object.
Transform functions are chained, so they are called in order. If you are
extending a binding and want to reset its transforms, you can call
resetTransform
() first.
- Parameters:
- transformFunc Function
- the transform function.
- Returns:
- SC.Binding
- this