Can an @Input binding be an observable in Angular?
Asked Answered
S

3

8

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?

Syllabism answered 8/7, 2017 at 1:11 Comment(0)
T
10

Yes, you can use an observable as an input.

Whether or not you need to unsubscribe depends upon the observable in question. When an observable completes or errors, any subscribers are unsubscribed automatically. So, in general, if you know an observable completes, explicit unsubscription is not necessary.

However, looking at your snippet, this seems to be a secondary issue, as you've written code suggesting that you expect the input to change.

In that case, you should unsubscribe whenever a change occurs. Otherwise, you will have two subscribers - with the first still listening to the original flash observable.

import { Subscription } from 'rxjs/Subscription';

@Component({..})
export class MessageComponent implement OnChanges, OnDestroy {

    @Input()
    public flash: Observable<any>;
    private flashSubscription: Subscription;

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

    public ngOnDestroy(): void {
        if (this.flashSubscription) {
            this.flashSubscription.unsubscribe();
        }
    }

I'd also call unsubscribe in ngOnDestroy - so that unsubscription will occur for flash observables that don't complete or error.

Note that is is safe to call a subscription's unsubscribe method multiple times.

Trierarch answered 8/7, 2017 at 1:45 Comment(1)
Also, you might want to test for flash changing to null or undefined.Trierarch
F
3

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.

Fitted answered 21/9, 2018 at 4:50 Comment(0)
E
3

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.

Extenuatory answered 4/3, 2022 at 15:25 Comment(2)
Any idea why it might not work ? In my case the input value is FormControl instance. When I subscribe(v => console.log(v)) nothing displays in consoleJanellejanene
Yes, try changing the Subject() to a BehaviorSubject(1). The problem might be that next() is called before the template has rendered and thus, before the async pipe subscribes. If that happens, the value is lost. BehaviorSubject acts as a buffer.Extenuatory

© 2022 - 2024 — McMap. All rights reserved.