Awesome Ember.js Form Components
A common ember pattern is a form that edits properties of an object. In this post I will show how to use nested components and dynamic bindings to produce concise forms in your templates with automatic <label>
tags, bootstrap compatibility, and a very clean api.
Shameless plug: This post builds a simplified example of the forms that are used in Emberkit, my Ember/Rails SaaS kit. If you find this post useful, have a look, as you'll find many more useful techniques demonstrated there:
Starting out
Let's start out with a basic form implemented using built–in ember primitives:
This is much like a normal HTML form. One thing that may be different to how you would usually use buttons in ember is that the button itself is a plain <button type="submit">
with no Ember code whatsoever, and the {{action "updateUser" on="submit"}}
helper is attached to the form tag.
This ensures that the form can be submitted both by clicking on the button, and by pressing "Enter" when one of the fields is focussed. It's important to preserve the UX that your users are used to and forms that don't submit properly with the usual keyboard interactions can be particularly frustrating.
Introducing a component template
This is a good start, and it works fine. However it's a bit verbose and repetitive. Each form input needs a lot of code to add a label and assign a unique ID so that the label is associated with the input. This is annoying and error-prone. Let's add in a component to reduce some of the duplication:
This is already a big improvement. The code duplication is reduced and it's simpler to look at. However I'm not completely happy with the API, and currently there's the potential for a bug - the form-field
component uses the label that you pass in as its ID. This is not ideal as IDs are supposed to be unique, and this component would only let you have one field with a given label on the page at a time.
Adding a component object
Currently there's only a component template, but no object. Components can be template–only, object–only, or have both a template and an object. For an example of an object-only component, have a look at my discuss post about Bootstrap, active links and LIs.
Let's add a component object and give it a smart default for the field type. It will default to "text" unless the label contains the string "password":
Nesting components
Of course, components can be nested. If we have lots of forms in our app, we probably want to make it simpler to write the surrounding form tag as well:
This is looking really good. Note the use of sendAction
in App.ObjectFormComponent
to send an action from the component that is otherwise isolated from the surrounding scope.
Adding some magic
Up to this point I don't think anything I've done is remotely controversial. However this next bit might be. Components are supposed to be isolated from their surrounding scope. However, I have found that it makes sense to break this isolation in the circumstance where you want to nest components and give them specific behaviour based on how they are nested. If you want to read more or have an opinion on this topic, have a look at my discourse post. For now, to improve this api even further and also solve the id problem, I'll show an example of what component nesting and dynamic binding can do:
There's quite a bit going on here so I'll break down the last changes one at a time.
The end result is that we now have a terser API for specifying form fields – {{form-field for="name"}}
instead of {{form-field label="Name" value=name}}
– and the form fields each have access to an object
property, which is what is passed into the parent object-form
component as the for=this
argument. Remember that in a template {{this}} refers to the controller, which in this case is an ObjectController that proxies to the current User created in the model hook.
Having access to the object that the form refers to allows solving the ID problem mentioned earlier, by creating a field id that is composed of a guid unique to the object, and also the field name:
fieldId: (->
"#{Em.guidFor @get('object')}-input-#{@get('for')}"
).property 'object', 'for'
The most complex part of this final version is the dynamic binding setup:
setupBindings: (->
@binding?.disconnect @ # Disconnect old binding if present
# Create a binding between the value property of the component,
# and the correct field name on the model object.
@binding = Em.Binding.from("object.#{@get 'for'}").to('value')
# Activate the binding
@binding.connect @
).on('init').observes 'for', 'object'
# Ensure the bindings are cleaned up when the component is removed
tearDownBindings: (->
@binding?.disconnect @
).on 'willDestroyElement'
The reason this is necessary is to allow for passing in a string to the component in the template, and getting access to both that string, and the value of the property referred to by the string. Usually it would be one or the other. Because we have access to the "object" that the form is referring to, through an alias to the parent component (object: Em.computed.alias 'parentView.for'
), we can use the string to create a binding to the correct property on the object.
First any existing dynamic binding is disconnected if it is present. Then a manual binding is created that connects the correct property on the form's object to a property called value
on the form-field
component, and then that binding is activated by calling the connect method. A little bit of binding magic can often help adding polish to APIs!
Finally, we destroy the binding when the component is removed from the DOM using the disconnect method.
Bonus - Optional block component
So there's one more feature of components that's not that often used but is really useful. Our form-field
component only handles text or password inputs at the moment, but what if we want to handle more input types? Sure, we can keep adding more automatic behaviour (and in the version of this code I use, textareas are automatically handled as well). But you can never plan for everything. What if you want some custom input that you only use in one place in your app?
This is when an optional block will come in handy. The idea is to make the component behave differently depending on whether it's used in block form or inline form:
Inline form - use default input field:
{{form-field for="email"}}
Block form - provide custom input but still use wrapper and label etc:
{{#form-field for="somethingElse"}}
{{my-crazy-custom-custom-input-field value=somethingElse}}
{{/form-field}}
This is easy enough to achieve with {{#if template}} {{yield}} {{/if}}
in your component's handlebars template. If a block was provided, the template
property will be present. If no block is provided, it will be absent:
Wrapup
Hopefully this has been a useful walk-through of some advanced uses of components. If you want a much more comprehensive form library that helps with building forms in ember, you might want to check out Ember EasyForm which inspired the API demonstrated here.