angular1to2-part1.png

Migrating from Angular 1 to 2: Part 1, Pipes – OrderBy

Over the past months vast improvements have been made to the Angular team’s new version of their product, dubbed Angular 2. Just recently, Angular 2 has came out of Alpha and catapulted into the long awaited Beta where the Angular team announced they believe their product is production ready.

In previous blogs, we’ve created dynamic and powerful directives that work with the new version’s predecessor, Angular 1.x. In this upcoming series, we want to show you how to migrate your Angular 1.x code into a fully featured Angular 2 application. Using our directive as a basis, we will show you what steps you need to take to make your migration as painless as possible.

The easiest way to migrate your code is to break down parts of your code as much as possible. In the first part of the series we are going to pick apart arguably one of the most powerful parts of our previous directive: the OrderBy filtering. In Angular 1.x, we had what were called Filters. Filters were a reusable way to transform any type of data on the fly. In Angular 2, these are now called Pipes.

Angular 2 Pipes

Angular 1.x had a filter named orderBy that came as default with the base package. This was used heavily in our Sortable Table Directive to help sort the table in order by any arbitrary property, ascending or descending. Unfortunately, Angular 2 does not include this as a default Pipe, so this is a great place to start with our migration!

Filter to Pipe

Using Filter Compared to Pipe

In Angular 1.x:

{{x.lastName}}, {{x.firstName}}

In Angular 2:

{{x.lastName}}, {{x.firstName}}

Above, we are trying to loop through a people array, ordered by the lastName property of each object within the people array, A to Z.

Creating Filter Compared to Pipe

In Angular 1.x:

.filter('customFilter', function() {
  return function (input, args) {   
    //transform input somehow
    return input
  }
})

In Angular 2:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'customPipe'})
export class CustomPipe implements PipeTransform {

 transform(input:any, args:string[]) : any {
   //transform input somehow
   return input;
 }
}

You’ll notice that the code in general looks very different compared against each other. This is due to Angular 2’s support for TypeScript, a typed superset of JavaScript that is able to be compiled into vanilla JavaScript. TypeScript helps keep your code modular and unobtrusive to other parts of your application, while maintaining speed. Angular 2 also has support for Dart, another superset of JavaScript that also compiles back to vanilla JavaScript. In our examples, we’ll stick with TypeScript.

OrderBy Pipe

Skeleton

So let’s move onto the fun stuff: the code. Now that we have the skeleton of how to build a pipe, let’s go ahead and change our classname and Pipe name to match what we are creating:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'orderBy'})
export class OrderBy implements PipeTransform {

 transform(input:any, args:string[]) : any {
   //transform input somehow
   return input;
 }
}

Improvements From Previous Version

Now that we have that set, let’s think about the old orderBy filter. You basically have an array of objects that you filter by a single property of an object, ascending or descending. Now that we’re building this from scratch, we can do even better than that. Let’s say we want to be able to sort by multiple properties of an object, and on each property we can specify if the property should be sorted ascending or descending. This should cover any use case we would ever need for sorting arrays.

Here are a few different ways we want to be able to call the new Pipe:

Single-Dimension Arrays

Ascending:

*ngFor="#x of array | orderBy"
*ngFor="#x of array | orderBy : '+'"
*ngFor="#x of array | orderBy : ['+']"

Descending:

*ngFor="#x of array | orderBy : '-'"
*ngFor="#x of array | orderBy : ['-']"

Multi-Dimension Arrays by Single Property

Ascending:

*ngFor="#x of array | orderBy : 'propertyName'
*ngFor="#x of array | orderBy : ['propertyName']"

Descending:

*ngFor="#x of array | orderBy : '-propertyName'
*ngFor="#x of array | orderBy : ['-propertyName']"

Multi-Dimension Arrays by Multiple Properties

Just add ‘-‘ in front of property name if wanting descending sorting:

*ngFor="#x of array | orderBy : ['-propertyName1', 'propertyName2', '-propertyName3']"

The Code

Pipe Arguments

We now have our outline of how we want our Pipe calls to look like. By the looks of our examples, we will only have one single argument object. The argument will either be a string or an array of strings correlating to the number of properties we will be sorting by. Let’s update our skeleton to correctly check for a valid input and argument:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'orderBy'})
export class OrderBy implements PipeTransform {

  transform(input:any, [config = '+']) : any {
    if(!Array.isArray(value)) return value; //value isn't even an array can't sort

    if(!Array.isArray(config) || (Array.isArray(config) && config.length == 1)){    
      //Single property to sort on
      var propertyToCheck:string = !Array.isArray(config) ? config : config[0];
      var desc = propertyToCheck.substr(0, 1) == '-';

      if(!propertyToCheck || propertyToCheck == '-' || propertyToCheck == '+'){
        //is a basic array that is sorting on the array's object itself
      }
      else {
        //is a complex array that is sorting on a single property
      }
    }
    else {
      //is a complex array with multiple properties to sort on
    }
  }
}

Right off the bat, you may notice we updated the second parameter of the transform method from args:string[] to [config = '+']. When you pass arguments through a Pipe like this someArray | orderBy : 'arg1' : 'arg2' : 'arg3'", the args variable is basically equal to ['arg1', 'arg2', 'arg3']. So by saying [config = '+'], we are letting the first element of the args array to be assigned to the config variable. Adding = '+' is just making a default for the variable in case no variables are passed. So if we passed someArray | orderBy : 'arg1' : 'arg2' : 'arg3'", config would be equal to 'arg1'.

We also added a check that the input is an array which is the only way we could sort it. Then you’ll notice we check to see if the config passed is an array or not. If it isn’t, we parse the config variable to find the single property to sort on. If the single property doesn’t exist or is simply '+' or '-' we can assume a simple array is attempting to be sorted. If the single property contains anything other than '+' or '-' we can assume we need to sort a complex array based on that single property. Now, if the config is an array, we know that there are multiple properties to sort on, making the input a complex array of objects.

Sorting

Single-Dimension Arrays

JavaScript does have basic sorting functionality already built into it. So for when we have a basic array, we can simply use this sorting functionality like so:

if(!Array.isArray(config) || (Array.isArray(config) && config.length == 1)){    
  //Single property to sort on
  var propertyToCheck:string = !Array.isArray(config) ? config : config[0];
  var desc = propertyToCheck.substr(0, 1) == '-';

  if(!propertyToCheck || propertyToCheck == '-' || propertyToCheck == '+'){
    //is a basic array that is sorting on the array's object itself
    return !desc ? input.sort() : input.sort().reverse();
  }
  else {
    //is a complex array that is sorting on a single property
  }
}

This is checking for the first character of the property string, and if the character '-' then we set desc to true. If it isn’t then we know we need to sort ascending. We simply use input.sort() to sort ascending and input.sort().reverse() for descending.

Multi-Dimensional Arrays

Now for multi-dimensional arrays, we’re going to have to create a custom comparator to tell the array how to sort. We need to make it custom because we want to be able to pass it any kind of value, string or number, and correctly parse that information to sort properly. Here’s the comparator:

static _orderByComparator(a:any, b:any):number{

  if((isNaN(parseFloat(a)) || !isFinite(a)) || (isNaN(parseFloat(b)) || !isFinite(b))){
    //Isn't a number so lowercase the string to properly compare
    if(a.toLowerCase() < b.toLowerCase()) return -1;
    if(a.toLowerCase() > b.toLowerCase()) return 1;
  }
  else{
    //Parse strings as numbers to compare properly
    if(parseFloat(a) < parseFloat(b)) return -1;
    if(parseFloat(a) > parseFloat(b)) return 1;
  }

  return 0; //equal each other
}

The way sorting comparator works is that you pass it two variables, a and b. Then you compare these two variables from a to b. If the value of a and b gets smaller, then you return 1 telling the sorting function that the first value is in fact larger than the second. Then if the values get bigger from a to b, return -1. Finally, if the values are equal to each other, return 0 which says there was no movement between the two values.

Now for us, we had to create a custom comparator because the default sort function compares everything as strings. It also is case-sensitive and sorts by capital letters, meaning ‘A’ is greater than ‘z’. In our custom comparator, we check each string to see if it is a string or number, then parse it as so. This allows us to sort an object array on number properties and string properties in the same Pipe call. We also lowercase the strings to make it compare without being case-sensitive.

Looping Through the Config

Now that we have our code structured to where we know what kind of array the input is, we need to be able to check and loop the config when the input is a complex array. If the config is an array we need to loop on each string and know which way to use the comparator sort on that property.

if(!Array.isArray(config) || (Array.isArray(config) && config.length == 1)){    
  //Single property to sort on
  var propertyToCheck:string = !Array.isArray(config) ? config : config[0];
  var desc = propertyToCheck.substr(0, 1) == '-';

  if(!propertyToCheck || propertyToCheck == '-' || propertyToCheck == '+'){
    //is a basic array that is sorting on the array's object itself
    return !desc ? input.sort() : input.sort().reverse();
  }
  else {
    //is a complex array that is sorting on a single property
    var property:string = propertyToCheck.substr(0, 1) == '+' || propertyToCheck.substr(0, 1) == '-'
                    ? propertyToCheck.substr(1)
                    : propertyToCheck;

    return input.sort(function(a:any,b:any){
      //need to sort with the comparator here
    });
  }
}
else {
  return input.sort(function(a:any,b:any){
    for(var i:number = 0; i < config.length; i++){
      var desc = config[i].substr(0, 1) == '-';
      var property = config[i].substr(0, 1) == '+' || config[i].substr(0, 1) == '-'
                        ? config[i].substr(1)
                        : config[i];

      //need to do comparison here
    }
  });
}

If we look at our comparator, all it takes is an a and a b as the arguments. Let's go ahead and add the comparator calls where they need to go:

if(!Array.isArray(config) || (Array.isArray(config) && config.length == 1)){    
  //Single property to sort on
  var propertyToCheck:string = !Array.isArray(config) ? config : config[0];
  var desc = propertyToCheck.substr(0, 1) == '-';

  if(!propertyToCheck || propertyToCheck == '-' || propertyToCheck == '+'){
    //is a basic array that is sorting on the array's object itself
    return !desc ? input.sort() : input.sort().reverse();
  }
  else {
    //is a complex array that is sorting on a single property
    var property:string = propertyToCheck.substr(0, 1) == '+' || propertyToCheck.substr(0, 1) == '-'
                    ? propertyToCheck.substr(1)
                    : propertyToCheck;

    return input.sort(function(a:any,b:any){
      return !desc ?
          ? OrderBy._orderByComparator(a[property], b[property])
          : -OrderBy._orderByComparator(a[property], b[property]);
    });
  }
}
else {
  return input.sort(function(a:any,b:any){
    for(var i:number = 0; i < config.length; i++){
      var desc = config[i].substr(0, 1) == '-';
      var property = config[i].substr(0, 1) == '+' || config[i].substr(0, 1) == '-'
                        ? config[i].substr(1)
                        : config[i];

      var comparison = !desc ?
          ? OrderBy._orderByComparator(a[property], b[property])
          : -OrderBy._orderByComparator(a[property], b[property]);
                    
      //Don't return 0 yet in case of needing to sort by next property
      if(comparison != 0) return comparison;
    }

    return 0; //equal each other
  });
}

Now you'll see how we set a comparison variable equal to OrderBy._orderByComparator(a[property], b[property]) in each of the input.sort functions. The .sort automatically has a callback function that passes a and b which we need to pass through our comparator. Then after we get the value from the comparator we check if desc is true and if it is, we negate the comparison value so that it flips the values to be opposite, or in our case, backwards.

When you look at the input.sort function for sorting on multiple properties, it get's a little confusion. So I'll try and break it down as best as I can. Once we're in the .sort callback, we immediately start looping across every property string in our config object. We trim off the leading '+' or '-' to get the actual property string, and then find if the property is descending or not. Then we run the comparison just like if we had a config of a single string. Then we check if the comparison variable is not equal to 0. We return comparison if it isn't 0, so the values are not the same, but if the values are the same then we hit the for loop and go to the next property. So for example if someone is trying to sort on first name then last name, if 2 people have the same first name, it will resort to the last name to make sure the sorting is correct.

Finished Pipe

Here's the entire Pipe put together:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'orderBy', pure: false})
export class OrderBy implements PipeTransform {

  static _orderByComparator(a:any, b:any):number{
    
    if((isNaN(parseFloat(a)) || !isFinite(a)) || (isNaN(parseFloat(b)) || !isFinite(b))){
      //Isn't a number so lowercase the string to properly compare
      if(a.toLowerCase() < b.toLowerCase()) return -1;
      if(a.toLowerCase() > b.toLowerCase()) return 1;
    }
    else{
      //Parse strings as numbers to compare properly
      if(parseFloat(a) < parseFloat(b)) return -1;
      if(parseFloat(a) > parseFloat(b)) return 1;
    }
    
    return 0; //equal each other
  }

  transform(input:any, [config = '+']): any{
        
    if(!Array.isArray(input)) return input;

    if(!Array.isArray(config) || (Array.isArray(config) && config.length == 1)){
      var propertyToCheck:string = !Array.isArray(config) ? config : config[0];
      var desc = propertyToCheck.substr(0, 1) == '-';
            
       //Basic array
       if(!propertyToCheck || propertyToCheck == '-' || propertyToCheck == '+'){
         return !desc ? input.sort() : input.sort().reverse();
       }
       else {
         var property:string = propertyToCheck.substr(0, 1) == '+' || propertyToCheck.substr(0, 1) == '-'
           ? propertyToCheck.substr(1)
           : propertyToCheck;

          return input.sort(function(a:any,b:any){
            return !desc ?
                ? OrderBy._orderByComparator(a[property], b[property])
                 : -OrderBy._orderByComparator(a[property], b[property]);
          });
        }
      }
      else {
        //Loop over property of the array in order and sort
        return input.sort(function(a:any,b:any){
          for(var i:number = 0; i < config.length; i++){
            var desc = config[i].substr(0, 1) == '-';
            var property = config[i].substr(0, 1) == '+' || config[i].substr(0, 1) == '-'
              ? config[i].substr(1)
              : config[i];

            var comparison = !desc ?
                ? OrderBy._orderByComparator(a[property], b[property])
                : -OrderBy._orderByComparator(a[property], b[property]);
                    
            //Don't return 0 yet in case of needing to sort by next property
            if(comparison != 0) return comparison;
          }

        return 0; //equal each other
      });
    }
  }
}

Using the Pipe

Component

The component is the controller of what you see, or the view. In the component, we'll need to import our OrderBy pipe, and assign it to be used in the component. Doing this will allow us to use the Pipe in our view.

Let's create a quick component to just test out the Pipe:

//our root app component
import {Component} from 'angular2/core'
import {OrderBy} from "./orderBy"

export class Person {
  constructor(public firstName: string, public lastName: string, public age: number) {}
} 

@Component({
  selector: 'my-app',
  templateUrl: 'path/to/your.html',
  pipes: [OrderBy]
})
export class App {
  
    fruit: string[] = ["orange", "apple", "pear", "grape", "banana"];
    numbers: number[] = [1234, 0.214, 8675309, -1, 582];
    people: Person[] = [
      new Person('Linus', 'Torvalds', 46),
      new Person('Larry', 'Ellison', 71),
      new Person('Mark', 'Zuckerberg', 31),
      new Person('Sergey', 'Brin', 42),
      new Person('Vint', 'Cerf', 72),
      new Person('Richard', 'Stallman', 62),
      new Person('John', 'Papa', 42)
    ];
    
  constructor() {}
}

We are building arrays in this component to test with. You'll see there's three arrays: fruit, numbers, and people. Each of these arrays are of different types: string, number, and Person respectively. At the top of the code above, you'll see I created a new Person class made up of firstName, lastName, and age properties to sort by.

You'll also see I imported the OrderBy class, and then assigned the class to the array of pipes in the @Component decorator by doing pipes: [OrderBy].

View

Now to write the view to print out the arrays we created in the component:

OrderBy Examples

One-Dimensional Arrays

Unordered

  • {{f}}
  • {{n}}

Asc

  • {{f}}
  • {{n}}

Desc

  • {{f}}
  • {{n}}

Multi-Dimensional Arrays

Unordered

  • {{person.firstName}} {{person.lastName}}, {{person.age}}

By Last Name Asc

  • {{person.firstName}} {{person.lastName}}, {{person.age}}

By Age Desc Then First Name Asc

  • {{person.firstName}} {{person.lastName}}, {{person.age}}

It's Working! Well… Almost…

You'll see that all of our ul's are being sorted correctly, but what if we are adding to these arrays after the view is rendered? You probably guessed that everything will work perfectly, but this isn't a perfect world. So if we were to add to any of our arrays, it would just append the new items on top of the arrays, and not be sorted into the existing items.

Let's go ahead and add a method to the component to see this error in action:

addToArrays(): void{
    this.fruit.push("new fruit");
    this.numbers.push(843);
    this.people.push(new Person('New', 'Person', 47));
    
    this.added = true;
  }

And add a button to the view to fire this method to add to the arrays on click:


The added boolean is there to hide the button after it is clicked once. So when you click that button, you'll see on every for loop of the fruit array, new fruit is just appended to the end. The same with the other pushes of the addtoArrays() method. So what gives??

The Fixes

This is simply caused by two things: Pipe Purity and ChangeDetection.

Pipe Purity

Angular 2 introduced a new idea of Pipe Purity. Pipe Purity is when a Pipe is basically stateless or stateful. By default Pipes are stateless or pure by nature, meaning the pipes are not re-executed during a change cycle. To override this functionality, we must add a property to the @Pipe decorator on the Pipe's class:

@Pipe({name: 'orderBy', pure: false})

Change Detection

Unlike the previous change we just made, Change Detection is changed on the Component level, rather than the Pipe level. As you may have guessed, this is overridden with a property added to the @Component decorator on the Component's class. We'll also need to import an extra class from one of Angular 2's typings:

import {Component,ChangeDetectionStrategy} from 'angular2/core'

@Component({
  selector: 'my-app',
  templateUrl: 'src/app.html',
  pipes: [OrderBy],
  changeDetection: ChangeDetectionStrategy.OnPush
})

There are a few different ChangeDetectionStrategy types. We are using OnPush because we only want to fire a change event when pushing objects to variables in that Component's class. There is also the All ChangeDetectionStrategy type that tells the Component to fire an event change when anything is modified at all. Added to, removed, updated, etc.

The REAL Finished Product!

And that’s it! You should now have a fully functional OrderBy Pipe with OnChange support!

Error: Embedded data could not be displayed.

Continue reading more on Angular 1.x to 2 migration with part 2 for how to migrate your Angular 1.x directives to Angular 2 components.

Here at Fuel, we are backers of Angular 2, and already have tools that use this fast, beneficial, and state-of-the-art technology on products like our Fuel Gauge Hotel Marketing Dashboard! Check out our Github for projects similar to this!

Leave a comment below!

comments

Cory ShawMigrating from Angular 1 to 2: Part 1, Pipes – OrderBy
Share this post

Related Posts