The Path to Contextual Components: Contextual Components

Contextual components are introduced in Ember.js 2.3 to improve component composition and the usage of components for DSL.

Contextual components bring three new features to Ember:

  1. The hash helper: this simple helper just returns a hash with the attributes passed to it.
  2. Closure components: similar to closure actions, a closure component encapsulates a component with a set of attributes for later reuse.
  3. Dot syntax: any identifier with a dot (.) whose context is a closure component will be rendered as the component itself.

Using the hash helper

The hash helper, though simple, is a fundamental element of this new feature as we will see in the third part. Invoking the hash helper with a collection of key-value pairs returns a hash with the given collection as content.

{{! app/templates/components/my-hash.hbs }}
{{yield (hash text="my-text-component"
checkbox="my-checkbox-component"
textarea="my-textarea")}}

This component will yield a hash with the passed key-value pairs. The following template:

{{#my-hash as |f|}}
{{f.text}}
{{f.checkbox}}
{{f.textarea}}
{{/my-hash}}

would produce three lines with the names of the components: my-text-component, my-checkbox-component, and my-textarea.

Twiddle

Creating a closure component

The first part of using contextual component is the ability to create a closure around a component and a set of parameters and attributes. The {{component}} helper has been modified to be able to render these closures too.

Creating a new contextual component is very easy: you just need to use the component helper!

Let us create our first contextual component:

{{! app/templates/components/my-greeter.hbs }}

{{yield (component (if formal "formal-greeting" "informal-greeting") name=name)}}
{{! app/templates/components/formal-greeting.hbs }}

Good morning, Mr/Mrs {{name}}!
{{! app/templates/components/informal-greeting.hbs }}
Hey, {{name}}, how's it going?

Then, we can render our closure using the component helper as in the following snippet:

{{#my-greeter formal=true name="Sergio" as |greet|}}
{{component greet}}
{{component greet name="Matthew"}}
{{/my-greeter}}

In my-greeter, (component ...) creates a closure over the inline if and name="Sergio". This closure is passed as first argument to the block. Then, when rendered with {{component greet}} would be like invoking it as {{component "formal-greeting" name="Sergio"}}. In the second example, the passed name is Matthew, the most recent value to the name attribute.

As with the usual {{component}} helper, the rendered component can change just by changing the first parameter to the helper. You can play with this twiddle to see basic contextual components in action.

Rendering a closure component without the component helper

Remember when I said that the hash helper, though simple, is a fundamental element of contextual components? Well, this is the part everything comes together. The best part of contextual components is that, if Ember finds and invocation with a point in the name containing a closure component, it will render the component. As an example:

{{! app/templates/components/form-for.hbs }}

{{yield (hash text-field=(component "my-text-field" object=object)
text-area =(component "my-text-area" object=object)
checkbox =(component "checkbox" object=object))}}
import Ember from 'ember';

const FormForComponent = Ember.Component.extend({});

FormForComponent.reopenClass({
positionalParams: ['object']
});

export default FormForComponent;

The FormForComponent is quite simple. It has a positional parameter mapped to object (just not to call it model) and exposes a hash with three different components. Then, we can invoke those components by using the dot syntax for contextual components:

{{! app/templates/your-route.hbs }}

{{#form-for model as |f|}}

{{f.text-field "name"}}
{{f.text-area "lastName"}}
{{f.checkbox "lovesEmber"}}

{{/form-for}}

When Ember finds a dot in the first part of a moustache, it checks if the value is a contextual component. If it is, Ember is intelligent enough to unbox it and render it.

Full example

Previous example needs some more code to be fully functional. The rest of the code can be found in this twiddle. There is an interesting trick being done there: you can use the mut helper with a get helper inside to make a mutable binding to a variable property of an object.

You can see the code in this gist as well.

Gotchas

  1. While passing null or undefined as component name to the component helper will render nothing, passing it to a closure component will raise an error.
  2. Conflicting positional parameters and attributes usually raise an assertion. When working with contextual components, a collision between current parameters and attributes with those from the closure won’t raise an error.

The Path to Contextual Components: Understanding dynamic components

The component helper allows the user to render a component based on a given name.

Given these two components:

{{! app/templates/components/first-component }}

This is first-component

{{name}}
{{! app/templates/components/second-component }}

This is second-component

{{name}}

We can render dynamically any of them with the following invocation:

{{component componentToRender name='Sergio'}}

The first parameter contains the name of the component you want to render. In this case, either 'first-component' or 'second-component'. The {{component}} helper passes everything but the first parameter to the component. In the example above, the rendered component receives name='Sergio'.

This twiddle demonstrates how to change the rendered component by changing a variable.

Rendering inputs and textareas

As of 2.3, inputs and textareas components cannot be rendered using dynamic components. The reason is simple: neither {{input}} nor {{texturea}} are components but helpers. There is a simple workaround for that. input is a helper around two components: Ember.TextField and Ember.Checkbox. Thus, we can extend those components just by creating new ones with no template and the following content:

// app/components/my-text-field
import Ember from 'ember';

// If you want to re-export it
export default Ember.TextField;

// If you want to extend it
export Ember.TextField.extend();
// app/components/my-checkbox
import Ember from 'ember';

// If you want to re-export it
export default Ember.Checkbox;

// If you want to extend it
export Ember.Checkbox.extend();

For textarea the code is pretty similar but with Ember.TextArea as the underlying component.

This twiddle demonstrates how it is done. There is a difference between using this technique and using a bound attribute to type. The former rises an assertion when using checkbox and a value attribute, like demonstrated in this twiddle.

Gotchas

  1. If null or undefined are passed as a component name, nothing is rendered.
  2. If a invalid or nonexistent component name is passed, an error is raised.

Go to the last post: Contextual Components!