The Goal
Today, I wanted to play with the GitHub users page in the Aurelia Skeleton Navigation project. I wanted to accomplish something simple: allow the user to select a subset of the users returned by GitHub and display avatar cards only for this subset.
The Plan
My original plan for doing this was simple enough. I would use a repeater to create a list of checkboxes bound to a selected
property on the user. I would then create a computed property selectedUsers
. selectedUsers
would simply use Lodash to filter the users
array and return the results as an array.
Why the Plan Failed
This is a naive plan. Computed properties are dirty checked in Aurelia. I am also creating a new array every time selectedUsers
is dirty checked. That's bad©. The other problem is that this plan didn't work as expected. When I would select a user, the repeater for avatar cards would repeadly show that user (or the selected users). This would keep going until I deselected all of the users. I'm not sure why this was happening, but I have asked those who know about these sort of things (other Aurelia Core Team members) and will update this post when they explain this to me. Update: it's a bug in the repeater
Plan B, The Right Way
So how should I handle this? I need to create a function on the parent view-model. This function will be called whenever the checked state of a checkbox changes. The function will take the user represented by that checkbox as a parameter and check the selected
property for the user. Based on this value, the function will either add or remove this user from an array property on the view-model. No more dirty checking, no more Lodash!
Let's have a look at the final code:
users.html
<template>
<require from="blur-image"></require>
<section class="au-animate">
<h2>${heading}</h2>
<div class="row">
<div class="col-md-4">
<div class="form-group" repeat.for="user of users">
<label>
${user.login}
<input type="checkbox" checked.bind="user.selected" change.delegate="$parent.userSelected(user)" />
</label>
</div>
</div>
<div class="col-md-6">
<div class="col-sm-6 col-md-3 card-container au-animate" repeat.for="user of selectedUsers">
<div class="card">
<canvas class="header-bg" width="250" height="70" blur-image.bind="image"></canvas>
<div class="avatar">
<img src.bind="user.avatar_url" crossorigin ref="image"/>
</div>
<div class="content">
<p class="name">${user.login}</p>
<p><a target="_blank" class="btn btn-default" href.bind="user.html_url">Contact</a></p>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
As you can see, I use change.delegate="$parent.userSelected(user)"
as the lynchpin of this exercise. change.delegate
tells Aurelia to call the userSelected
method of the parent view-model and pass the user
object (from the repeater) when the change
event fires. Using delegate
tells Aurelia to utilize event delegation for this. This means that new event handlers aren't added to each input
element. The event is allowed to bubble to a single event handler created by Aurelia and is thus more performant.
users.js
...
userSelected(user) {
if(user.selected === true) {
this.selectedUsers.push(user);
}
else {
let index = this.selectedUsers.indexOf(user);
if( index > -1) {
this.selectedUsers.splice(index, 1);
}
}
}
...
In the userSelected
function, I simply check if the selected
property is true and then either add or remove this user from the selectedUsers
array. Aurelia will handle updating the UI as users are added and removed from the selectedUsers
property.
Conclusion
With a bit of simple error handling and some smart usage of Aurelia's templating and binding engine, I was able to create a UI to filter an array of GitHub users based on user interaction. Hopefully this sample will help you as you write awesome applications using Aurelia!