Filter vs. ngHide With ngRepeat In AngularJS
Out of the box, AngularJS feels magical. You add a few notations here, define a few objects there, and suddenly your page is actually doing something. And you didn't even need to write any jQuery to make it work. AngularJS is awesome; and it is magical; but sometimes, side-stepping the magic, so to speak, can give you more options. Take, for example, collection filtering in ngRepeat. You can easily wire a search field into an ngRepeat and magically your collection collapses and expands as your end-user types. But, you can accomplish the same thing with a little more manual labor, if and when you need more options.
When you filter a collection in AngularJS, you end up working with a subset of your original collection. And, when you do this in an ngRepeat, it means that AngularJS is actually removing Document Object Model (DOM) nodes from your HTML. This initiates a $destroy on the removed $scopes and unbinds all of your watchers and event handlers. Then, as your filter becomes less constrictive, AngularJS rebuilds the DOM nodes, relinks your directives, and re-instantiates all of the relevant Controllers (though, not necessarily in that order).
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title>
Filter vs. ngHide With ngRepeat In AngularJS
</title>
<style type="text/css">
form, p, li, input, button {
font-size: 15px ;
}
li {
border: 1px dotted #FF9900 ;
margin: 0px 0px 7px 0px ;
padding: 5px 5px 5px 5px ;
}
</style>
</head>
<body>
<h1>
Filter vs. ngHide With ngRepeat In AngularJS
</h1>
<p>
You have {{ friends.length }} friends!
</p>
<!-- BEGIN: Search Filter. -->
<form>
Filter:
<input type="text" ng-model="filters.name" size="20" />
<button type="button" ng-click="clearFilter()">
Clear Filter
</button>
</form>
<!-- END: Search Filter. -->
<!-- BEGIN: List Of Friends. -->
<ul>
<li
ng-repeat="friend in friends | filter: filters.name"
bn-line-item>
{{ friend.name }}
</li>
</ul>
<!-- END: List Of Friends. -->
<!-- Load jQuery and AngularJS from the CDN. -->
<script
type="text/javascript"
src="//code.jquery.com/jquery-2.0.0.min.js">
</script>
<script
type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
</script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// Set up the default filters.
$scope.filters = {
name: ""
};
// Set up the default collection of friends.
$scope.friends = [
{
id: 1,
name: "Vin Diesel"
},
{
id: 2,
name: "Helena Bonham Carter"
},
{
id: 3,
name: "Lori Petty"
},
{
id: 4,
name: "Jason Statham"
}
];
// ---
// PUBLIC METHODS.
// ---
// I clear the search filter.
$scope.clearFilter = function() {
$scope.filters.name = "";
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I log the linking of the line item.
app.directive(
"bnLineItem",
function() {
// I bind the UI events to the scope.
function link( $scope, element, attributes ) {
console.log( "Linked:", $scope.friend.name );
}
// Return the directive configuration.
return({
link: link,
restrict: "A"
});
}
);
</script>
</body>
</html>
In many cases, this is totally fine. In fact, it's kind of magical. But, sometimes, you want a little more control. Sometimes, you don't want to $destroy and then relink your directives. Sometimes, you need to differentiate between "first-run" and "reappear" for your ngRepeat items. In such cases, we can implement filtering manually by watching and reacting to changes in the search query.
In the following code, each ngRepeat item sets up a $watch() on the search filter. It then hides and shows itself, using ngHide, as the search value changes:
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title>
Filter vs. ngHide With ngRepeat In AngularJS
</title>
<style type="text/css">
form, p, li, input, button {
font-size: 15px ;
}
li {
border: 1px dotted #FF9900 ;
margin: 0px 0px 7px 0px ;
padding: 5px 5px 5px 5px ;
}
</style>
</head>
<body>
<h1>
Filter vs. ngHide With ngRepeat In AngularJS
</h1>
<p>
You have {{ friends.length }} friends!
</p>
<!-- BEGIN: Search Filter. -->
<form>
Filter:
<input type="text" ng-model="filters.name" size="20" />
<button type="button" ng-click="clearFilter()">
Clear Filter
</button>
</form>
<!-- END: Search Filter. -->
<!-- BEGIN: List Of Friends. -->
<ul>
<li
ng-repeat="friend in friends"
ng-controller="FriendController"
ng-hide="isExcludedByFilter"
bn-line-item>
{{ friend.name }}
</li>
</ul>
<!-- END: List Of Friends. -->
<!-- Load jQuery and AngularJS from the CDN. -->
<script
type="text/javascript"
src="//code.jquery.com/jquery-2.0.0.min.js">
</script>
<script
type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
</script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// Set up the default filters.
$scope.filters = {
name: ""
};
// Set up the default collection of friends.
$scope.friends = [
{
id: 1,
name: "Vin Diesel"
},
{
id: 2,
name: "Helena Bonham Carter"
},
{
id: 3,
name: "Lori Petty"
},
{
id: 4,
name: "Jason Statham"
}
];
// ---
// PUBLIC METHODS.
// ---
// I clear the search filter.
$scope.clearFilter = function() {
$scope.filters.name = "";
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the friend line-item.
app.controller(
"FriendController",
function( $scope ) {
// I determine if the current line item should be
// hidden from view due to the current search filter.
$scope.isExcludedByFilter = applySearchFilter();
// ---
// WATCHERS.
// ---
// Any time the search filter changes, we may have to
// alter the visual exlcusion of our line item.
$scope.$watch(
"filters.name",
function( newName, oldName ) {
if ( newName === oldName ) {
return;
}
applySearchFilter();
}
);
// ---
// PRIVATE METHODS.
// ---
// I apply the current search filter to the friend
// controlled by this line item.
function applySearchFilter() {
var filter = $scope.filters.name.toLowerCase();
var name = $scope.friend.name.toLowerCase();
var isSubstring = ( name.indexOf( filter ) !== -1 );
// If the filter value is not a substring of the
// name, we have to exclude it from view.
$scope.isExcludedByFilter = ! isSubstring;
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I log the linking of the line item.
app.directive(
"bnLineItem",
function() {
// I bind the UI events to the scope.
function link( $scope, element, attributes ) {
console.log( "Linked:", $scope.friend.name );
}
// Return the directive configuration.
return({
link: link,
restrict: "A"
});
}
);
</script>
</body>
</html>
This is clearly more code; we have an additional Controller and we have to actually implement the search-query comparison with our ngRepeat list items. But, if you look at the console logging in the video you will notice that the bnListItem directive is only linked once per item. Even as the items are hidden and then shown, there is no more logging. This is because ngHide doesn't $destroy the scope; it doesn't unbind watchers; it doesn't strip out DOM nodes. It simply hides them.
I am not advocating that you stop using AngularJS' filter functionality. After all, it makes certain problems really easy to solve. I am simply pointing out that it has trade-offs. And, that you can accomplish the same thing using ngHide; which has a different set of trade-offs.
Want to use code from this post? Check out the license.
Reader Comments
In your assessment, is it correct to say that given a list of say 500 items its more performant to use the `ngHide` method over the `filter` method?
@Mike,
Honestly, in the majority of cases, I would say there isn't going to be a difference. Both approaches have trade-offs. If you use the filter, then you have fewer DOM elements and fewer $scope instances. As such, there will be fewer $watch statements and fewer $on statements.
But, on the other hand, if probably have more DOM labor for the re-building things and more linking overhead on directives. And, it's going to depend on what your Directives are doing (and if you have any).
I guess, if you have Directives and the link() and $destroy handlers do a lot of work, then it may be better to keep the DOM nodes around by simply hiding them. But, if you're ngRepeat elements are fairly simple, it won't make a difference.
I had done something similar in the past, without using an extra controller. I used filtering inside ng-show. Like this:
You can see a live demo of it here: http://plnkr.co/edit/ZsROkmXx4oUmXg7HElSa?p=preview
Maybe I'm putting too much logic in the view. But this way, I can reuse angular's filtering and avoid an extra controller.
On a large array, using ng-show instead of filtering ngReapeat's items gave me a much better and smoother performance.
@Keyamoon,
It's good to hear that there is a noticeable difference on performance in large collections. I've really only explored this in small demos / R&D - but I think it makes sense. Thanks for the feedback!
That's pretty neat and efficient. In fact I am struggling with a huge amount of data in one of my ongoing projects, and i thought it is because it is rendering these objects again & again.
But thanks to you i now get where the problem might be. Will definitely try your solution.
@Ravi,
Good luck! After doing some profiling in Chrome dev-tools, I was able to cut the rendering time of a very complex set up nested-ngRepeats by removing all Directive-linking that forced repaints and by moving my ngRepeat filtering to an internal $watch() statement (as opposed to a filter in the ngRepeat statement).
At first, these things cause NO problems at all. But once your dataset size reaches a critical mass, things can quickly slow down.
Great tutorial! This helped me to create an app that allows individual list items to be deleted within a filtered view. Is it possible to combine other filters with this method? For instance, I want to be able to filter by your method as well as by category of the list items.
@Clark,
You can definitely combine different filter methods. Right now, this works by watching the filter value and adjusting the visibility of the Node. However, you could watch more than one filter value; or, you could even have your parent-controller $broadcast() events that indicate filter changes; and, your item-controllers could listen for those events.
Also, you could do a combination of things. For example, you could actually limit the collection in the ngRepeat based on the Category (meaning you actually change the structure of the collection when the category changes). And then, you could show/hide the items based on the search.
I like this filter example using the $watch instead of the filter itself. But an issue I'm having is when you do have nested ng-repeats and you want to hide the parent repeater when there are 0 items available in the child repeater. I actually used a similar solution to what you have posted before to build the nested repeaters. I'm just having an issue with the logic for the hiding of the parent. I was trying to devise a way to use $emit on the child repeater items to tell when there are no more items to hide the parent.. Was trying to stay away from $parent also. Any thoughts?
In one of our apps I implemented a list using the ng-hide method a while back. (I did find it was noticeably faster than the inline filtering in the ng-repeat for our situation.)
Now I need to write a utility function that allows me to download a list of the items that are visible...
If I had used the inline filtering, it would be relatively simple (by using something like ng-repeat="result in ( filteredResults = ( resultList | filter:query .... ) )" ); I could simply access the $scope.filteredResults to get the list of visible items.
By using the ng-hide option though, I can't do this. The visibility is determined by the individual item's controller, and is governed by a $scope variable on that individual item's scope. So now I have to jump through a few more hoops to be able to determine the list of visible items. (Thinking I may just need to iterate over the parent scope's child scopes to get the items to build the list...)
Since this is a definite trade-off of using the ng-hide method, I figured I'd mention that here for the benefit of future readers.
Hey this was exactly what I was looking for -- complete with video, code and two ways to solve the problem :)
I use the ng-hide filter method, and its really, really! massive faster! Use it!
@Keyamoon,
Very good contribution, I worked well
@Keyamoon,
Such an easy and brilliant approach.
I only have a few hundred records being displayed but have to pull images from a remote server for each one. I use imgcache to reduce user bandwidth consumption but it adds a second to the render.
By hiding the li's instead of filtering, the images aren't destroyed and recreated (and rerendered with a second delay) therefore my filter (a.k.a hide) is instant!
Thanks!!!