12

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?

1
  • 1
    Can you please create a quick bin so the snippet can be played around. Commented Mar 10, 2016 at 23:27

2 Answers 2

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>
Sign up to request clarification or add additional context in comments.

7 Comments

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 The async pipe returns null before the promise resolves, so your pipe will need to handle null without error for chaining to work.
@Douglas Thanks for the nullhint.. 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 ;)
@MarcBorni A quick search on github found it. Link updated.
@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.
|
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;
  }
}

3 Comments

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.
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.
This answer is uber uber cool but I think most people won't understand how cool it is! I've used this approach here - stackoverflow.com/a/67732691/1205871 - which may help people understand how cool this is!

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.