What you didn't know about class and attribute bindings in Ember

What you didn't know about class and attribute bindings in Ember

Every Ember developer has done this many times:

Ember.Component.extend({  
  validationResult: Ember.computed(/*...*/),
  classNameBindings: ['validationResult:is-valid:is-invalid']
})

Ember will apply either is-valid or is-invalid HTML class to the component depending on whether validationResult property is truthy.

In this case, the validationResult property is looked up on the component.

Trying to pass classNameBindings externally

There are situations when you want to pass classNameBindings into a component from the parent template.

Say, you need a custom HTML class on the {{textarea}} component, but you don't want to bother subclassing the Ember.TextArea component. Why create a custom component when you can simply pass classNameBindings and validationResult into the standard {{textarea}}, right?

This is what my intuition tells me to do, but it does not work:

{{textarea
  validationResult  = (gte myText.length 100),
  classNameBindings = 'validationResult:is-valid:is-invalid'
}}

What happens

classNameBindings is operated by Ember's deprecated binding mechanism. The mechanism is documented here: http://emberjs.com/api/classes/Ember.Binding.html

Historically, this low-level API was used to set up bindings in EmberJS. Then it was replaced with the convenient high-level API that we know today, and instead of myPropBinding='foo' we can simply do myProp=foo in our templates. Note that the former uses quotes and the latter doesn't.

This code:

{{textarea
  classNameBindings = 'validationResult:is-valid:is-invalid'
}}

is roughly equivalent to this:

{{textarea
  classNames = (if validationResult 'is-valid' 'is-invalid')
}}

But if you use the latter in your template, the HTML class will not be dynamic. It will use the initial value of validationResult, but when validationResult changes, the HTML class will not be updated.

This is why classNameBindings are there.

Passing classNameBindings into the default textarea component

You have to define the property on the parent component/controller and use its name in classNameBindings:

Ember.Component.extend({  
  validationResult: Ember.computed(/*...*/),
})
{{textarea
  classNameBindings = 'validationResult:is-valid:is-invalid'
}}

It is very important to understand that this example is different from the very first example in this article, even though it feels identical to classNameBindings: 'validationResult:is-valid:is-invalid'.

  • In the first example of this article, classNameBindings is evaluated in the context of the same component that it's applied to.

  • In the last example, classNameBindings is applied to the {{textarea}} component, but it is evaluated in the context of the parent component.

Passing multiple properties into classNameBindings externally seems to be impossible

I assumed this would work, but it doesn't:

{{textarea
  classNameBindings = (array 'validationResult:is-valid:is-invalid')
}}

...where array is a simple helper that returns its arguments as an array.

I did not find a way to pass more than one property into classNameBindings. If you need that, you'll have to subclass the component in question, so that you can apply classNameBindings internally, in the component's own JS file.

Know more? Share!

I wrote this post because classNameBindings caused a lot of frustration to me. Ricardo (@locks) kindly explained to me how it works, and I'm sharing my findings with the community.

But I still don't fully understand this bindings business, and what I've written here may very well be inaccurate.

If you see an inaccuracy or have a better explanation of the matter, don't hesitate to share in the comments!

Seamless software development.

Code management and collaboration platform with Git, Subversion, and Mercurial.

Sign up for free
comments powered by Disqus