This articles describes the difference between how component invocation differs
when using curlies {{...}}
, angle brackets <...>
or an (...)
s-expression
in Ember templates.
Ember has three methods for invoking components and helpers in a template, either of the three can be used to invoke both classic and modern glimmer components.
The “classic” syntax in the form that uses curly braces {{...}}
, e.g.
{{user-profile firstName="Dan" lastName="F."}}
The “angle bracket” syntax that uses a HTML-like form <... />
, e.g.:
<UserProfile @firstName="Dan" @lastName="F." />
Finally the s-expression (for sub-expression) form used inside curly and angle
bracket syntax to invoke a sub component or helper. This takes the form
(concat a " " b)
, thus:
{{user-profile name=(concat firstName " " lastName)}}
and
<UserProfile @classNames={{concat "sticky " (if this.isActive "is-active")}} />
Angle Brackets
Similar to the vanilla DOM APIs that distinguish between JS properties from HTML attributes, angle bracket component invocations have two different namespaces you’re operating against.
You are probably most familiar with HTML attributes, which tell the browser how
to draw an HTML element. These attributes can do things like defining the alt
text on an image <img alt="A bird">
or the URL on an anchor tag
<a href="https://example.com">
. Angle bracket syntax implements attributes in
a similar way, allowing the developer to apply these attributes to a DOM node
somewhere in the component’s template.
<!-- parent.hbs -->
<UserProfile class="abc" @tagName="figure" />
The ...attributes
syntax determines where the attributes passed into a
component from an angle bracket invocation should appear in the component’s
template. Any number of attributes
and element modifiers
can be specified on the user profile component now, and they will all be applied
to the element that has ...attributes
on it.
<!-- UserProfile.hbs -->
<figure>
<img ...attributes src="default.jpg" />
</figure>
the resulting HTML output is:
<figure>
<img class="abc" src="default.jpg" />
</figure>
Anything prefixed with @
is an “argument”, is passed to the component by its
caller, is accessible to the class backing the component, and can be any JS
runtime value. Unlike attributes, which tell the browser what to render,
arguments tell your custom Ember component tag what to do.
<!-- parent.hbs -->
<UserProfile @name="Dan F." />
This template invokes the <UserProfile>
component, which expects one argument:
@name
, the value we pass is the hardcoded string “Dan F.”.
In the backing component class, arguments are namespaced on the this.args
object, which is immutable. You can access this argument as:
// user-profile.js
this.args.name; // "Dan F."
To illustrate the differences, here is an invocation of an angle bracket component using both arguments and attributes:
<Foo
@bar="{{123}}"
@baz="{{hash"
a="1"
b="hi"
}}
class="hello"
data-fizz="ok"
/>
In the above, bar
and baz
would be arguments with the values 123
and
{ a: 1, b: 'hi' }
respectively, while class
and data-fizz
would be
attributes that could be applied to a DOM node somewhere in the component’s
template.
Curly and s-expression Syntax
Ember curly (also called “classic”) component invocation only have one
namespace: the argument, everything is treated as an argument, as if you
implicitly included @
in front of each one. Historically, this was compensated
for by automatically applying the value of the class
argument to the class
attribute of a component’s root element (if it had one), and this behavior
could be extended to other attributes using the Ember classic
attributeBindings
API.
<!-- parent.hbs -->
{{user-profile bar={{123}} baz=(hash a=1 b='hi') class="hello"
data-fizz="ok"}}
In the above, bar
, baz
, class
and data-fizz
would all be treated as
arguments to the receiving component, so any ...attributes
in its template
would be a no-op. In Ember classic components, all of these arguments can be
accessed in the backing component class using either the argument name alone,
<argument name>
, or using this.<argument name>
, thus:
// user-profile.js
bar; // 123
this.bar; // 123
baz; // { a: 1, b: 'hi' }
this.baz; // { a: 1, b: 'hi' }
this.class; // 'hello'
this["data-fizz"]; // 'ok'
Usage in the template is:
<!-- user-profile.hbs -->
{{bar}}<br />
{{this.bar}}<br />
{{baz}}<br />
{{this.baz}}<br />
{{this.class}}<br />
{{data-fizz}}<br />
{{this.data-fizz}}
results in the following output:
123<br />
123<br />
[Object]<br />
[Object]<br />
hello<br />
ok<br />
ok
It’s important to note that both Ember classic and glimmer components can be
invoked by either syntax: classic curly or angle brackets—and what matters is
how the backing class is defined. Thus, if the above example was implemented as
a modern glimmer component, the backing class must access the argument bar
with this.args.bar
. If the above example was implemented as a classic
component, the backing class must access the argument with either bar
or
this.bar
.
So What’s Missing?
For those following closely, you may have noticed that this leaves us with some kind of hole in the programming model, because there’s no angle-bracket equivalent for setting up a contextual component to pass in as an argument or yield out as a block parameter. That’s the topic under discussion at Ember RFC issue #497.
In the meantime, <div class={{@class}} local-class="whatever" ...attributes>
will ensure that a class applied to a component will pass through correctly
regardless of how it’s being invoked.