Search code examples
angulartypescriptangular2-httpangular2-pipe

Angular 2 Usage of Http Observable and Pipe


I have a service that call a REST endpoint:

import { Task } from './task';
import { TaskStatus } from './task-status';
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class TaskService {
    constructor(private http: Http){
    }
    getTasks() {
        return this.http.get('http://localhost:8080/tasks').map(res => res.json()).map(rest => rest._embedded.tasks);
    }
}

The endpoint returns a result like this:

{
  "_embedded": {
    "tasks": [
      {
        "title": "zxc",
        "description": "zxc",
        "status": "New",
        "_links": {
          "self": {
            "href": "http://localhost:8080/tasks/1"
          },
          "task": {
            "href": "http://localhost:8080/tasks/1"
          }
        }
      },
      {
        "title": "asd",
        "description": "qweqwe",
        "status": "New",
        "_links": {
          "self": {
            "href": "http://localhost:8080/tasks/2"
          },
          "task": {
            "href": "http://localhost:8080/tasks/2"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/tasks"
    },
    "profile": {
      "href": "http://localhost:8080/profile/tasks"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 3,
    "totalPages": 1,
    "number": 0
  }
}

I use the service in this component:

@Component({
  selector: 'app-mycomp',
  templateUrl: './my.component.html',
  styleUrls: ['./my.component.css']
})
export class MyComponent implements OnInit {
  tasks: Array<Task>;

  constructor(private taskService:TaskService) {
    taskService.getTasks()
    .subscribe(tasks => this.tasks = tasks, 
                err => console.error(err), 
                () => console.log('done'));
  }
}

And the template looks like this;

<task-details *ngFor="let task of tasks" [task]="task"></task-details>

This works as expected, but when I try to use a pipe in the template:

<task-details *ngFor="let task of tasks | WithStatus: TaskStatus.New" [task]="task"></task-details>

I got the error "Cannot read property 'filter' of undefined".

Here is the pipe implementation:

import { Pipe, PipeTransform } from '@angular/core';
import { TaskStatus } from './task-status';

@Pipe({ name: 'WithStatus', pure: true })
export class TaskStatusFilter implements PipeTransform{
    transform(value: any, ...args: any[]): any {
        console.log(value);// value is undefined
        return value.filter(item => item.status == args[0]);
    }
}

Solution

  • On initial change detection cycle when ajax haven't been completed that time it tries to evaluate bindings and pass tasks value as in undefined to WithStatus pipe and it throws an error. For such case you have to handle it inside a Pipe only.

    @Pipe({ name: 'WithStatus', pure: true })
    export class TaskStatusFilter implements PipeTransform{
        transform(value: any, ...args: any[]): any {
            console.log(value);// value is undefined
            return (value || []).filter(item => item.status == args[0]);
        }
    }
    

    The other way would be you should inject task-details DOM inside DOM tree until ajax gets succeeded using *ngIf structural directive.

    <template [ng-if]="tasks">
      <task-details 
        *ngFor="let task of tasks | WithStatus: TaskStatus.New" 
        [task]="task">
      </task-details>
    </template>
    

    Instead of <template> you can also use <ng-container> which allows to use the same syntax as inline *ngIf:

    <ng-container *ngIf="tasks">
      <task-details 
        *ngFor="let task of tasks | WithStatus: TaskStatus.New" 
        [task]="task">
      </task-details>
    </ng-container>