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. ✨