3

How to make a sorting pipe in angular2 with an array of objects

Original Problem:

I have a TODOs list, (Todo[ ]) and I want to sort it every time I make some changes. I want that the completed todo are displayed at the bottom of the list. The Todo object has a property named .completed that stores a boolean value, it will tell us if the todo is completed or not.

Creating the Pipe:

In Angular2 the "OrderBy" pipe does not exist. So we have to build it:

import { Pipe, PipeTransform } from "angular2/core";
//Todo is the interface for our todo object
import {Todo} from './todo';

@Pipe({
  name: "sort",
  //set to false so it will always update, read below the code.
  pure: false
})
export class TodosSortPipe implements PipeTransform {
  transform(array: Todo[], args: any): Todo[] {
    //watch the console to see how many times you pipe is called
    console.log("calling pipe");
    /* javascript is async, so could be that the pipe is called before
    that the todos list is created, in this case we do nothing
    returning the array as it is */
    if (isBlank(array)) return null;
    array.sort((a, b) => {
      if (a.completed < b.completed) {
        return -1;
      //.completed because we want to sort the list by completed property
      } else if (a.completed > b.completed) {
        return 1;
      } else {
        return 0;
      }
    });
    return array;
  }
}

If you didn't understand the sort method check MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

The Pipe is done, let's move to the Component.

Creating the Component:

The AppComponent class, creates an array of Todo, called Todos, getting the objects from a mock with a service.

import {Component, OnInit} from 'angular2/core';
import {Todo} from './todo';
import {TodoService} from './todo.service';
import {TodosSortPipe} from './sort-pipe.component'

@Component({
    //name of the html element
    selector: 'my-app',
    //view of the selector,  " ` " is alt + 9
    templateUrl: "./app/todo-list.component.html",
    providers: [TodoService],
    pipes: [ TodosSortPipe ]
})

export class AppComponent implements OnInit{

    public todos: Todo[];
    public edited = false;
    public changes = 0;
    //creating an istance of todoService
    constructor(private _todoService: TodoService) { };

    //getting the todos list from the service
    getTodos() {
        this._todoService.getTodos().then(todos => this.todos = todos);
    }

   (...)
   editTodo(todo: Todo): void {
        //slice is very important, read below the code
        this.todos = this.todos.slice();
        this.saveTodos();
    }
}

Template implementation

This is the pipe calling:

<li *ngFor="#todo of todos | sort; #i=index">
 (...)
</li>

Demo:

For the full example with all the code: https://plnkr.co/edit/VICRMVNhqdqK9V4rJZYm?p=preview Watch it on github: https://github.com/AndreaMiotto/Angular2-TodoApp

Pipe Unpure

Pipes only change by default when your Pipe input parameters change and not when your data changes. Setting Pure to false, you will make it "unpure" so you pipe will always update.

3 Answers 3

1

Perhaps the value todos is null at the beginning because it's loaded asynchronously using HTTP.

To prevent from such use case you could add this in your pipe:

@Pipe({
  name: "sort"
})
export class TodosSortPipe implements PipeTransform {
  transform(array: Todo[], args: any): Todo[] {
    if (array == null) {
      return null;
    }
    (...)
  }
}

Then the value todos will be received and the transform method of the pipe will be called again with this non null value...

Moreover it seems that your <li> tag isn't ended. You must have valid HTML into component templates. I don't know if it's the complete code or a truncated one...

Hope it helps you, Thierry

Sign up to request clarification or add additional context in comments.

11 Comments

Thank you Therry, now it works, but it does not sort because it will return null. This is just avoiding the error, not fixing it.
If the input value is null, there is nothing to sort.
Yes agreed with @Günter ;-)
Is your todos property null?
The line describing how you call the pipe is the complete one or a truncated one? I mean you must have valid HTML (<li> closed) to have the code works as expected...
|
1

I've created an OrderBy pipe that supports both single and multi-dimensional arrays. It also supports being able to sort on multiple columns of the multi-dimensional array.

<li *ngFor="#todo of todos | orderBy : ['completed']; #i=index">
    {{i}}) {{todo.name}} - {{todo.completed}}
</li>

This pipe does allow for adding more items to the array after rendering the page, and still sort the arrays with the new items dynamically.

I have a write up on the process here.

And here's a working demo: http://fuelinteractive.github.io/fuel-ui/#/pipe/orderby and https://plnkr.co/edit/DHLVc0?p=info

Comments

1

You need to change your html template so that your pipe can accommodate your async code.

Change this line:

<li *ngFor="#todo of todos | sort; #i=index">
   (...)
</li>

To this:

<li *ngFor="#todo of todos | async | sort; #i=index">
   (...)
</li>

I copied your Pipe code into this Plunker:

https://plnkr.co/edit/RBpZilgxIyRDLwktNZN1?p=preview

6 Comments

Now it calls the pipe only twice, when i make some changes at any todo, so the .completed property changes, the sort isn't call again.
In fact, it's another problem ;-) In fact the filter will be called again if the reference of the array changes not one element in it. See this answer for more details: stackoverflow.com/questions/35076907/…
Ok i got the problem, so I need to force a change of reference. How can I make it?
You can do it with combining with async pipe, i will update my plunker, wait a sec.
question edited with the answer with "pipe unpure" solution.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.