11

I'm trying to create a custom Pipe in Angular 2 that will sort an array of objects. I garnered a bit of help from this post. However, I can't seem to get this working.

My pipe looks like this:

@Pipe({
  name: "orderByAsync",
  pure: false
})
export class AsyncArrayOrderByPipe  {
  private _promise : Promise<Array<Object>>;
  private _output: Array<Object>;
 
  transform(promise: Promise<Array<Object>>, args: any): Array<Object>{
    var _property : string = "";
    var _descending : boolean = false;

    this._property = args[0]["property"] || "";
    this._descending = args[0]["descending"] || false;

    if(!this._promise) {
      this._promise = promise.then((result) => {
        result.sort((a: any, b: any) => {
          if (a[this._property] < b[this._property])  return (this._descending ? 1: -1);
          else if (a[this._property] > b[this._property]) return (this._descending ? -1: 1);
          else return 0;
        });
    
        this._output = result;
      });
    }

    return this._output;
  }
}

The use of the pipe would look like this:

<div *ngFor="#c of countries | orderByAsync">{{c.name}}</div>

It's like the view is never notified that the promise has resolved and data has been returned.

What am I missing?

Community
  • 1
  • 1
RHarris
  • 10,641
  • 13
  • 59
  • 103

2 Answers2

15

The built in async pipe injects a ChangeDetectorRef and calls markForCheck() on it when the promise resolves. To do it all in one pipe, you should follow that example. You can view the Typescript source for that here.

I would suggest, however, forgetting about handling async on your own and instead write a pure stateless sorting pipe and chain it with the built in async pipe. For that, you would write your pipe to handle a bare Array, not a promise, and use it like this:

<div *ngFor="#c of countries | async | orderBy">{{c.name}}</div>
Douglas
  • 5,017
  • 1
  • 14
  • 28
  • I had originally tried this; however, I ran into issues -- I think because my pipe was being called before the promise resolved so my `array.sort` was throwing an error. Maybe I just need to handle an empty array to begin with to account for the delay in the resolved array. I'll give that a shot. – RHarris Mar 11 '16 at 03:20
  • 2
    @RHarris The `async` pipe returns null before the promise resolves, so your pipe will need to handle null without error for chaining to work. – Douglas Mar 11 '16 at 03:42
  • @Douglas Thanks for the `null`hint.. I noticed this behavior, but I thought I di a mistake and thus got `null`.. But of course it is the desired behavior for a not yet resolved promise ;) – wzr1337 Jul 02 '16 at 20:12
  • @Douglas The link doesn't work anymore. Do you have the current sources? – Marc Borni May 10 '17 at 11:10
  • 1
    @MarcBorni A quick search on github found it. Link updated. – Douglas May 10 '17 at 20:55
  • @Douglas Great stuff thx! I'm still having problems on understanding how to use this pattern, when I need to load data in my own custom pipe itself. So for me in this example `countries` is a normal array and I have my own Pipe `loadAsyncData`. Which returns a Promise or a string (depending if extra data needs to be loaded; for example: extra data is only loaded if the country is "Switzerland". Else we just display the name). In this case would the syntax would be: `countries | loadAsyncData | async | orderBy`? – Marc Borni May 11 '17 at 05:37
  • 1
    @MarcBorni For that kind of design, you will need to write your `loadAsyncData` pipe to always return a Promise. If no extra data needs to be loaded, you can return an already-completed Promise, but it has to always be a Promise because that's what the `async` pipe handles. After making that change, your syntax would be correct. – Douglas May 13 '17 at 00:09
3

Simply return a BehaviorSubject out of your pipe which then can get bound with angular async pipe.

Small example (put it in your transform method of your pipe) which should give you 'value' after 3 seconds:

const sub = new BehaviorSubject(null);
setTimeout(() => { sub.next('value'); }, 3000);
return sub;

Complete example:

import { IOption } from 'somewhere';
import { FormsReflector } from './../forms.reflector';
import { BehaviorSubject } from 'rxjs';
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'getOptions' })
export class GetOptionsPipe implements PipeTransform  {

  public transform(value, ...args: any[]) {
    const _subject = new BehaviorSubject('-');
    if (args.length !== 2) {
      throw `getOptions pipe needs 2 arguments, use it like this: {{ 2 | getOptions:contract:'contractType' | async }}`;
    }
    const model = args[0];
    if (typeof model !== 'object') {
      throw `First argument on getOptions pipe needs to be the model, use it like this: {{ 2 | getOptions:contract:'contractType' | async }}`;
    }
    const propertyName = args[1];
    if (typeof propertyName !== 'string') {
      throw `Second argument on getOptions pipe needs to be the property to look for, ` +
        `use it like this: {{ 2 | getOptions:contract:'contractType' | async }}`;
    }
    const reflector = new FormsReflector(model);
    reflector.resolveOption(propertyName, value)
    .then((options: IOption) => {
      _subject.next(options.label);
    })
    .catch((err) => {
      throw 'getOptions pipe fail: ' + err;
    });
    return _subject;
  }
}
Michael Baarz
  • 406
  • 5
  • 17
  • Hey, man, you can't put some code out the context. There is no information about `FormsReflector`, despite i might guess its functionality. For your inspiration, +1. – e-cloud Jul 26 '17 at 09:07
  • Yeah, so what is important is that you should return a BehaviorSubject from your pipe. So you can then fill the value at any time and it will get reflected: const sub = new BehaviorSubject(null); setTimeout(() => { sub.next('value'); }, 3000); return sub; This is the short version of what you need. Pipes are pushing their values from one to each other. The BehaviorSubject can be then piped to async pipe of angular. – Michael Baarz Jul 26 '17 at 21:17
  • This answer is uber uber cool but I think most people won't understand how cool it is! I've used this approach here - https://stackoverflow.com/a/67732691/1205871 - which may help people understand how cool this is! – danday74 May 28 '21 at 04:01