Ember Handbook

Composable Components

Synopsis

Invocation Styled Content Structure Logic
both optional optional optional yes

Also Known As

High(er)-Order Components (hoc), Contextual Components

A higher-order component is just a function that takes an existing component and returns another component that wraps it.

Abramov, D. (2015a)

Patterns

Combine with:

Principles

  • Are concerned with how things are connected.
  • May contain both presentational and container components inside but usually don’t have any DOM markup of their own except for some wrapping divs, and never have any styles.
  • Provide the data and behavior to presentational or other container components or yields it.
  • Yields components to compose them your own way.

Usages

  • Composite (inline/block): Connects internal components together
  • Kit (block only): Yields a construction kit
  • Chameleon:
    • Inline (Composite) - sensible default
    • Block (Kit) - customizer kit

Example

To demonstrate, the <Toggle> example from Camba (2017) is been used. His talk is highly recommend to watch!

Toggle Component

The toggle component consists of a label and switch component. See the folder structure for more information:

toggle/
  label/
    template.hbs
  switch/
    template.hbs
  component.ts
  template.hbs

And the code for each file:

{{#if (has-block-params)}}
  {{yield
    (hash
      Label=(component this.labelComponent for=this.id invoke=this.change)
      Switch=(component
        this.switchComponent
        id=this.id
        invoke=this.change
        checked=this.checked
        enabled=this.enabled
      )
    )
  }}
{{else if (has-block)}}
  {{#if @offLabel}}
    <this.labelComponent
      @value={{false}}
      @for={{this.id}}
      @invoke={{this.change}}
    >
      {{@offLabel}}
    </this.labelComponent>
  {{/if}}

  <this.switchComponent
    @id={{this.id}}
    @checked={{this.checked}}
    @invoke={{this.change}}
    @enabled={{this.enabled}}
  />

  <this.labelComponent @for={{this.id}} @invoke={{this.change}}>
    {{yield}}
  </this.labelComponent>

  {{#if @onLabel}}
    <this.labelComponent
      @value={{true}}
      @for={{this.id}}
      @invoke={{this.change}}
    >
      {{@onLabel}}
    </this.labelComponent>
  {{/if}}
{{else}}
  {{#if @offLabel}}
    <this.labelComponent
      @value={{false}}
      @for={{this.id}}
      @invoke={{this.change}}
    >
      {{@offLabel}}
    </this.labelComponent>
  {{/if}}

  <this.switchComponent
    @id={{this.id}}
    @checked={{this.checked}}
    @invoke={{this.change}}
    @enabled={{this.enabled}}
  />

  {{#if @label}}
    <this.labelComponent @for={{this.id}} @invoke={{this.change}}>
      {{@label}}
    </this.labelComponent>
  {{/if}}

  {{#if @onLabel}}
    <this.labelComponent
      @value={{true}}
      @for={{this.id}}
      @invoke={{this.change}}
    >
      {{@onLabel}}
    </this.labelComponent>
  {{/if}}
{{/if}}

Usage

Contextual components allow a variety of usages. They are showcased and explained in this section.

With Sensible Defaults

Using a component inline is its bare usage and applies the most default assumptions of the component author.

<Examples::Toggle @label="Toggle" @onLabel="On" @offLabel="Off" />

With Block Customization

Block usage let you modify the main part of the component.

<Examples::Toggle @onLabel="On" @offLabel="Off">
  <strong>Toggle</strong>
</Examples::Toggle>

As Kit

At this point, the component turns into a provider component. By yielding blocks you can build the UI yourself but the contextual component keeps the logic.

<Examples::Toggle as |t|>
  <t.Label @value={{true}} local-class="on">On</t.Label>
  <t.Switch />
  <t.Label>Toggle</t.Label>
  <t.Label @value={{false}} local-class="off">Off</t.Label>
</Examples::Toggle>

Parametrized with Custom Replacements

For more customizations use the decorator pattern to wrap the original component (or create a new one) and pass that component as an argument into the contextual component as its strategy. It will now yield your own component but connected to the logic of the contextual component.

<Examples::Toggle::Label
  @for={{@for}}
  @invoke={{@invoke}}
  @value={{@value}}
  local-class='fancy'
>{{yield}}</Examples::Toggle::Label>

Put into the example:

<Examples::Toggle
  @labelComponent={{component "examples/fancy-label"}}
  @onLabel="On"
  @offLabel="Off"
>
  <strong>Toggle</strong>
</Examples::Toggle>