Upgrade AngularJS Sorting Filters to Angular
Publikováno: 14.5.2018
In the early days of AngularJS, one of the most celebrated features was the ability to filter and sort data on the page using only template variables and filters. The magic of two-way data binding ...
In the early days of AngularJS, one of the most celebrated features was the ability to filter and sort data on the page using only template variables and filters. The magic of two-way data binding won over many converts to AngularJS and helped it spread like wildfire.
Today, though, the front end world prefers one-way data binding, and those orderBy
and filter
filters have ridden off into the sunset with the advent of Angular. (Note: throughout this article I'll be using "AngularJS" to refer to 1.x and just "Angular" to refer to 2+.)
But how are we supposed to achieve that same effect? The answer lies in our components, so let's look at an ngUpgrade project and learn how to do this!
Our Starting Point
We're going to step through updating the template of a freshly rewritten component. Then, we'll add sorting and filtering to restore all of the functionality it had in AngularJS. This is a key skill to develop for the ngUpgrade process.
To get started, take a moment to clone the sample project for my course Upgrading AngularJS (don't forget to run npm install
in both the public
and server
folders). Check out this commit for our starting point:
git checkout 9daf9ab1e21dc5b20d15330e202f158b4c065bc3
This sample project is an ngUpgrade hybrid project that uses both AngularJS 1.6 and Angular 4. It's got a working Express API and a Webpack builds for both development and production. Feel free to explore, fork it, and use the patterns in your own projects. If you'd like to look at a version of this project that uses Angular 5, check out this repo. For the purposes of this tutorial, the differences between the two versions won't matter (I'll point out anything minor).
(And, if you're confused on how to set up an ngUpgrade project with working builds, head over to Upgrading AngularJS. I've got detailed, step-by-step videos in the course to help you.)
Replace the AngularJS Syntax
At this stage in our application, our orders component is rewritten in Angular, with all of its dependencies injected and resolved. If we were to try to run our application, though, we'd see errors in the console indicating problems with our template. That's what we need to fix first. We're going to replace the AngularJS syntax in the orders template (orders/orders.html
) so we can get the route loading and the orders displayed on the page. We'll fix the filtering and sorting next.
The first thing we need to do is get rid of all of the instances of $ctrl
in this template. They're no longer necessary in Angular. We can just do a find and replace to find for $ctrl.
(note the dot), and replace it with nothing.
Now let's replace the data-ng-click
in our button on line 13. In Angular, instead of ng-click
, we just use the click
event, with parentheses to indicate that it's an event. Brackets indicate an input, and parentheses indicate an output or an event.
<button type="button" (click)="goToCreateOrder()" class="btn btn-info">Create Order</button>
We're just saying here that on the click event, fire off the goToCreateOrder
function on our orders component.
Before we keep going, let's take a minute to prove that our component is actually loading. Comment out the whole div
that loads our orders (from line 17 on). To run the application, open a terminal and run the following commands:
cd server
npm start
That will start the Express server. To run the Webpack dev server, open another terminal and run:
cd public
npm run dev
(You can keep these processes running for the remainder of this tutorial.)
You should see that our application is loading again. If you go to the orders route, you'll see that the orders component is displaying correctly.
We can also click the Create Order button and it will send us correctly over to our Create Order route and form.
Okay, let's get back to the HTML. Un-comment that div
(our app will be broken again).
Let's replace all of the rest of the instances data-ng-click
with the (click)
event handler. You can either use Find & Replace or just use your editor's shortcut for selecting all occurrences (in VS Code for Windows, this is Ctrl+Shift+L).
Next, replace all of the occurrences of data-ng-show
with *ngIf
. There's actually no direct equivalent to ng-show
in Angular, but that's okay. It's preferable to use *ngIf
, because that way you're actually adding and removing elements from the DOM instead of just hiding and showing them. So, all we need to do is find our data-ng-show
s and replace with *ngIf
.
Finally, we need to do two things to fix our table body. First, replace data-ng-repeat
with *ngFor="let order of orders"
. Note that we're also removing the orderBy
and filter
filters in that line so that the entire tr
looks like this:
<tr *ngFor="let order of orders">
Second, we can delete the data-ng
prefix before the href
link to the order detail route. AngularJS is still handling the routing here, but we don't need to use that prefix anymore since this is now an Angular template.
If we look at the application again, you can see that the orders are loading correctly on the screen (yay!):
There are a couple things wrong with it, of course. The sorting links no longer work, and now our currency is kind of messed up because the currency pipe in Angular is slightly different than its AngularJS counterpart. We'll get to that. For now, this is a great sign, because it means that our data is getting to the component and loading on the page. So, we've got the basics of this template converted to Angular. Now we're ready to tackle our sorting and filtering!
Adding Sorting
We've got our orders loading on the screen, but we don't have a way of ordering or sorting them yet. In AngularJS, it was really common to use the built-in orderBy
filter to sort the data on the page. Angular no longer has an orderBy
filter. This is because it's now strongly encouraged to move that kind of business logic into the component instead of having it on the template. So, that's what we're going to do here. (Note: we're going to be using plain old functions and events here, not a reactive form approach. This is because we're just trying to take baby steps into understanding this stuff. Once you've got the basics down, feel free to take it further with observables!)
Sorting in the Component
We already removed the orderBy
filter from ng-repeat
when we changed it to *ngFor
. Now we're going to make a sorting function on the orders component. We can use the click events on our table headers to call that function and pass in the property that we want to sort by. We're also going to have that function toggle back and forth between ascending and descending.
Let's open the orders component (./orders/orders.component.ts
) and add two public properties to the class. These are going to match the two properties that our template already references. The first one will be sortType
of type string
. The second one will be sortReverse
of type boolean
and we'll set the default value to false. The sortReverse
property just keeps track of whether to flip the order - don't think of it as a synonym for ascending or descending. (Also, side note: in the sample code we mark this variable as private to demonstrate that it won't work with Angular's AOT compiler, which we cover later in the course. You can ignore that here and keep it public.)
So you now should have this after the declaration of the title in the class:
sortType: string;
sortReverse: boolean = false;
Next, we'll add the function that we'll use with the Array.sort prototype function in JavaScript. Add this after the goToCreateOrder
function (but still within the class):
dynamicSort(property) {
return function (a, b) {
let result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
return result;
}
}
This dynamic sort function will compare the property value of objects in an array. The nested ternary function can be a little bit tricky to understand at first glance, but it's basically just saying that if the value of our property of A is less than B, return -1. Otherwise, if it's greater, return 1. If the two are equal, return 0.
Now, this isn't super sophisticated or deep comparison. There are way more sophisticated helper functions you could write to sort for you, and feel free to experiment with how you can break this one. It will do for our purposes, though, and you can just swap out this logic with whatever custom sorting logic you like.
So that's our helper function. The sort function on the Array prototype can be passed a function that it can then use to compare items in an array. Let's make a function called sortOrders
on our class that takes advantage of that with the new dynamicSort
function:
sortOrders(property) { }
The first thing we need to do is set the sortType
property on our class equal to the property that's passed in. Then we want to toggle the sortReverse
property. We'll have this:
sortOrders(property) {
this.sortType = property;
this.sortReverse = !this.sortReverse;
}
Now we can call the sort
function on this.orders
, but pass in our dynamic sort function with our property:
sortOrders(property) {
this.sortType = property;
this.sortReverse = !this.sortReverse;
this.orders.sort(this.dynamicSort(property));
}
And there's one last thing we need to do. We need to modify our dynamicSort
function just a little bit to be able to reverse the order of the array for ascending or descending. To do this, we'll tie the result of the dynamicSort
to the sortReverse
property on the class.
The first thing we'll do is declare a variable:
let sortOrder = -1;
Then, we can check if our sortReverse
property on our class is true or false. If it's true, we'll set our sort order variable equal to 1:
if (this.sortReverse) {
sortOrder = 1;
}
We're tying our functions together like this because we're only doing a simple toggle in our sort function for the sake of simplicity. To be more thorough, another approach would be to have a variable called sortDescending
instead of sortReverse
that's controlled through a separate function. If you go this route, you'll do the opposite -- sortOrder
would be 1 unless sortDescending
was true.
We could also combine these last two things into a ternary expression, but for the sake of clarity, I'm going to leave it a little bit more verbose. And then to just make our result the opposite of what it normally would be, I can just multiply result
by our sortOrder
. So our dynamicSort
function now looks like this:
dynamicSort(property) {
let sortOrder = -1;
if (this.sortReverse) {
sortOrder = 1;
}
return function(a, b) {
let result = a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0;
return result * sortOrder;
};
}
Again, this is a simple implementation of sorting, but I want to be sure you understand the key concepts of using a custom sorting function on your component.
Let's see if the sort works
So far, we've added a dynamicSort
helper function and a sortOrders
function to our class so that we can sort on our component instead of on our template.
To see if these functions are working, let's add a default sorting to our ngOnInit
function.
Inside of our forkJoin
subscription, after the forEach
where we add the customer name property, let's call this.sortOrders
and pass in the total items property:
this.sortOrders('totalItems');
When the screen refreshes, you should see that the orders are being sorted by the total items (yay!).
Now we just need to implement this sorting on our template by calling the sortOrders
function in the from the table header links.
Add sorting to the template
We've got our sortOrders
function working correctly on our orders component, which means we're now ready to add it to our template so that the table headers are clickable again.
Before we do that, let's change the default sorting in our ngOnInit
function to just be ID:
this.sortOrders('id');
That's a little bit more normal than using the total items.
Now we can work on our template. The first thing we want to do is call the sortOrders
function in all of our click events. You can select the instances of sortType =
and replace them with sortOrders(
. Then, you can replace the instances of ; sortReverse = !sortReverse
with a simple )
.
We also need to fix two of the property names that we're passing in here, as well as in the *ngIf
instances. Replace the 3 instances of orderId
with id
and the 3 instances of customername
with customerName
.
The last thing I need to do is wrap each of the href
tags in the headers in brackets so that Angular will take over and these links won't actually go anywhere. The click event will be the thing that's fired. So, the headers should follow this pattern:
<th>
<a [href]="" (click)="sortOrders('id')">
Order Id
<span *ngIf="sortType == 'id' && !sortReverse" class="fa fa-caret-down"></span>
<span *ngIf="sortType == 'id' && sortReverse" class="fa fa-caret-up"></span>
</a>
</th>
Hop over to the browser and test out all of your table header links. You should see that each one of our properties now sorts, both in ascending and descending order. Awesome!
This is great, but we did lose one thing - our cursor is a selector, not a pointer. Let's fix that with some CSS.
Fix the Cursor
We've got our sorting working correctly on our orders page, but our cursor is now a selector instead of a pointer, and that's annoying.
There are a couple of different ways we could use CSS to fix this:
- We could make a class in our main app SCSS file.
- We could write in-line CSS, although that's almost never preferable.
- We could take advantage of Angular's scoped CSS using the styles option in the component decorator
We're going to go with the last option, because it's really simple and all we need to do is add one rule to our styles for this particular component.
Open up the orders component class again. In the component decorator, we can add a new property called styles
. Styles is an array of strings, but the strings are CSS rules. To fix our cursor, all we need to do is write out a rule that says that in a table row, if we have a link, then change the cursor property to pointer. Our decorator will now look like this:
@Component({
selector: 'orders',
template: template,
styles: ['tr a { cursor: pointer; }']
})
Now, when we hover over our row headers, you see we have the pointer cursor. What's cool about this approach is that this CSS rule won't affect any other components. It will just apply to our orders component!
Now, let's see if we can do something about our filtering. That "filter filter" was removed from Angular, so we're going to have to be creative and come up with a way to implement it on our component.
Add Filtering
We're ready to replace our filter box that used to use the AngularJS filter to search through orders collection based on a string that we were searching. The AngularJS filter lived on our template and didn't require any code in our controller or component. Nowadays, that kind of logic in the template is discouraged. It's preferred to do that kind of sorting and filtering on our component class.
Add a Filter Function
Back in our component, we're going to make a new array of orders called filteredOrders
. Then we're going to pass our orders
array into a filter function that sets the filteredOrders
array. Finally, we'll use the filteredOrders
on our template in our *ngFor
instead of our original array. That way we're not ever modifying the data that comes back from the server, we're just using a subset of it.
The first thing we'll do is declare the new property on our class :
filteredOrders: Order[];
Then, in our forkJoin
that sets our original array of orders, we can set the initial state of filteredOrders
to our orders array:
this.filteredOrders = this.orders;
Now we're ready to add our function that will actually do the filtering for us. Paste this function in right after our sorting functions at bottom of our component:
filterOrders(search: string) {
this.filteredOrders = this.orders.filter(o =>
Object.keys(o).some(k => {
if (typeof o[k] === 'string')
return o[k].toLowerCase().includes(search.toLowerCase());
})
);
}
Let's talk about what's going on in this function. First, we're giving the function a string property of search
. Then, we loop through our orders and then find all of the keys of the objects. For all of the keys, we're going to see if there are some
values of those properties that match our search term. This bit of JavaScript can look a little confusing at first, but that's basically what's going on.
Note that, in our if
statement, we're explicitly testing for strings. In our example right now we're just going to limit our query to strings. We're not going to try to deal with nested properties, number properties, or anything like that. Our search term will match on our customer name property, and if we ever choose to display our address or any other string property it'll search through those as well.
Of course, we could also modify this function to test for numbers, or look through another layer of nested objects, and that's totally up to you. Just like with our sorting, we're going to start with a simple implementation and let you use your imagination to make it more complex.
Speaking of the sortOrders
function, before we move on, we need to do one last thing on the component. We just need to modify sortOrders
to use filteredOrders
now and not our original orders
, because we want the filter to take priority over the sorting. Just change it to this:
sortOrders(property) {
this.sortType = property;
this.sortReverse = !this.sortReverse;
this.filteredOrders.sort(this.dynamicSort(property));
}
Now we're ready to implement this filtering on the template.
Add Filtering to the Template
Let's move back to our template and fix it up to use our filtering.
The first thing we need to do is replace data-ng-model
. Instead of that, we're going to use the keyup
event, so we'll write, “keyup” and surround it parentheses ((keyup)
). This is a built-in event in Angular that lets us run a function on the key up of an input. Since we named our function filterOrders
, which used to be the name of the property that we were passing into the AngularJS filter, we just need to add parentheses next to it. Our input looks like this so far:
<input type="text" class="form-control" placeholder="Filter Orders (keyup)="filterOrders()">
But what do we pass into the filter orders function? Well, by default, events pass something called $event
. This contains something called a target
, which then contains the value of the input. There's one problem with using $event
. It's very difficult to keep track of those nebulous types because target.value
could really be anything. This makes it tough to debug or know what type of value is expected. Instead, Angular has a really nifty thing we can do, which is to assign a template variable to this input.
Luckily, Angular provides a really easy way to do this. After our input tag, we can add the hash sign (#) and then the name of our desired model. Let's call it #ordersFilter
. It really doesn't matter where in the tag you put this or what you call it, but I like to put it after the input so that it's easy to catch which model is associated with which input if I just glance down the page.
Now I can pass that variable into our filterOrders
function on the keyup
event. We don't need the hash symbol before it, but we do need to add .value
. This will pass the actual value of the model and not the entire model itself. Our finished input looks like this:
<input #ordersFilter type="text" class="form-control"
placeholder="Filter Orders" (keyup)="filterOrders(ordersFilter.value)">
Finally, we need to modify our *ngFor
to use the filteredOrders
array instead of the regular orders
array:
<tr *ngFor="let order of filteredOrders">
Does it all work?
You can see how much cleaner our template is now that our filtering and sorting is in the component.
Now let's check this out in the browser. If you enter some text in the box, like "sally," you should see that our orders are changing and that the sorting works on top of it: Awesome, we've replaced another AngularJS feature!
Now we've just got one last thing we need to do on this component - fix the currency pipe.
Bonus: Fix the Currency Pipe
Our final touch is to update the former currency filter, which is now called the currency pipe in Angular. We just need to add a couple of paramters to the pipe in the template that we didn't have to specify in AngularJS. This part differs if you're using Angular 4 or Angular 5:.
In Angular 4, do this:
<td>{{order.totalSale | currency:'USD':true}}</td>
In Angular 5+, do this:
<td>{{order.totalSale | currency:'USD':'symbol'}}</td>
The first option is the currency code (there's lots, you're not limited to US dollars!). The second one is the symbol display. In Angular 4, this is a boolean that indicates whether to use the currency symbol or the code. In Angular 5+, the options are symbol
, code
, or symbol-narrow
as strings.
You should now see the expected symbol:
And we're done! To see the finished code, check out this commit.
Where to Go From Here
You did a great job sticking with this to the end! Here's what we've accomplshed in this guide:
- Replacing AngularJS template syntax with Angular syntax
- Moving sorting to the component
- Using scoped CSS styles
- Moving filtering to the component
- Replacing the AngularJS currency filter with the Angular currency pipe
Wow! That's a lot - you should feel super proud!
Where should you go from here? There are lots of things you could do:
- Make the sorting more sophisticated (for example: should the ordering reset or stay the same when the user clicks a new header?)
- Make the filtering more sophisticated (search for numbers or nested properties)
- Change to a reactive approach. You could listen to an observable of value changes instead of the
keyup
function and do sorting and filtering in there. Using observables would also let you do really cool things like debounce the input!
If you love this guide, I’ve got 200+ detailed videos, quiz questions, and more for you in my comprehensive course Upgrading AngularJS. I created it for everyday, normal developers and it’s the best ngUpgrade resource on the planet. Head on over and sign up for our email list to get yourself a free Upgrade Roadmap Checklist so you don’t lose track of your upgrade prep. And, while you’re there, check out our full demo.
See you next time, Scotchers!