Ember Octane is here! A lot has changed since Ember 3.14, including these Guides. Read more in the Ember Blog.

Edit Page

Looping Through Lists


Oftentimes we'll need to repeat a component multiple times in a row, with different data for each usage of the component. We can use the {{#each}} helper to loop through lists of items like this, repeating a section of template for each item in the list.

For instance, in a messaging app, we could have a <Message> component that we repeat for each message that the users have sent to each other.

<div class="messages">
  <Message
    @username="Tomster"
    @userIsActive={{true}}
    @userLocalTime="4:56pm"
  >
    <p>
      Hey Zoey, have you had a chance to look at the EmberConf
      brainstorming doc I sent you?
    </p>
  </Message>
  <Message
    @username="Zoey"
    @userIsActive={{true}}
  >
    <p>Hey!</p>

    <p>
      I love the ideas! I'm really excited about where this year's
      EmberConf is going, I'm sure it's going to be the best one yet.
      Some quick notes:
    </p>

    <ul>
      <li>
        Definitely agree that we should double the coffee budget this
        year (it really is impressive how much we go through!)
      </li>
      <li>
        A blimp would definitely make the venue very easy to find, but
        I think it might be a bit out of our budget. Maybe we could
        rent some spotlights instead?
      </li>
      <li>
        We absolutely will need more hamster wheels, last year's line
        was <em>way</em> too long. Will get on that now before rental
        season hits its peak.
      </li>
    </ul>

    <p>Let me know when you've nailed down the dates!</p>
  </Message>

  <NewMessageInput />
</div>

First, we would add a component class and extract the parts of each <Message> component that are different into an array on that class. We would extract the username, active value, local time, and the yielded content for each message. For the yielded content, since it's plain HTML, we can extract it as a string.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class MessagesComponent extends Component {
  messages = [
    {
      username: 'Tomster',
      active: true,
      localTime: '4:56pm',
      content: `
        <p>
          Hey Zoey, have you had a chance to look at the EmberConf
          brainstorming doc I sent you?
        </p>
      `
    },
    {
      username: 'Zoey',
      active: true,
      content: `
        <p>Hey!</p>

        <p>
          I love the ideas! I'm really excited about where this year's
          EmberConf is going, I'm sure it's going to be the best one yet.
          Some quick notes:
        </p>

        <ul>
          <li>
            Definitely agree that we should double the coffee budget this
            year (it really is impressive how much we go through!)
          </li>
          <li>
            A blimp would definitely make the venue very easy to find, but
            I think it might be a bit out of our budget. Maybe we could
            rent some spotlights instead?
          </li>
          <li>
            We absolutely will need more hamster wheels, last year's line
            was <em>way</em> too long. Will get on that now before rental
            season hits its peak.
          </li>
        </ul>

        <p>Let me know when you've nailed down the dates!</p>
      `
    }
  ];
}

Then, we can add an {{each}} helper to the template by passing this.messages to it. {{each}} will receive each message as its first block param, and we can use that item in the template block for the loop.

<div class="messages">
  {{#each this.messages as |message|}}
    <Message
      @username={{message.username}}
      @userIsActive={{message.active}}
      @userLocaltime={{message.localTime}}
    >
      {{{message.content}}}
    </Message>
  {{/each}}
  <Message
    @username="Tomster"
    @userIsActive={{true}}
    @userLocalTime="4:56pm"
  >
    <p>
      Hey Zoey, have you had a chance to look at the EmberConf
      brainstorming doc I sent you?
    </p>
  </Message>
  <Message
    @username="Zoey"
    @userIsActive={{true}}
  >
    <p>Hey!</p>

    <p>
      I love the ideas! I'm really excited about where this year's
      EmberConf is going, I'm sure it's going to be the best one yet.
      Some quick notes:
    </p>

    <ul>
      <li>
        Definitely agree that we should double the coffee budget this
        year (it really is impressive how much we go through!)
      </li>
      <li>
        A blimp would definitely make the venue very easy to find, but
        I think it might be a bit out of our budget. Maybe we could
        rent some spotlights instead?
      </li>
      <li>
        We absolutely will need more hamster wheels, last year's line
        was <em>way</em> too long. Will get on that now before rental
        season hits its peak.
      </li>
    </ul>

    <p>Let me know when you've nailed down the dates!</p>
  </Message>

  <NewMessageInput />
</div>

Notice that we used triple curly brackets around {{{message.content}}}. This is how Ember knows to insert the content directly as HTML, rather than directly as a string.

Zoey says...
Using triple curly brackets is just one way to put dynamic HTML into Ember templates. We can also use the htmlSafe function to wrap template strings. Additionally, inserting unknown HTML into an app can always produce unexpected results, so we should be careful and always make sure that the HTML is safe (as in, won't cause major issues) before inserting it.

Updating Lists

Next, let's add a way for the user to send a new message. First, we need to add an action for creating the new message. We'll add this to the <NewMessageInput /> component:

<form>
<form {{on "submit" this.createMessage}}>
  <input>
  <Input @value={{this.message}}>
  <button type="submit">
    Send
  </button>
</form>

We're using the submit event on the form itself here rather than adding a click event handler to the button since it is about submitting the form as a whole. We also updated the input tag to instead use the built in <Input> component, which automatically updates the value we pass to @value. Next, let's add the component class:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class NewMessageInputComponent extends Component {
  @tracked message;

  @action
  createMessage(event) {
    event.preventDefault();

    if (this.message && this.args.onCreate) {
      this.args.onCreate(this.message);

      // reset the message input
      this.message = '';
    }
  }
}

This action uses the onCreate argument to expose a public API for defining what happens when a message is created. This way, the <NewMessageInput> component doesn't have to worry about the external details - it can focus on getting the new message input.

Next, we'll update the parent component to use this new argument.

<div class="messages">
  {{#each this.messages as |message|}}
    <Message
      @username={{message.username}}
      @userIsActive={{message.active}}
      @userLocaltime={{message.localTime}}
    >
      {{{message.content}}}
    </Message>
  {{/each}}

  <NewMessageInput />
  <NewMessageInput @onCreate={{this.addMessage}} />
</div>

And in the component class, we'll add the addMessage action. This action will create the new message from the text that the <NewMessageInput> component gives us, and push it into the messages array. In order for the messages array to react to that change, we'll also need to convert it into an EmberArray. EmberArray provides special methods that tell Ember when changes occur to the array itself.

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { A } from '@ember/array';

export default class MessagesComponent extends Component {
  username = 'Zoey';

  @action
  addMessage(messageText) {
    this.messages.pushObject({
      username: this.username,
      active: true,
      content: `<p>${messageText}</p>`
    });
  }

  messages = A([
    {
      username: 'Tomster',
      active: true,
      localTime: '4:56pm',
      content: `
        <p>
          Hey Zoey, have you had a chance to look at the EmberConf
          brainstorming doc I sent you?
        </p>
      `
    },
    {
      username: 'Zoey',
      active: true,
      content: `
        <p>Hey!</p>

        <p>
          I love the ideas! I'm really excited about where this year's
          EmberConf is going, I'm sure it's going to be the best one yet.
          Some quick notes:
        </p>

        <ul>
          <li>
            Definitely agree that we should double the coffee budget this
            year (it really is impressive how much we go through!)
          </li>
          <li>
            A blimp would definitely make the venue very easy to find, but
            I think it might be a bit out of our budget. Maybe we could
            rent some spotlights instead?
          </li>
          <li>
            We absolutely will need more hamster wheels, last year's line
            was <em>way</em> too long. Will get on that now before rental
            season hits its peak.
          </li>
        </ul>

        <p>Let me know when you've nailed down the dates!</p>
      `
    }
  ]);
}

Now, whenever we type a value and submit it in the form, a new message object will be added to the array, and the {{each}} will update with the new item.

Item Indexes

The index of each item in the array is provided as a second block param. This can be useful at times if you need the index, for instance if you needed to print positions in a queue

import Component from '@glimmer/component';

export default class SomeComponent extends Component {
  queue = [
    { name: 'Yehuda' },
    { name: 'Jen' },
    { name: 'Rob' }
  ];
}
<ul>
  {{#each this.queue as |person index|}}
    <li>Hello, {{person.name}}! You're number {{index}} in line</li>
  {{/each}}
</ul>

Empty Lists

The {{#each}} helper can also have a corresponding {{else}}. The contents of this block will render if the array passed to {{#each}} is empty:

{{#each this.people as |person|}}
  Hello, {{person.name}}!
{{else}}
  Sorry, nobody is here.
{{/each}}

Looping Through Objects

There are also times when we need to loop through the keys and values of an object rather than an array, similar to JavaScript's for...in loop. We can use the {{#each-in}} helper to do this:

import Component from '@glimmer/component';

export default class StoreCategoriesComponent extends Component {
  // Set the "categories" property to a JavaScript object
  // with the category name as the key and the value a list
  // of products.
  categories = {
    'Bourbons': ['Bulleit', 'Four Roses', 'Woodford Reserve'],
    'Ryes': ['WhistlePig', 'High West']
  };
});
<ul>
  {{#each-in this.categories as |category products|}}
    <li>{{category}}
      <ol>
        {{#each products as |product|}}
          <li>{{product}}</li>
        {{/each}}
      </ol>
    </li>
  {{/each-in}}
</ul>

The template inside of the {{#each-in}} block is repeated once for each key in the passed object. The first block parameter (category in the above example) is the key for this iteration, while the second block parameter (products) is the actual value of that key.

The above example will print a list like this:

<ul>
  <li>Bourbons
    <ol>
      <li>Bulleit</li>
      <li>Four Roses</li>
      <li>Woodford Reserve</li>
    </ol>
  </li>
  <li>Ryes
    <ol>
      <li>WhistlePig</li>
      <li>High West</li>
    </ol>
  </li>
</ul>

Ordering

An object's keys will be listed in the same order as the array returned from calling Object.keys on that object. If you want a different sort order, you should use Object.keys to get an array, sort that array with the built-in JavaScript tools, and use the {{#each}} helper instead.

Empty Lists

The {{#each-in}} helper can have a matching {{else}}. The contents of this block will render if the object is empty, null, or undefined:

{{#each-in this.people as |name person|}}
  Hello, {{name}}! You are {{person.age}} years old.
{{else}}
  Sorry, nobody is here.
{{/each-in}}