Site editor

Angle Bracket Components: A first look

Rejoice! Angle bracket invocation now supports nested components! Let's see how this sweet new syntax improves the clarity of our templates.

Summary

Angle bracket invocation is a new feature that lets us invoke components like this

<Header />

<DropdownMenu as |menu|>
  <menu.Item>Item 1</menu.Item>
  <menu.Item>Item 2</menu.Item>
  <menu.Item>Item 3</menu.Item>
</DropdownMenu>

instead of this

{{x-header}}

{{#dropdown-menu as |menu|}}
  {{#menu.Item}}Item 1{{/menu.Item}}
  {{#menu.Item}}Item 2{{/menu.Item}}
  {{#menu.Item}}Item 3{{/menu.Item}}
{{/dropdown-menu}}

The feature first landed in 3.4 but it wasn't until 3.10 (currently in beta) that every aspect of the design was finalized. Fortunately for us, there's a polyfill for pre-3.10 Ember apps called ember-angle-bracket-invocation-polyfill that we can use today.

Let's play around with it a bit!

ember install ember-angle-bracket-invocation-polyfill

We'll start with a ui-button component that we use in EmberMap's codebase. Here's a current invocation:

{{#ui-button style='brand link' href=(href-to 'subscribe') data-test-id='signup'}}
  Subscribe
{{/ui-button}}

and here's what it looks like with angle brackets:

<Button @style='brand link' @href={{href-to 'subscribe'}} data-test-id='signup'>
  Subscribe
</Button>

We've done several things:

  • changed curlies {{ }} to angle brackets < >
  • prefixed JavaScript arguments with an @ sign (@style=)
  • left HTML attributes as plain words (data-test-id=)
  • dropped the restriction on two-word components ("ui-button" to just "button")

Hopefully you agree the clarity of this template is much improved! It's great that we can now use single-word components, and that we can so easily distinguish our JavaScript data flow from normal HTML attributes.

Let's look at one more use case. This one is exciting because we are finally able to invoke nested components using angle brackets.

We have a ui-grid component that yields out a contextual ui-grid/column component. Here's how ui-grid's template is implemented:

<div class='flex flex-wrap justify-center {{gridClasses}}'>
  {{yield (hash
    column=(component 'ui-grid/column' class=columnClasses)
  )}}

  {{#each (range 1 maxColumns}}
    {{ui-grid/column class=columnClasses}}
  {{/each}}
</div>

With the new syntax, we'll be able to change our invocation of ui-grid/column to this:

- {{ui-grid/column class=columnClasses}}
+ <UiGrid::Column class={{columnClasses}} />

Notice we use the :: separator for each directory.

We can also drop the ui-* prefix, since we can use single-word components. Our final diff looks like this:

  <div class='flex flex-wrap justify-center {{gridClasses}}'>
    {{yield (hash
-     column=(component 'ui-grid/column' class=columnClasses)
+     column=(component 'grid/column' class=columnClasses)
    )}}

    {{#each (range 1 maxColumns}}
-     {{ui-grid/column class=columnClasses}}
+     <Grid::Column class={{columnClasses}} />
    {{/each}}
  </div>

That means we can change the calling site as well. Here's the original invocation:

{{#ui-grid columns='md:2 lg:3' gutters='md:3' as |grid|}}
  {{#each sortedEpisodes as |episode|}}
    {{#grid.column}}
      {{podcast/components/podcast-card episode=episode}}
    {{/grid.column}}
  {{/each}}
{{/ui-grid}}

and our final template:

<Grid @columns='md:2 lg:3' @gutters='md:3' as |grid|>
  {{#each sortedEpisodes as |episode|}}
    <grid.Column>
      <Podcast::Components::PodcastCard @episode={{episode}} />
    </grid.Column>
  {{/each}}
</Grid>

And the peasants rejoiced. ✨

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

us, or ask in #media on Discord