Iterative Sections
Lists of data, of some form or another, are often at the heart of webapps. In this tutorial we're going to build a sortable table of superheroes, using data taken from superherodb.com.
Step 1
We've already got an array of objects representing four of the X-Men, over there on the right. We just need to update the template.
Begin by wrapping the second <tr>
in an #each
section:
{{#each superheroes}}
<tr>
<!-- row content -->
</tr>
{{/each}}
As with
#with
and#if
sections, you could just do{{#superheroes}}...{{/superheroes}}
and it would work the same way, as long as superheroes is an array.
Then, insert mustaches representing each of the three properties in the table – name
, realname
and power
. For extra credit, wrap the name in a link pointing to the info URL.
Execute the code.
// You can use array notation to update the data:
ractive.set( 'superheroes[1].power', 'Martial arts' );
// Or, you can use dot notation. Whichever you prefer:
ractive.set( 'superheroes.3.power', 'Enhanced senses' );
What if there weren't any items in the array? Displaying a table with no rows has been recognized by the International Web Decorum Foundation as impolite, so Ractive.js will allow you to provide alternate content using an
{{else}}
section in the#each
block, which will be rendered if the array is empty.{{#each superheroes}} <tr>...</tr> {{else}} <tr><td colspan="4">Oh no! There are no superheroes!</td></tr> {{/each}}
This also works with plain mustache sections.
Step 2
Often when working with lists, we want to know the index of the list item we're currently rendering.
Mustache doesn't have a good way of doing this, so Ractive.js introduces the index reference:
{{#each list: num}}
<!-- inside here, {{num}} is the index -->
{{/each}}
If you don't want to name your index, you can also use the generic index special reference
@index
.@index
will resolve to the index of the nearest iteration, so if you happen to have nested iterations, it will be the nearest parent iteration, not the root.If your section happens to be iterating an object rather than an array, you can use the
@key
special reference to get the object key of the current iteration.
By declaring num
to be an index reference, we can use it the same way we'd use any other variable. Let's add a number column to our table – first add the column to the header row:
<tr>
<th>#</th>
<th>Superhero name</th>
<!-- etc -->
</tr>
Then to the list row:
{{#each superheroes: num}}
<tr>
<td>{{num}}</td>
<td><a href='{{info}}'>{{name}}</a></td>
<td>{{realname}}</td>
<td>{{power}}</td>
</tr>
{{/each}}
Execute the code.
Not bad, but it would look better if the numbers started at 1 rather than 0. Use an expression to increment each row number by 1.
Step 3
Let's say you wanted to add an item to your list. You could use ractive.set()
the way you're used to, but you'd have to find the length of the existing array first:
var index = ractive.get( 'superheroes' ).length;
ractive.set( 'superheroes[' + index + ']', newSuperhero );
That's not ideal. We could use ractive.update('superheroes')
instead, which will make sure that the table is up to date:
xmen[ xmen.length ] = newSuperhero;
ractive.update( 'superheroes' );
If you don't pass a keypath argument to
ractive.update()
, Ractive.js will update everything that has changed since the last set or update.
But there's a more convenient way. Ractive.js provides mutator methods for arrays (push, pop, shift, unshift, splice, sort and reverse) that work with a keypath:
ractive.push( 'superheroes', newSuperhero );
Try adding Storm to the list by pushing to the array in the Script pane:
var newSuperhero = {
name: 'Storm',
realname: 'Monroe, Ororo',
power: 'Controlling the weather',
info: 'http://www.superherodb.com/Storm/10-135/'
};
// add the code here...
Step 4
It's time to make our table sortable. We've added a 'sortable' class to the three headers to indicate they can be clicked on.
First, let's add an event listener to each column header, calling the instance sort
method with the column header as an argument:
<th class='sortable' on-click='@.sort("name")'>Superhero name</th>
<th class='sortable' on-click='@.sort("realname")'>Real name</th>
<th class='sortable' on-click='@.sort("power")'>Superpower</th>
That way, when the user clicks one of the column headers, the view will fire call the sort
method.
ractive.sort = function ( column ) {
alert( 'Sorting by ' + column );
});
You can add methods and properties directly to a Ractive.js instance by simply including them in the init options. Any keys that don't match know init options are added to the instance upon creation.
Execute the code. When you click on the three sortable headers, the browser should alert the name of the column we're sorting by. Now we just need to add the sorting logic.
Step 5
So we've wired up our event handler, and it's behaving as it should. The next step is to add some logic that actually sorts the table. For bonus points, we'll add a 'sorted' class to the header of the sorted column.
There's a nice easy way to ensure that the table remains sorted, even when we add more data: an expression. That's right, you can use expressions with sections.
Update the template:
{{#each sort( superheroes ) : num}}
<tr>
<!-- row contents -->
</tr>
{{/each}}
Now we need to add the sort
function. Here's one (if you're not sure why this works, here's an MDN page that will help explain):
function ( array ) {
// grab the current sort column
var column = this.get( 'sortColumn' );
// clone the array so as not to modify the underlying data
var arr = array.slice();
return arr.sort( function ( a, b ) {
return a[ sortColumn ] < b[ sortColumn ] ? -1 : 1;
});
}
Wiring it up is easy:
ractive.sort = function ( column ) {
this.set( 'sortColumn', column );
});
Try executing this code and clicking different headers to sort the table. (You could specify an initial sort column by adding e.g. sortColumn: 'name'
to data
.)
The last job is to add a sorted class to the header of the currently sorted column. There are several ways we could do this – you could use a bit of jQuery inside the sort proxy event handler, for example. But for this demonstration we'll put the logic in the template, using the conditional operator:
<th class='sortable' class-sorted="sortColumn === 'name'" on-click='@.sort("name")'>
Superhero name
</th>
The
class-
directive is similar to thestyle-
directive - it gives you direct control over the presence of a single class. Theclass-
directive exists in an expression context, so mustaches are not required. If the expression passed to the directive is truthy, Ractive.js will add the class to the element, and if it's false-y, it will remove it.You could also add an additional expression within the existing
class
attribute using a ternary e.g.class="sortable {{ sortColumn === 'name' ? 'sorted' : '' }}
.
Do this for each of the headers, then execute the code. Congratulations! You've built a sortable table in just a few steps. Now comes the fun part – add Storm back to the table. The table will maintain its sort order.
ractive.push( 'superheroes', {
name: 'Storm',
realname: 'Monroe, Ororo',
power: 'Controlling the weather',
info: 'http://www.superherodb.com/Storm/10-135/'
});