The Backstory
We are going to take a break from the RESTful API series and tackle an interesting challenge that my partner in crime Shane Mielke asked me to help him solve the other day. We needed to filter a list of items depending on whether it was created before or after a predetermined date in the past i.e. three months ago. After stumbling on an amazing article by Todd Motto on creating custom filters and adding in some Moment.js… a solution was born! Mwahahahahahah! #maniacalLaughter
We are not going to go over every single facet of the code as most of it is quite rudimentary; but I will hit the highlights and everything else should be quite easy to piece together from the plunk below.
Demo CodeDemo
The Date Ranges
Before we get into the filter, we are going to create a data structure with the date ranges to populate our select component. I tend to prefer using value or constant services when dealing with data structures that are fairly static, so we will create a value service called DateRanges that contains an array of option objects. It would be nice if the ranges were dynamic so we did not have to update them every month with new dates. Enter Moment.js, stage left. We are going to create relative dates by subtracting time from the current moment using moment().subtract(). This subtract method takes two arguments; a number to subtract and the unit to subtract, such as ‘month’ or ‘year’.
.value('DateRanges', [ {name:'All items', date:moment().subtract(10, 'year')}, {name:'Newer than 3 months', date:moment().subtract(3, 'month')}, {name:'Newer than 6 months', date:moment().subtract(6, 'month')}, {name:'Newer than 12 months', date:moment().subtract(1, 'year')} ])
We are setting an appropriately long range to include everything and then setting ranges for 3 months, 6 months and a year.
We will assign DateRanges to main.dateRanges and then use ng-options to display the ranges in a select control. We will keep track of the selected range object by tracking it as main.dateAfter with ng-model=”main.dateAfter”.
<body ng-controller="MainCtrl as main"> <select class="form-control" ng-model="main.dateAfter" ng-options="range.name for range in main.dateRanges"></select> </body>
The Filter
Now that we have our date ranges in the page it is time to create a filter to operate on our items collection and filter out the dates that happened before the selected date range. Creating a filter is very much like creating a service, controller, directive, etc in AngularJS. We are going to call the filter method on module and pass in two arguments; the name of the filter and function defining its behavior. Our filter is going to return a function that accepts the collection we want to manipulate and an optional secondary argument that we can use to apply logic in our filter. In our case, we are passing in the items array and a dateAfter argument which we will use to filter our collection.
.filter('isAfter', function() { return function(items, dateAfter) { // Pending } })
And from here we are going to literally filter the array and return a subset of that array. This can be accomplished via a for loop, a third party library such as Lo-Dash or in our case, using the ES6 filter method.
.filter('isAfter', function() { return function(items, dateAfter) { // Using ES6 filter method return items.filter(function(item){ return moment(item.date).isAfter(dateAfter); }) } })
And Moment.js returns for an encore! This entire challenge comes down to the next line… we are able to determine if item.date is before or after dateAfter by calling moment(item.date).isAfter(dateAfter). If the result is true then item.date gets added to the array that gets returned by the filter.
The one remaining loose end is how to pass the selected date range into the filter to know what to filter against. With AngularJS, we can pass a parameter into a filter by placing it after a colon when we declare our filter. In this case, we want to filter based on the date property of the currently selected main.dateAfter object and so we declare it like this isAfter:main.dateAfter.date
<body ng-controller="MainCtrl as main"> <select class="form-control" ng-model="main.dateAfter" ng-options="range.name for range in main.dateRanges"></select> <hr /> <div class="thumb-wrapper"> <div class="thumb my-repeat-animation" ng-repeat="item in main.items | isAfter:main.dateAfter.date track by item.id"> <img ng-src="{{item.img}}"> {{item.date}} </div> </div> </body>
We are going to add in some animations in just a moment so it is highly recommended to use track by with ng-repeat to improve performance.
The Animations
And just for fun, we are going to add in some animations. I have attached a class called my-repeat-animation in our view and then defined the animations themselves in animations.css.
Define the Base Transition
We define the base transition for both the ng-enter and ng-leave event to last a 0.5 second with linear easing across all properties.
.my-repeat-animation.ng-enter, .my-repeat-animation.ng-leave { -webkit-transition: 0.5s linear all; transition: 0.5s linear all; position:relative; }
Define the Starting and Active Styles
We will set the starting style for ng-enter to be opacity:0 when the event is triggered and opacity:1 when ng-enter has been actively applied aka ng-enter-active. We will do the reverse for ng-leave so that it fades out when an item is leaving ng-repeat.
.my-repeat-animation.ng-enter { opacity:0; } .my-repeat-animation.ng-enter.ng-enter-active { opacity:1; } .my-repeat-animation.ng-leave { opacity:1; } .my-repeat-animation.ng-leave.ng-leave-active { opacity:0; }
And we can stagger the animations when an item is entering the ng-repeat control by defining an ng-enter-stagger style. In our case, we want to delay the transition by 0.1 seconds.
.my-repeat-animation.ng-enter-stagger { /* 100ms will be applied between each sucessive enter operation */ -webkit-transition-delay:0.1s; transition-delay:0.1s; /* this is here to avoid accidental CSS inheritance */ -webkit-transition-duration:0; transition-duration:0; }
For Fun: Permanent Marker Font
One more quick and easy tidbit. I wanted to make the handwriting at the bottom of the pictures look like they were handwritten with a permanent marker; this was super easy using Google Fonts.
I simply had to import the font.
@import url(http://fonts.googleapis.com/css?family=Permanent+Marker); [/cc] And add it to my style. [cc lang="css"] .thumb p { font-family: 'Permanent Marker', cursive; margin-top: 5px; margin-bottom: 0px; }
Bam! Done!
Review
Let us do a quick review of what we covered in this lesson.
- Moment.js provides some amazing and powerful time and date functionality such as substract() and isBefore().
- Custom filters are a really powerful way to apply logic to a collection that is being rendered via ng-repeat.
- Animations are easy in AngularJS and a really great way to put that extra something in your design.
- I heart Google Fonts.
Resources
A huge shout out to Todd Motto, Matias Niemela, and Shane Mielke for doing awesome work that makes me want to be awesome. Hit me up in the comments if you have any questions or ideas of how to extend this example. Thanks!
Everything about custom filters in AngularJS
Staggering Animations in AngularJS
Remastered Animation in AngularJS 1.2
Demo Code
This is nice! I wish this came out a few days ago because I had a similar problem to solve on my own project. I also did it with Moment and Angular, but without the animations. Good stuff.
Thanks! Glad to see we arrived at the same conclusion at least… 😀
Hi Lukas. Nice post!
You have there a typo. Array.prototype.filter is actually an ES5 method not ES6.
Btw looking forward to your AngularJS Application Architecture series on egghead.io 🙂
thanks
Thanks! Appreciate that! 😀