Can an @Input binding be an observable in Angular?

Viewed 8059

I'm creating a component that displays validation errors under input fields. If there is an error message shown, and the user submits the form I want to flash the message to draw their attention.

I was wondering if it's possible to use an observable as an input binding?

That way I can subscribe to the input and flash when any data is observed.

Here's an example of my idea:

@Component({..})
export class MessageComponent implement OnChanges {
   @Input()
   public flash: Observable<any>;

    public ngOnChanges(changes: SimpleChanges): void {
        if ('flash' in changes) {
            (<Observable<any>> changes['flash'].currentValue).subscribe(() => {
                // trigger the flash animation here
            });
        }
    }
}

What I can't figure out is if this will leak memory, and how/when should I unsubscribe (or is it even necessary).

Is this kind of practice allowed in Angular?

3 Answers

You are probably are better off using the async pipe if the source of the message is an observable.

<app-message [message]="message$ | async"></app-message>

This will handle the subscriptions for you, with regard to cleanup.

Then since you're already using ngOnChanges - which only triggers when there's a change - so you just trigger the animation when you get a change in message.

public ngOnChanges(changes: SimpleChanges): void {
    if ('message' in changes) {

        // trigger the flash animation here
    }
}

For this example as presented there's just no need to have your message control subscribing to things and expecting an observable as input.

Alternatively you could use animations and the :enter event to show an animation when a message is first displayed. Flashing everytime gets annoying.

If you really really want to have some kind of external trigger to cause a flash (and I realize you probably simplified your example) then you could investigate using a directive to do this so you could reuse it later. (Then you may need to use AnimationBuilder). This is a more complex route.

A bit late to the party, but I think I've found a better solution than the other two answers, by using a setter.

@Component({..})
export class ComponentTakingANumber {
  private readonly numberInput$ = new Subject<number>();

  @Input()
  set theNumber(value: number) {
    this.numberInput$.next(value);
  }
}

Now you can use numberInput$ properly and get all of the advantages, like .pipe() and | async in the template.

For example:

public readonly number$ = this.numberInput$.pipe(debounceTime(100), map(String));

And then

<div *ngIf="number$ | async as n">
    <h3>{{ n }}</h3>
</div>

I believe this is more efficient and more idiomatic than the clunky and imperative ngOnChanges.

Related