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.
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}}
© 2020 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.25.0/components/looping-through-lists