A common piece of functionality that I typically include in my web applications is the ability for users to quickly filter the data displayed on the page. A simple text box is often used to provide the filtering.
It’s pretty easy to bind a text box to a property on the controller and filter the results by the value in the text box. But this also means that the filter is performed after every keystroke the user enters. For small datasets this is fine, but for large datasets it can cause the UI to start locking up. So it’s better to wait until the user has finished entering the filter text before actually performing the filter.
I’m going to share three ways to perform filtering in Ember, starting with applying the filter immediately after each keystroke, and then go on to show how to apply the filter when the user clicks a button, and finally how to apply the filter after the user has stopped entering text in the filter text box.
Filter Immediately
Applying the filter immediately is straightforward in Ember. You just need a text box that’s bound to a property on the controller. And the controller will also have a computed property that returns the filtered results. This computed property will be re-calculated anytime the filter text changes.
Here’s a basic example of how that looks:
App.IndexController = Ember.Controller.extend({
filterText: '',
filteredResults: function() {
var filter = this.get('filterText');
return this.get('model').filter(function(item) {
return item.toLowerCase().indexOf(filter) !== -1;
});
}.property('filterText')
});
<script type='text/x-handlebars' data-template-name='index'>
{{input value=filterText type='text' placeholder='filter'}}
<ul>
{{#each item in filteredResults}}
<li>{{item}}</li>
{{/each}}
</ul>
</script>
Filter on Demand
The easiest way to mitigate the filter being applied after every keystroke is to wait until the user clicks a button to apply the filter. In this scenario the filterText
property on the controller will still be bound to the text box and will be updated with every keystroke. That means we can no longer apply the filter when the filterText
property changes.
We’ll need to create an additional property called filter
that keeps track of the current filter value. And the filteredResults
computed property will now be dependent upon filter
instead of filterText
.
And the last change we’ll need to make is to add the applyFilter
action to the controller. When this is called after the user clicks the button, it will copy the value from filterText
to filter
and cause the filteredResults
to be re-computed.
App.IndexController = Ember.Controller.extend({
filter: '',
filterText: '',
filteredResults: function() {
var filter = this.get('filter');
return this.get('model').filter(function(item) {
return item.toLowerCase().indexOf(filter) !== -1;
});
}.property('filter'),
actions: {
applyFilter: function() {
this.set('filter', this.get('filterText'));
}
}
});
<script type="text/x-handlebars" data-template-name="index">
{{input value=filterText type='text' placeholder='filter'}}
<button {{action 'applyFilter'}}>Apply Filter</button>
<ul>
{{#each item in filteredResults}}
<li>{{item}}</li>
{{/each}}
</ul>
</script>
This does the job, but it doesn’t provide the best experience for the user because they have to click the filter button each time. What would be better is to wait until the user is done typing in the filter text box and then automatically apply the filter.
Filter on Pause
To wait to perform the filter until the user has finished entering the filter value, we’ll use the Ember.run.debounce
function.
In case you don’t have much experience with a debounce function, let me explain how it works.
When you call debounce, you tell it what function you want called and how long it should wait to call the function. For example, you can tell debounce to call a function named updateQuotes in 5 seconds. And debounce will set a timer for 5 seconds and call updateQuotes when the timer has elapsed.
However, if debounce is called again before the timer completes, then the timer is reset and starts counting down again. And the function will not be called as long as debounce is called again before the timer completes.
We’ll use debounce’s functionality to automatically apply the filter after the user finishes entering the value they want to filter by.
Here is how that looks:
App.IndexController = Ember.Controller.extend({
filter: '',
filterText: '',
onFilterTextChange: function() {
// wait 1 second before applying the filter
Ember.run.debounce(this, this.applyFilter, 1000);
}.observes('filterText'),
applyFilter: function() {
this.set('filter', this.get('filterText'));
},
filteredResults: function() {
var filter = this.get('filter');
return this.get('model').filter(function(item) {
return item.toLowerCase().indexOf(filter) !== -1;
});
}.property('filter')