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.
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.
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:
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, lets
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.
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 { tracked } from '@glimmer/tracking';
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>
<li>Hello, ! You're number in line</li>
</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:
Hello, !
Sorry, nobody is here.
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']
};
}
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:
Hello, ! You are years old.
Sorry, nobody is here.