home
  • Blog
6.7
  • Introduction
  • Getting Started
  • Tutorial
  • Core Concepts
  • Components
  • Routing
  • Services
  • EmberData
  • In-Depth Topics
    • Autotracking In-Depth
    • Patterns for Components
    • Patterns for Actions
    • Making API Requests
    • Native Classes In-Depth
    • Rendering Values
  • Application Development
  • Application Concerns
  • Accessibility
  • Configuration
  • Testing
  • Addons and Dependencies
  • Using TypeScript
  • Developer Tools
  • Ember Inspector
  • Code Editors
  • Additional Resources
  • Upgrading
  • Contributing to Ember.js
  • Glossary
GJS Format - This guide is written assuming you are using GJS in your app. See this page to read more about GJS.

Patterns for Components


This guide assumes that you have read the component guides and are familiar with how Ember components work. We'll cover components in more depth in this section. At the end, we'll present recommended component patterns.

Argument Defaults

At some point, you may want to add default values to your arguments if one wasn't passed to your component. Arguments are not mutable, so if you attempt to reassign a value on this.args, it'll fail. Instead, you should define a getter on your component that provides the default value if the argument was not provided.

For instance, if you wanted to create a tooltip icon that had a standard icon and class, you could do it like so:

app/components/tooltip.gjs
import Component from '@glimmer/component';

export default class TooltipComponent extends Component {
  get icon() {
    return this.args.icon ?? 'icon-info';
  }

  get tooltipClass() {
    return this.args.tooltipClass + ' tooltip';
  }

  <template>
    <div class={{this.tooltipClass}}>
      <i class={{this.icon}}></i>
      {{@content}}
    </div>
  </template>
}

Now when called like so:

import Tooltip from 'my-app/components/tooltip';

<template>
  <Tooltip @content="I'm a tooltip!"/>
</template>

The result will be:

<div class="tooltip">
  <i class="icon-info"></i>
  I'm a tooltip!
</div>

Note that because arguments are prefixed with @ in templates, and placed on args in the component definition, we can use the same name for our icon and tooltipClass getters, which is pretty convenient. We can also tell clearly when we look at the template for the tooltip that this.tooltipClass and this.icon are values that come from the class definition, and that means they probably have been used in some kind of dynamic code (in this case, our defaulting logic).

Attributes

Attribute Ordering

The positioning of ...attributes matters, with respect to the other attributes in the element it is applied to. Attributes that come before ...attributes can be overridden, but attributes that come after cannot:

<template>
  <p
    data-overridable="you can override me"
    ...attributes
    data-non-overridable="but you can't override me!"
  >
    ...
  </p>
</template>

There is one exception to this, which is the class attribute. class will get merged, since its more often the case that users of the component want to add a class than completely override the existing ones. For class, the order of ...attributes will determine the order of merging. Putting it before:

<template>
  <p ...attributes class="friend-greeting">
    Hello {{@friend}}, I'm {{this.name}}!
  </p>
</template>

Results in:

<p class="red-alert friend-greeting">
  Hello {{@friend}}, I'm {{this.name}}!
</p>

And putting it after:

<template>
  <p class="friend-greeting" ...attributes>
    Hello {{@friend}}, I'm {{this.name}}!
  </p>
</template>

Results in:

<p class="friend-greeting red-alert">
  Hello {{@friend}}, I'm {{this.name}}!
</p>

aria Attributes

There are some aria attributes that can have multiple values and the order of those values matter. The most frequently used of these is aria-describedby and aria-labelledby.

In these cases, make sure to declare all of the relevant values in the correct order.

import MyInput from 'my-app/components/my-input';

<template>
  <MyInput @input-label="Password" aria-describedby="text-help-0 text-help-1" />
</template>

To learn more about aria roles and accessibility in Ember, check out the Accessibility Guide.

Conditional Component Rendering

Sometimes you want to defer the selection of a component to runtime. Suppose you have a blog post model that contains a string postType indicating that the post is either a "root" or a "reply". Below is an example of choosing different components for displaying different kinds of posts. First, define your two components:

app/components/root-post.gjs
<template>
  <h3>Hello from root!</h3>
  <p>{{@post.body}}</p>
</template>
app/components/reply-post.gjs
<template>
  <h3>Hello from reply!</h3>
  <div>{{@post.author}}</div>
</template>

Then, you can choose which to render based on the string data. We can pass the @post into our ShowPostComponent and it can render the correct type for us:

app/components/show-post.gjs
import Component from '@glimmer/component';

import RootPost from  'my-app/components/root-post';
import ReplyPost from 'my-app/components/reply-post';


export default class ShowPostComponent extends Component {
  // returns either RootPost or ReplyPost (default: RootPost)
  get postComponent(postType) {
    return this.args.post?.postType === 'reply' ? ReplyPost : RootPost;
  }

  <template>
    <this.postComponent @author={{@post.author}} @body={{@post.body}} />
  </template>
}

This is great when RootPost and ReplyPost take the same arguments, like author and body in the above example. But what if the components accept different arguments? One way would be to move the selection conditional into the template, like so:

app/components/show-post.gjs
import RootPost from 'my-app/components/root-post';
import ReplyPost from 'my-app/components/reply-post';

const eq = (a, b) => a === b;

<template>
  {{!-- Post is either RootPost or ReplyPost --}}
  {{#if (eq @post.postType "reply")}}
    <ReplyPost
      @author={{post.author}}
      @body={{post.body}}
      @replyTo={{post}}
    />
  {{else}}
    <RootPost
      @author={{post.author}}
      @body={{post.body}}
      @category={{post.topic}}
    />
  {{/if}}
</template>

Contextual Components

The built-in {{component}} helper allows us to partially apply some component arguments.

The first parameter of the helper is a component to render. So {{component BlogPost}} is the same as using <BlogPost />. Any named arguments are passed as arguments to the component, so {{component BlogPost author="Sam"}} is the same as <BlogPost @author="sam" />.

The component helper is often used when yielding components to blocks. For example the layout for a SuperForm component might be implemented as:

app/components/super-form.gjs
import Component from '@glimmer/component';
import { hash } from '@ember/helper';
import SuperInput from 'my-app/components/super-input';
import SuperTextarea from 'my-app/components/super-textarea';
import SuperSubmit from 'my-app/components/super-submit';

export default class SuperFormComponent extends Component {
  get data() {
    return this.args.post;
  }

  <template>
    <form>
      {{yield (hash
        Input=(component SuperInput form=this model=this.data)
        Textarea=(component SuperTextarea form=this model=this.data)
        Submit=(component SuperSubmit form=this model=this.data)
      )}}
    </form>
  </template>
}

And be used as:

app/templates/index.gjs
import SuperForm from 'my-app/components/super-form';

<template>
  <SuperForm @post={{@model}} as |f|>
    <f.Input @name="title" />
    <f.Textarea @name="body" />
    <f.Submit />
  </SuperForm>
</template>

The {{component}} helper is a powerful tool for improving code modularity.

Contextual helpers & modifiers

We can even use helpers and modifiers in the same way. Let's extend the SuperForm component:

app/components/super-form.gjs
import Component from '@glimmer/component';
import { hash } from '@ember/helper';
import SuperInput from 'my-app/components/super-input';
import SuperTextarea from 'my-app/components/super-textarea';
import SuperSubmit from 'my-app/components/super-submit';
import superIsValid from 'my-app/helpers/super-is-valid';
import superErrorFor from 'my-app/helpers/super-error-for';
import superAutoResize from 'my-app/modifiers/super-auto-resize';

export default class SuperFormComponent extends Component {
  get data() {
    return this.args.post;
  }

  <template>
    <form>
      {{yield (hash
        Input=(component SuperInput form=this model=this.data)
        Textarea=(component SuperTextarea form=this model=this.data)
        Submit=(component SuperSubmit form=this model=this.data)

        is-valid=(helper superIsValid form=this model=this.data)
        error-for=(helper superErrorFor form=this model=this.data)

        auto-resize=(modifier superAutoResize)
      )}}
    </form>
  </template>
}

And be used as:

app/templates/index.gjs
import SuperForm from 'my-app/components/super-form';

<template>
  <SuperForm @model={{this.post}} as |f|>

    {{! Invoke a contextual component }}
    <f.Input @name="title" />

    {{! Invoke contextual helpers }}
    {{#unless (f.is-valid "title")}}
      <div class="error">This field {{f.error-for "title"}}</div>
    {{/unless}}

    {{! Invoke a contextual modifier on a contextual component invocation }}
    <f.Textarea @name="body" {{f.auto-resize maxHeight="500"}} />

    <f.Submit />
  </SuperForm>
</template>

These APIs open the doors for the creation of new, more powerful UI abstractions.

left arrow
Autotracking In-Depth
Patterns for Actions
right arrow
On this page

  • Argument Defaults
  • Attributes
  • Attribute Ordering
  • aria Attributes
  • Conditional Component Rendering
  • Contextual Components
  • Contextual helpers & modifiers
Team Sponsors Security Legal Branding Community Guidelines
Twitter GitHub Discord Mastodon

If you want help you can contact us by email, open an issue, or get realtime help by joining the Ember Discord.

© Copyright 2025 - Tilde Inc.
Ember.js is free, open source and always will be.


Ember is generously supported by
blue Created with Sketch.