Components
Components in Ember are little pieces with a purpose and can be composed into a larger picture.
They can be written in different formats.
Here is an <Avatar> component:
<template>
<aside>
<div class="avatar" title={{@title}}>{{@initial}}</div>
</aside>
</template>The top-level <template> tag is exported as the default component from the file. You can write this export explicitly, but it's not necessary. The following example is equivalent to the previous one.
export default <template>
<aside>
<div class="avatar" title={{@title}}>{{@initial}}</div>
</aside>
</template>;Arguments
Arguments are the input to your component:
import Avatar from '#/components/avatar.gjs';
<template>
<Avatar
@img="https://avatars.githubusercontent.com/u/1253363?s=200&v=4"
@name="Ember"
/>
</template>Which can be accessed inside the component from markup as well as from code.
import Component from '@glimmer/component';
export default class Avatar extends Component {
get initials() {
return this.args.name.split(' ').map(compound => compound.charAt(0)).join(' ');
}
<template>
{{#if @img}}
<img src={{@img}} title={{@name}} alt="Avatar of {{@name}}" />
{{else}}
<span title={{@name}}>{{this.initials}}</span>
{{/if}}
</template>
}Fallback Values
Some arguments are optional and you provide a fallback value with a getter:
import Component from '@glimmer/component';
import { getImageFromName } from 'some/where';
export default class Avatar extends Component {
get img() {
return this.args.img ?? getImageFromName(this.args.name);
}
<template>
<img sc={{this.img}} title={{@name}} alt="Avatar of {{@name}}" />
</template>
}Attributes
Attributes refer to HTML attributes that a component can forward to one or many elements with the special ...attributes keyword. They are unprefixed to give it the natural feeling we know from HTML and also for Ember to differentiate them from arguments.
Here is an example of forwarding HTML attributes to ...attributes.
import Component from '@glimmer/component';
export default class Avatar extends Component {
get initials() {
return this.args.name.split(' ').map(compound => compound.charAt(0)).join(' ');
}
<template>
{{#if @img}}
<img src={{@img}} title={{@name}} alt="Avatar of {{@name}}" ...attributes/>
{{else}}
<span title={{@name}} ...attributes>{{this.initials}}</span>
{{/if}}
</template>
}In either case of which element, the <Avatar> component is using, the attributes are forwarded. You can have as many ...attributes as you need, most often one is enough on the root of the component. Not using ...attributes disallows passing HTML attributes to the component.
import Avatar from '#/components/avatar.gjs';
<template>
<Avatar
@img="https://avatars.githubusercontent.com/u/1253363?s=200&v=4"
@name="Ember"
class="orange"
/>
</template>This will put the class="orange" attribute on the <img> element.
Blocks
Components can define areas that users can fill with their own HTML. Here is an example:
import Message from '#/components/message';
<template>
<Message>
This is the default <b>block content</b> that will
replace `{{yield}}` (or `{{yield to="default"}}`)
in the `Message` template.
</Message>
</template>inside of the <Message> component {{yield}} is used as "placeholder" for whatever a user is passing in:
<template>
<div>
{{yield}}
</div>
</template>The yield keyword makes sense, when writing the message component as a JavaScript function:
export function* message() {
const div = document.createElement('div');
yield div;
return div;
}
const msg = message();
const content = msg.next().value;
content.append(...);Conditional Blocks
Sometimes, we may want to provide some default content if the user of a component hasn't provided a block. For instance, consider an error message dialog that has a default message in cases where we don't know what error occurred. We could show the default message using the (has-block) syntax in an <ErrorDialog> component.
<template>
<dialog>
{{#if (has-block)}}
{{yield}}
{{else}}
An unknown error occurred!
{{/if}}
</dialog>
</template>Now, if we use our ErrorDialog component without a block, we'll get the default message.
import ErrorDialog from '#/components/error-dialog';
<template>
<ErrorDialog />
</template><!-- rendered -->
<dialog>An unknown error occurred!</dialog>If we had a more detailed message, though, we could use the block to pass it to the dialog.
import ErrorDialog from '#/components/error-dialog';
import Icon from '#/components/icon';
<template>
<ErrorDialog>
<Icon @type="no-internet" />
<p>You are not connected to the internet!</p>
</ErrorDialog>
</template>Block Parameters
Blocks can also pass values back into the template, similar to a callback function in JavaScript. Consider for instance a simple <BlogPost> component.
<template>
<h1>{{@post.title}}</h1>
<h2>{{@post.author}}</h2>
{{@post.body}}
</template>import BlogPost from '#/components/blog-post';
<template>
<!-- usage -->
<BlogPost @post={{@blogPost}} />
</template>We may want to give the user the ability to put extra content before or after the post, such as an image or a profile. Since we don't know what the user wants to do with the body of the post, we can instead pass the body back to them.
<template>
<h1>{{@post.title}}</h1>
<h2>{{@post.author}}</h2>
{{yield @post.body}}
</template>import BlogPost from '#/components/blog-post';
import AuthorBio from '#/components/author-bio';
<template>
<!-- usage -->
<BlogPost @post={{@blogPost}} as |postBody|>
<img alt="" role="presentation" src="./blog-logo.png">
{{postBody}}
<AuthorBio @author={{@blogPost.author}} />
</BlogPost>
</template>We can yield back multiple values as well, separated by spaces.
<template>
{{yield @post.title @post.author @post.body }}
</template>import BlogPost from '#/components/blog-post';
import AuthorBio from '#/components/author-bio';
<template>
<!-- usage -->
<BlogPost @post={{@blogPost}} as |postTitle postAuthor postBody|>
<img alt="" role="presentation" src="./blog-logo.png">
{{postTitle}}
{{postBody}}
<AuthorBio @author={{postAuthor}} />
</BlogPost>
</template>Named Blocks
If you want to yield content to different spots in the same component, you can use named blocks. Specify a name for the yielded block, like this:
<template>
{{yield someThing to="somePlace"}}
</template>INFO
You could also want to pass some values. This is the same process as the default yield, but you just have to pass to as the last argument.
Here is an <Alert> component as an example, that has content and optionally a title or action block.
<template>
<div role="alert">
{{#if @icon}}
<Icon @icon={{@icon}} part="icon" />
{{/if}}
{{#if (has-block "title")}}
<span part="title">
{{yield to="title"}}
</span>
{{/if}}
<div part="content">
{{#if (has-block "content")}}
{{yield to="content"}}
{{else}}
{{yield}}
{{/if}}
</div>
{{#if (has-block "actions")}}
<div part="actions">
{{yield to="actions"}}
</div>
{{/if}}
</div>
</template>Developing the component this way allows for some very friendly consumption. Let's start with a title and some content:
import { Alert } from '#/above';
<template>
<Alert>
<:title>A Magical Journey Takes Turns</:title>
<:content>
Here be dragons - ye'd be warned!
</content>
</Alert>
</template>The <:title> and <:content> blocks are matched with (has-block "<name>") and then yielded to. This form of constructing your components is very consumer friendly, as it allows a much simpler invocation:
import { Alert } from '#/above';
<template>
<Alert>
Here be dragons - ye'd be warned!
</Alert>
</template>No title, but a content block, even without <:content> named block, as it falls back to (has-block) and {{yield}}. When those two omit the name, it falls back to default.
(has-block)is the equivalent to(has-block "default"){{yield}}is the equivalent to{{yield to="default"}}
This is a very powerful strategie to develop components. Provide sensible defaults, then allow more verbose invocations that provide more customizations.
Signature
Components accept a Signature type parameter as part of their definition, that contains the synopsis of your component. This parameter is expected to be an object type with (up to) three members: Args, Element and Blocks.
Describing your component with a signature is for example used by an LSP and editor tooling to help you with intellisense. TypeScript is the language of choice, you can do it directly in a .gts file or with a .d.ts file.
Args represents the arguments your component accepts. Typically this will be an object type mapping the names of your args to their expected type. If no Args key is specified, it will be a type error to pass any arguments to your component.
The Element field declares what type of element(s), if any, the component applies its passed ...attributes to. This is often the component's root element. Tracking this type ensures any modifiers used on your component will be compatible with the DOM element(s) they're ultimately attached to. If no Element is specified, it will be a type error to set any HTML attributes or modifiers when invoking your component.
The Blocks field specifies the names of any blocks the component yields to, as well as the type of any parameter(s) those blocks will receive. If your component does not support block invocation, omit the Blocks field altogether to generate type errors when invoked in block form.
INFO
Note that the inverse block is an alias for else. These should be defined in Blocks as else, though {{yield to="inverse"}} will continue to work.
import type { TOC } from '@ember/component/template-only';
export interface SimpleHelloSignature {
// We have a `<div>` as our root element
Element: HTMLDivElement;
// We accept no arguments or block form, so don't specify them in the signature
}
const SimpleHello: TOC<SimpleHelloSignature> = <template>
...
</template>