Ember Best Practices: Using Native Select Elements

By: Aaron Sikes

This is the third in a series of posts designed to share our experiences around developing Ember.js apps and the best practices that we've found while doing so. Check out Lin's post on page objects and acceptance tests, or Estelle's post on not leaking state into factories.

Closure actions

Ember 1.13 came with the release of improved actions. A side effect of this feature is that it enables us to move away from {{view 'select'}} and simply use DOM <select> elements with actions.

His linked JSBin links to Ember canary and no longer works, but a clone modified to use 1.13 is available here.

You are probably familiar with the previous action bubbling semantics. Actions in a component are isolated within that component. Actions outside a component bubble first through the current controller, then through the route hierarchy.

The new approach of closure actions is simply to wrap this action up in a function and pass it around. Anyone can call this function later. And since the context is wrapped up in the function closure, the caller does not need to know where it came from.

For us, the important thing is that it enables us to bind actions directly to elements, like this: <select onchange={{action "doThing"}}>.

What's wrong with {{view 'select'}}?

Ember is moving away from two of the core concepts used by {{view 'select'}}.

Views

The Ember team, as well as many people in the community building Ember apps have discovered that the confusion around view scopes has been a huge source of hassle and bugs. The explicit scoping of components is much more maintainable. Thus, the Views section has been removed from the Ember guides, and the {{view}} helper has been deprecated. Nowadays, you get one view to go with your route, and otherwise View serves simply as a superclass to Component. As we move towards routable components, even the routes' views will begin to disappear.

Two-way data binding

Ember and the Ember ecosystem are also moving away from two-way bindings, and towards a Data Down, Actions Up architecture (DDAU).

Triggering an action includes context that is missing with simple data binding. Why is this value changing? Did we receive an update from the back-end, or did the user hit save? Lacking this context can lead to complicated conditional logic. Or, when combined with observers can cause an infinite loop hitting your back end.

It's also hard to go back once you opt-in to two-way bindings. You've hooked up a form field with data binding, and now you realize you need validation on that field. Oh, you also need a confirmation when it is valid (this is some high-stakes data). You'll find yourself using an observer, or splitting your value property into previousValue and currentValue properties. It is easier to hook in for these kinds of things when triggering actions instead.

There's also an issue of trust and boundaries. It turns out, "components want to be able to hand out data to their children without having to be on guard for wayward mutations." Data flowing only in one direction keeps things more predictable.

Okigetit, how do I do it?

Well, Stefan's JSBin I spoke about above is really a better example than I could give here. But, basically:

  1. Use a DOM <select> tag, and use an {{#each}} to render option tags.
  2. Bind an action to its onchange attribute.
  3. Create an action handler to set the property value.

But what about {{input}}?

Although {{input}} behaves more like a component than a view, there are similar issues around the data binding.

However, it turns out the same approach as above does not work quite as well with <input> tags. Because of how HTMLBars re-renders views, any time you type a character, your cursor will be reset to the end of the text field. This is awful! There's a way around it, using the side effects of readDOMAttr(). I won't go into it here, but Rob Jackson has put together a small example component using this technique.

May your forms be filled, your inputs valid, and your error template never render!

Note: This post has been edited. It originally recommended using <input> tags with actions. Because of the issues mentioned around cursor position, it has been changed to discuss select instead.