6

I'm running into a problem with a null value (instead of an array of Lectures) being passed to a pipe when using the safe navigation operator on an async loaded observable:

<div *ngFor="let lecture of ((lecturesObservable | async)?.lectures | lectureType: 'main')" class="list-group-item">

lecture-type.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

import { Lecture } from './lecture';

@Pipe({name: 'lectureType'})
export class LectureTypePipe implements PipeTransform {
    transform(allLectures: Lecture[], lectureType: string): Lecture[]{
        return allLectures.filter(lecture => lecture.type==lectureType);
    }
}

The lectures are iterated through fine without the pipe, once they are loaded by async. Is this just something I have to live with in ng2?

theotherdy
  • 715
  • 1
  • 7
  • 22

4 Answers4

4

The async pipe resolves to null by design when its input observable does not yet have a value. So yes, you will have to live with it, by designing your pipe to handle a null input.

Douglas
  • 5,017
  • 1
  • 14
  • 28
  • 1
    Thanks @Douglas, obvious really - thought I had tried that already but a simple: if(allLectures!==null) in the transform works beautifully! – theotherdy Aug 03 '16 at 19:49
3

You can also hide the block completely with an *ngIf, and put the async pipe there instead.

Note the important addition of let to assign to a locally scoped variable.

In your *ngFor you use that newlectures variable and it's guaranteed to always have a value - so your pipe won't see empty values.

Plus you get a 'Loading template' for 'free'

<div *ngIf="(lecturesObservable | async)?.lectures; let lectures; else loadingLectures">

    <div *ngFor="let lecture of (lectures | lectureType: 'main')" class="list-group-item">        
        Lecture details...
    </div>

</div>

<ng-template #loadingLectures>
    Loading lectures!
</ng-template>

Also note that *ngIf can be used on <ng-container> which adds nothing to the DOM.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • 1
    This is great, thanks @Simon_Weaver. Had no idea that I could assign a local variable in that check observable statement. Although the code I originally asked about has long been put to bed, I was actually struggling with checking and looping through an observable yesterday (two calls to the observable) and gave up in favour of subscribing in the .ts - this would have made that unnecessary! Thanks again. – theotherdy Jan 25 '19 at 09:09
  • Another alternative is to expose lectures to the UI with a default already set. Eg. lectures$=service.lectures.pipe(startWith([])); in the ts file. Then you can use lectures$ | async – Simon_Weaver Jan 25 '19 at 18:29
2

You can also make an Angular pipe that works directly with Observables. Then your filtering pipe could operate on either Observables or 'hard values'.

But when it's using an observable - you'll never get an error because the code won't run until there is a value.

To do this, in the signature (note the any is an array) put:

 transform(value: Observable<any> | any[])

and then check inside for either an observable or a 'hard value':

@Pipe({ name: 'filterLectures' })
export class FilterLecturesPipe implements PipeTransform {

    constructor() {}

    transform(value: Observable<any> | Array<any>, filterValue: any): Observable<any> | Array<any>
    {
        if (isObservable(value))
        {
            return value.pipe(filter(v => filterLecturesFunction(filterValue, v) ));
        } 
        else
        {
            return (value as Array<any>).filter(v => filterLecturesFunction(filterValue, v) );
        }
    }
}

Then you use it like this - nice and clean:

However this is probably bad practice, because pipes shouldn't really know much about your data model. Better to filter in the client and expose various observables.

Another way is to make a generic 'startWith' pipe with a default:

@Pipe({ name: 'startWith' })
export class StartWithPipe implements PipeTransform {

    constructor() {}

    transform(value: Observable<any> | any, defaultValue: any): Observable<any> {
        if (isObservable(value)) {
            return value.pipe(startWith(defaultValue));
        } else {
            throw Error('Needs to be an observable');
        }
    }
}

This only works with observables so you have to put it before the async:

I'm not sure of the official guidance regarding pipes using observables OR values, but this is what I've done if I need something to work with either an observable or an actual value.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
0

You can also use for example a BehaviorSubject that provides a default value so Angular doesn't throw when there wasn't a value received yet.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567