anchorEmber Octane and editions
As a framework that values stability and providing its users solid upgrade paths, Ember has accrued its fair share of idiosyncrasies. One of the more demonstrable consequences of this is the so called pit of incoherence, alluded to in the opening keynote of EmberConf 2019: APIs are introduced that move the framework to a new mental model, but that mental model isn't complete yet. This leaves users unsure of which APIs to use when, and how the interop might work.
This is where editions come into play. The main goal of editions is to document and present a new and coherent mental model that developers can adhere to, while keeping compatibility with existing APIs. This is done in one of two ways, either a new API that can coexist with existing ones is introduced, or an optional feature that users can opt into is introduced. This allows for new applications to have the optional features and default blueprints of the newer editions, while existing applications can update their codebase at their own pace.
In this blog post we will address some changes to templates that were introduced with Ember Octane in mind, the first planned edition for the Ember.js framework. You can read more about Octane in the official edition page.
anchorAmbiguity
In Ember templates, interpolation of dynamic values is done through curly braces, {{}}
. Given this is the only syntax for dynamic values, there is an ambiguity problem at times. Let us look at a template:
{{! app/templates/components/blog/post.hbs }}
<h1>{{title}}</h1>
{{post-body post=post}}
<button {{action 'expandComments'}}>
{{#if commentsExpanded}}
{{#each post.comments as |commentObject|}}
{{comment comment=commentObject onReport=onReportComment
}}
{{/each}}
{{/if}}
Looking only at the template, can we be certain where title
comes from? Whether post-body
and comment
are helpers or components? We can generously assume that comment
is a component as it is receiving an action, but when scanning the file it is indistinguishable from if
, each
, and other templating constructs.
We will work step by step to remove ambiguity where we can.
anchorDisambiguating properties
We will start by marking which dynamic values come from the component's JavaScript. These are called properties. To do that we are going to consult the JavaScript file to see which properties it might define:
import Component from '@ember/component';
import { computed } from '@ember/object';
import titleCase from 'my-app/utils/title-case.js';
export default Component.extend({
title: computed('post.title', function () {
return titleCase(this.post.title);
}),
commentsExpanded: false,
actions: {
toggleComments() {
this.toggleProperty('commentsExpanded');
},
},
});
We see that title
is a computed property, and commentsExpanded
is a boolean defined in the class definition, so we will update the template accordingly:
{{! app/templates/components/blog/post.hbs }}
<h1>{{this.title}}</h1>
{{post-body post=post}}
<button {{action 'expandComments'}}>
{{#if this.commentsExpanded}}
{{#each post.comments as |commentObject|}}
{{comment comment=commentObject onReport=onReportComment
}}
{{/each}}
{{/if}}
The great thing about this feature is that all versions of Ember support it, so you can start annotating properties in your templates today. You can also enforce that no new implicit this
are added to templates by configuring the appropriate ember-template-lint
rule.
anchorDisambiguating named arguments
Next we will turn our attention to named arguments. We give this name to values that we pass into components when we use them. Since components do not have access to the context where they are called in, this is the primary way to pass information into them.
To see what we are passing into our blog/post
component, we need to see how it is used in our application. To do this, we will check a route template, namely the template for the blog
route, where the component is used:
{{! app/templates/blog.hbs }}
{{blog/post post=post onReportComment=(action "onReportComment")}}
We can see that when calling the component, we are passing in post
and onReportComment
. The syntax to mark them as named argument is a @
prefix, so let us update our template:
{{! app/templates/components/blog/post.hbs }}
<h1>{{this.title}}</h1>
{{post-body post=@post}}
<button {{action 'expandComments'}}>
{{#if this.commentsExpanded}}
{{#each post.comments as |commentObject|}}
{{comment comment=commentObject onReport=@onReportComment
}}
{{/each}}
{{/if}}
We have improved our template a further step, now we can tell properties and named arguments apart. Next we will dive into component invocation.
If you wish to use named arguments in your application, you can use ember-named-arguments-polyfill
for Ember.js versions older than 3.1.
Named arguments mostly apply to components, since they are the piece of Ember that is explicitly invoked by developers unlike route templates which the framework renders for you.
Note that @model
was introduced by RFC #523 "Model Argument for Route Templates" so that you can refer directly to the model that was passed to a route template.
anchorDisambiguating components
After the last change, it would be nice to have a way to disambiguate component invocations from other kinds of dynamic interpolations. Fortunately, a new syntax was introduced by RFC #311 "Angle Bracket Invocation", that allows us to do just that. Using angle bracket invocation also enables us to distinguish between named arguments passed to the component, and HTML attributes passed to the component. HTML attributes and ...attributes
will be covered in a future post.
To update, we replace curly braces with an HTML-like <>
syntax–hence angle bracket invocation,–using capital case for the name of the component, ::
instead of /
for nested components, and prefixing named arguments with @
. Here is how the above blog
and blog/post
templates look like once updated:
{{! app/templates/blog.hbs }}
<Blog::Post @post={{post}} @onReportComment={{action "onReportComment"}} />
{{! app/templates/components/blog/post.hbs }}
<h1>{{this.title}}</h1>
<PostBody @post={{@post}} />
<button {{action 'expandComments'}}>
{{#if this.commentsExpanded}}
{{#each post.comments as |commentObject|}}
<Comment @comment={{commentObject}} onReport={{@onReportComment
}} />
{{/each}}
{{/if}}
To use this feature in older versions of Ember and its dependencies, you can use ember-angle-bracket-invocation-polyfilll
.
With just a couple of tweaks, we now have much more clarity when reading a template, and the need to consult additional files is lessened:
- `` is a property that comes from the JavaScript file of the component;
<PostBody />
is a component invocation;- `` is a named argument that is passed to the component when invoked;
- `` is also a property;
if
andeach
are templating constructscommentObject
is a block argument that is available inside theeach
block scope<Comment />
is a component invocation
These were not the only improvements made to Ember's templating engine, or even to components. Also introduced to the framework were the ability to pass HTML attributes to components and apply them with ...attributes
, a simplification of DOM event handling with the on
and fn
modifiers replacing action
, the ability for users to create custom element modifiers like the built-in action
, on
, and fn
, rendering lifecycle element modifiers, and Glimmer components.
These new features will be covered in upcoming posts, so be sure to keep an eye out for them!
If you are looking for help to update your codebase to these new idioms, or you want to level up your engineering team, make sure to contact us so we can work together towards achieving your goals.