Advanced Ember.js Bindings

Bindings are one of the killer features of Ember.js. When you use the framework your are likely to use this good stuff probably via adding a Binding to your property name, like valueBinding: 'App.controller.value'. There are some enhanced ways of using bindings which are covered in this blog post. I’ll show how you can create bindings which transform values, convert a value to a boolean and how chained bindings work.


Hey! This article references a pre-release version of Ember.js. Now that Ember.js has reached a 1.0 API the code samples below are no longer correct and the expressed opinions may no longer be accurate.


The stuff which handles bindings is defined in binding.js in the ember-metal package whereas the corresponding tests are located here. The source and the tests are definitely worth a look as they’ll give you in insight how bindings are implemented and the tests show how they can be used.

The magic where a property ending with Binding is bound to the specified path happens in packages/ember-runtime/lib/ext/mixin.js. What it basically does is to check if the key ends with ‘Binding’ and if so, it creates a new Ember.Binding object if the value is not already one. So the following two statements are the same but the first approach is much more concise and elegant.

1
2
boundValueBinding: 'App.controller.value'
boundValueBinding: Ember.Binding.from('App.controller.value')

Specifying the path to a property via valueBinding: 'App.controller.value' is limited though. It’s not possible to specifiy that an uppercase String version of the bound value shall be used. Transforming the length of an array to a boolean hasMoreThanTwoEntries is also not possible. This is where the Ember.Binding object enters the game. It offers useful methods for enhanced bindings which are covered in the next sections.

oneWay

If you need to bind to a property and only want to propagate changes in one direction you can use the #oneWay binding. Changes are only forwarded from the from side to the to side, which means that if the to side is changed directly, the from side may have a different value (as you can see in line #7 in the following example).

Ember.Binding#oneWay()JSFiddle
1
2
3
4
5
6
7
8
var obj = Ember.Object.create({
    value: 10,
    boundValueBinding: Ember.Binding.oneWay('value')
});

obj.set('value', 20); // value = 20 ; boundValue = 20
obj.set('boundValue', 30); // value = 20 ; boundValue = 30
obj.set('value', 40); // value = 40 ; boundValue = 40

single

If you want to assure that your bound value only has a single item and is not an array, use the #single binding. You can specify a placeholder which is returned when the array has more than one entry.

You can use any object as placeholder, so you’re not restricted to use a String.

Ember.Binding#single()JSFiddle
1
2
3
4
5
6
7
8
9
var obj = Ember.Object.create({
    value: null,
    boundValueBinding: Ember.Binding.single('value', 'more entries available')
});

obj.set('value', null); // value = null ; boundValue = null
obj.set('value', 1); // value = 1 ; boundValue = 1
obj.set('value', [1,2]); // value = [1,2] ; boundValue = 'more entries available'
obj.set('value', [3]); // value = [3] ; boundValue = 3

multiple

The #multiple() binding goes in the opposite direction of #single(): the value is transformed to an array.

Ember.Binding#multiple()JSFiddle
1
2
3
4
5
6
7
8
9
var obj = Ember.Object.create({
    value: null,
    boundValueBinding: Ember.Binding.multiple('value')
});

obj.set('value', null); // value = (null) ; boundValue = []
obj.set('value', 1); // value = 1 ; boundValue = [1]
obj.set('value', [1,2]); // value = [1,2] ; boundValue = [1,2]
obj.set('value', [3]); // value = [3] ; boundValue = [3]

bool, not and isNull

#bool() converts the from value to a boolean, where the transformation returns true for every value except null, undefined, an empty string or an empty array. #not() has the same rules as the #bool() binding, only that it inverts it. #isNull() transforms the value to true if the source is either null or undefined.

#bool(), #not() and #isNull()JSFiddle
1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = Ember.Object.create({
    boolValueBinding: Ember.Binding.bool('value'),
    notValueBinding: Ember.Binding.not('value'),
    isNullValueBinding: Ember.Binding.isNull('value')
});

App.obj.setSync('value', null); // value = null ; boolValue = false ; notValue = true ; isNullValue = true
App.obj.setSync('value', false); // value = false ; boolValue = false ; notValue = true ; isNullValue = false
App.obj.setSync('value', []); // value = [] ; boolValue = true ; notValue = false ; isNullValue = false
App.obj.setSync('value', ''); // value = '' ; boolValue = false ; notValue = true ; isNullValue = false
App.obj.setSync('value', 'hello');// value = 'hello' ; boolValue = true ; notValue = false ; isNullValue = false
App.obj.setSync('value', [1]);// value = [1] ; boolValue = true ; notValue = false ; isNullValue = false
App.obj.setSync('value', {}); // value = {} ; boolValue = true ; notValue = false ; isNullValue = false

notEmpty and notNull

These bindings are similar to #single: they transform to a placeholder if the value is null/undefined respectively an empty string or an empty array.

#notEmpty() and #notNull()JSFiddle
1
2
3
4
5
6
7
8
9
10
var obj = Ember.Object.create({
    notEmptyValueBinding: Ember.Binding.notEmpty('value', 'is empty'),
    notNullValueBinding: Ember.Binding.notNull('value', 'is null')
});

obj.set('value', null); // value = null ; notEmptyValue = is empty ; notNullValue = is null
obj.set('value', ''); // value = '' ; notEmptyValue = is empty ; notNullValue = ''
obj.set('value', []); // value = [] ; notEmptyValue = is empty ; notNullValue = []
obj.set('value', 'hello'); // value = 'hello' ; notEmptyValue = 'hello' ; notNullValue = 'hello'
obj.set('value', [1, 2]);// value = [1,2] ; notEmptyValue = [1,2] ; notNullValue = [1,2]

and and or

By using these bindings you can combine two paths logically by and respectively or. If one of the sources changes, the bound value is recalculated. This binding only works in one way.

For the and transformation the returned value is the result of Ember.getPath('pathA') && Ember.getPath('pathB') where for the or transformation the calculation is Ember.getPath('pathA') || Ember.getPath('pathB').

#and() and #or()JSFiddle
1
2
3
4
5
6
7
8
9
10
11
var obj = Ember.Object.create({
    user: Ember.Object.create({isAdmin: false, isOwner: false}),
    isSaveEnabledBinding: Ember.Binding.and('user.isAdmin', 'isSelected'),
    canReadBinding: Ember.Binding.or('user.isAdmin', 'user.isOwner')
});

obj.set('isSelected', false); // isSaveEnabled = false ; canRead = false
obj.set('user', Ember.Object.create({isAdmin: true, isOwner: false})); // isSaveEnabled = false ; canRead = true
obj.set('isSelected', true); // isSaveEnabled = true ; canRead = true
obj.set('user', Ember.Object.create({isAdmin: false, isOwner: true})) // isSaveEnabled = false ; canRead = true
obj.set('user', Ember.Object.create({isAdmin: false, isOwner: false})); // isSaveEnabled = false ; canRead = false

transform

If none of the above transformations fits your needs, you can simply write your own transformation function. Simply add a transformation function to the transform binding helper. If you want to support bi-directional bindings, pass a hash with to and from properties.

Ember.Binding#transform()JSFiddle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = Ember.Object.create({
    uppercaseValueBinding: Ember.Binding.transform(function(value, binding) {
        return (Ember.typeOf(value) === 'string') ? value.toUpperCase() : value;
    }).from('value'),

    camelizedValueBinding: Ember.Binding.transform({
        to: function(value, binding) {
            return (Ember.typeOf(value) === 'string') ? value.camelize() : value;
        },
        from: function(value, binding) {
            return (Ember.typeOf(value) === 'string') ? value.decamelize() : value;
        }
    }).from('value')
});

obj.set('value', null); // value = null ; uppercaseValue = null ; camelizedValue = null
obj.set('value', ''); // value = '' ; uppercaseValue = '' ; camelizedValue = ''
obj.set('value', 'hello_world'); // value = 'hello_world' ; uppercaseValue = 'HELLO_WORLD' ; camelizedValue = 'helloWorld'

// because camelizedValue has a bi-directional binding, the decamelized string is set for value
obj.set('camelizedValue', 'helloAgain'); // value = 'hello_again' ; uppercaseValue = 'HELLO_AGAIN' ; camelizedValue = 'helloAgain'

If you want to reuse your transformation function you could add it to Ember.Binding

add transformation to Ember.BindingJSFiddle
1
2
3
4
5
6
7
8
9
10
11
Ember.Binding.uppercase = function(path) {
  return this.from(path).transform(function(value) {
      return (Ember.typeOf(value) === 'string') ? value.toUpperCase() : value;
  });
};

var obj = Ember.Object.create({
  uppercaseBinding: Ember.Binding.uppercase('value')
});

obj.set('value', 'hello'); // value = 'hello' ; uppercase = 'HELLO'

chaining

It’s possible to chain bindings and hereby create even more powerful constructs. So you could create an oneWay binding which transforms to the uppercase version of a string.

chainingJSFiddle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var uppercase = function(value) {
    return (Ember.typeOf(value) === 'string') ? value.toUpperCase() : value;
};

var obj = Ember.Object.create({
  value: '',
  uppercaseValueBinding: Ember.Binding.from('value').transform(uppercase),
  oneWayUppercaseValueBinding: Ember.Binding.oneWay('value').transform(uppercase)
});

obj.set('value', 'hello'); // value = 'hello' ; oneWayUppercaseValue = 'HELLO' ; uppercaseValue = 'HELLO'
obj.set('oneWayUppercaseValue', 'world'); // value = 'hello' ; oneWayUppercaseValue = 'world' ; uppercaseValue = 'HELLO'
obj.set('uppercaseValue', 'This Works Too'); // value = 'This Works Too' ; oneWayUppercaseValue = 'THIS WORKS TOO' ; uppercaseValue = 'This Works Too'
obj.set('value', 'buster');  // value = 'buster' ; oneWayUppercaseValue = 'BUSTER' ; uppercaseValue = 'BUSTER'

Manually create bindings

It’s also possible to define bindings outside of an object definition, although as stated in the docs you should almost always use the higher level APIs to create bindings.

To create a new binding instance simply call var binding = Ember.Binding.from('App.controller.value').to('myValue'). This describes a binding from the value property of App.controller to a myValue property.

To actually connect the binding to a specific object just invoke #connect() and to disconnect invoke the #disconnect() method. UPDATE: as Peter stated in the comments, it’s important to say if you manually create bindings via #connect you have to make sure to #disconnect them as soon as you don’t need them anymore. Otherwise you’ll run into memory leaks. If you use the ***Binding syntax you’re fine since Ember.js handles this for you.

manually create bindingsJSFiddle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
App.controller = Ember.Object.create({
  value: 'my Value'
});
App.target = Ember.Object.create({});

var binding = Ember.Binding.from('App.controller.value').to('boundValue');
binding.connect(App.target);

// synchronize bindings
Ember.run.sync();

// App.controller.value === App.target.boundValue === 'my Value'

App.controller.set('value', 10);
Ember.run.sync();

// App.controller.value === App.target.boundValue === 10

binding.disconnect(App.target);
App.controller.set('value', 'trololo');
Ember.run.sync();

// App.controller.value === 'trololo' && App.target.boundValue === 10

There are shortcuts available: Ember.bind() and Ember.oneWay().

But again, always use the elegant approach via propertyBinding: 'App.controller.value' respectively propertyBinding: Ember.Binding.oneWay('App.controller.value')

Summary

Bindings in Ember.js are a very mighty concept. By using transformations and custom transformations they are even mightier. I hope this post gave you some insight in bindings and how you can use them in your applications.

Comments