Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Input as Observable #5689

Open
lacolaco opened this issue Dec 8, 2015 · 79 comments
Open

Proposal: Input as Observable #5689

lacolaco opened this issue Dec 8, 2015 · 79 comments

Comments

@lacolaco
Copy link
Contributor

@lacolaco lacolaco commented Dec 8, 2015

Sorry, I'm not good at English.

@Input property values are provided by parent component. The changes come asynchronously.
And if Input property was changed in the child component (it has the property as own property) , its change detector never notice it.

Goal

  • Parent's input data and child's input property should be synchronized.
  • Developers should understand that input properties are changed asynchronously.

Proposal

@Component({ selector: "child" })
class Child {
  @Input("input") inputValue: Observable<T>;

  ngOnInit() {
    this.inputValue.map((value)=>...);
  }
}

@Component({
  template: `
  <child [input]="valueToChild"></child>
  `
})
class Parent {
  valueToChild: T;
}

Above code does not work. Currently, to receive input as Observable<T>, I must write it like below.

@Component({ selector: "child" })
class Child {
  @Input("input") inputValue: Observable<T>
}

@Component({
  template: `
  <child [input]="valueToChild"></child>
  `
})
class Parent {
  valueToChild: Observable<T> = new Observable<T>((observer)=>{
    ...
    observer.next(val);
  });
}

Example: http://plnkr.co/edit/BWziQygApOezTENdTVp1?p=preview

This works well, but it's not essential. Parent's input data is a simple data originally.

I think this proposal make us happy.

Thanks.

@robwormald

This comment has been minimized.

Copy link
Member

@robwormald robwormald commented Dec 8, 2015

Hi @laco0416 - your English is fine, don't worry!

I very much like this idea, and it's something we've discussed before. It also matches up nicely with #4062 (Observing view events) and #5467 (Observable child events from parents)

It's important to remember that not everybody will want to use Observables (these people are missing out!), so we must provide options for both use cases, and so it's unlikely we'll make @Input() directly into an Observable. I do think that having something like @ObserveInput() might work, and we'll have a discussion after we ship beta about some of these more interesting features I think.

In the meantime, here's a basic (and very experimental!!! do NOT do this for real) implementation of this idea. Is this conceptually what you were thinking? http://plnkr.co/edit/Nvyd9IPBZp9OE2widOcW?p=preview

@alexpods

This comment has been minimized.

Copy link

@alexpods alexpods commented Dec 8, 2015

I think it's a very bad idea to change input properties in a child component. Input properties should be "read-only". Your data should always flow from parent to child (and never in the reverse direction).

@robwormald

This comment has been minimized.

Copy link
Member

@robwormald robwormald commented Dec 8, 2015

@alexpods i believe the idea here is exactly that - its listening to the change in input properties as an Observable, not emitting values upstream, which is absolutely fine as far as I'm concerned.

@lacolaco

This comment has been minimized.

Copy link
Contributor Author

@lacolaco lacolaco commented Dec 8, 2015

@robwormald

your English is fine, don't worry!

Thank you! I'm so relieved.

Your @ObserveInput is what just I want!
Also, @Input has no breaking changes. I think it is a very good solution.

@lacolaco

This comment has been minimized.

Copy link
Contributor Author

@lacolaco lacolaco commented Dec 8, 2015

@alexpods Me too at all.

its listening to the change in input properties as an Observable, not emitting values upstream, which is absolutely fine as far as I'm concerned.

I think in the same way as Rob.

@alexpods

This comment has been minimized.

Copy link

@alexpods alexpods commented Dec 8, 2015

@laco0416 Ooh, sorry for misunderstanding. The phrase "if Input property was changed in the child component" confused me.

@wmaurer

This comment has been minimized.

Copy link

@wmaurer wmaurer commented Jan 3, 2016

I don't know if I should comment here or if I should open a new issue. Please let me know if I'm adding a request to the wrong place.

I've been trying (but, until now, failing) to write a such a decorator, and then I stumbled upon @robwormald's plunkr, which works almost perfectly (but not quite).

What got me excited by this approach was the fact that it is leveraging into the ngOnChanges lifecycle hook.
What I would like to see is some way for all lifecycle hooks to be exposed as Observables, i.e. as onChanges$: Observable<{[key: string]: SimpleChange}>, onInit$: Observable<{}>, etc.

Having all lifecycle hooks available as Observables will help me Rx all the things ;-)

@fxck

This comment has been minimized.

Copy link

@fxck fxck commented Feb 23, 2016

Any updates on this?

@lacolaco

This comment has been minimized.

Copy link
Contributor Author

@lacolaco lacolaco commented Feb 23, 2016

AFAIK, No.
@robwormald do you have any news?

@Guardiannw

This comment has been minimized.

Copy link

@Guardiannw Guardiannw commented Jul 15, 2016

I know this is old, but this would be great! @robwormald Any word on this?

@lephyrus

This comment has been minimized.

Copy link

@lephyrus lephyrus commented Oct 17, 2016

I'd love to provide changing @Input property values as an Observable, just like @robwormald's @ObserveInput decorator does. It's not always feasible to have parent components pass Observables, especially when you're migrating an existing (Angular 1) application. Not being able to "contain" Observables within a single component makes leveraging the power and elegance of RxJS much harder, though.

Unfortunately, Rob wasn't kidding when he said not to use this version of @ObserveInput. The problem I'm running into is that the Observables are created on a "class-level" (if that terminology makes sense) and are hence shared across all instances of the component. This is no good, obviously. Creating the Observables at instantiation time did not work for me, either. It seems Angular doesn't correctly wire up change detection it that case.

Has anyone managed a better implementation of @ObserveInput or is there any news on official support?

@wmaurer

This comment has been minimized.

Copy link

@wmaurer wmaurer commented Oct 17, 2016

@lephyrus While an official @ObserveInput decorator would be a very nice, it's not strictly necessary in order to get an Observable of changing @Input property values. The decorator would simply be very elegant "sugar".

There is already a lifecycle event ngOnChanges. Inside ngOnChanges we can use an rxjs Subject, to create an observable of changes, i.e.:

@Input inputString: string;
private Subject<string> inputString$ = new Subject<string>;

ngOnChanges(changes: { [key: string]: SimpleChange }) {
    if (changes.hasOwnProperty('inputString')) {
        this.inputString$.next(changes['inputString'].currentValue);
    }
}

constructor() {
    inputString$.subscribe(x => {
        console.log('inputString is now', x);
    });
}

Or, if you want something more reusable, you could create a base class that your componentextends:

import { SimpleChange } from '@angular/core';
import { Observable, ConnectableObservable, Observer } from 'rxjs';

export interface TypedSimpleChange<T> {
    previousValue: T;
    currentValue: T;
}

export class ReactiveComponent {
    private changesObserver: Observer<{ [key: string]: SimpleChange }>;
    private changes$: ConnectableObservable<{ [key: string]: SimpleChange }>;

    constructor() {
        this.changes$ = Observable.create((observer: Observer<{ [key: string]: SimpleChange }>) => this.changesObserver = observer).publishReplay(1);
        this.changes$.connect();
    }

    public observeProperty<T>(propertyName: string): Observable<TypedSimpleChange<T>> {
        return this.changes$
            .filter(changes => changes.hasOwnProperty(propertyName))
            .map(changes => changes[propertyName]);
    }

    public observePropertyCurrentValue<T>(propertyName: string): Observable<T> {
        return this.observeProperty<T>(propertyName)
            .map(change => change.currentValue);
    }

    ngOnChanges(changes: { [key: string]: SimpleChange }) {
        this.changesObserver.next(changes);
    }
}

... which could be used as follows:

@Component({
    ...
})
export class YourComponent extends ReactiveComponent {
    @Input() inputString: string;

    constructor() {
        super();
        this.observePropertyCurrentValue<string>('inputString')
            .subscribe(x => console.log('inputString is now', x));
    }
}

I am using this approach until an official @ObserveInput decorator is available.

@lephyrus

This comment has been minimized.

Copy link

@lephyrus lephyrus commented Oct 18, 2016

Thank you, @wmaurer. Your ReactiveComponent is very welcome, and using impeccable code and safe typing to boot - really nice! Importantly, it also behaves well under test and still allows using the OnPush change detection strategy. I'm now happy to wait for the "official sugar". (Also I'm sure I'll learn something when I figure out why you had to use the Observable.create() logic - I haven't found time to look into it yet.) Again: merci gäll! 😉

@wmaurer

This comment has been minimized.

Copy link

@wmaurer wmaurer commented Oct 18, 2016

@lephyrus you're welcome, gärn gescheh ;-)

I used Observable.create() to get hold of an Observer in order to be able to do a next(). I could have used a Subject which is both an Observable and an Observer, but I believe it's generally bad practice to 'expose' a Subject (Observer).

@DzmitryShylovich

This comment has been minimized.

Copy link
Contributor

@DzmitryShylovich DzmitryShylovich commented Feb 5, 2017

@laco0416 close in favor of #13248 ?

@lacolaco

This comment has been minimized.

Copy link
Contributor Author

@lacolaco lacolaco commented Feb 8, 2017

@DzmitryShylovich No. The feature proposed in this issue is read-only and event-driven data passing.

@huan

This comment has been minimized.

Copy link

@huan huan commented May 6, 2017

@ObserveInput will be super cool! 👍

@ChrisWorks

This comment has been minimized.

Copy link

@ChrisWorks ChrisWorks commented May 30, 2017

Thank you @wmaurer for a good example. I have one question though. I would like to be able to use an object instead of a string as the observable.

E.g.


@Input() chartConfig: ChartConfig;

constructor(private _reportService: ReportService) {
		super();
             this.observePropertyCurrentValue<string>('chartConfig')
            .subscribe(changedConfig => this.updateChart(changedConfig));
 }

export class ChartConfig {
	public id: string;
	public type: any;
	public data: any;
	public labels: any;
}

However the this.updateChart and the ngOnChanges is not called. How can expand your sample from a simple string to observe an object instead?

@wmaurer

This comment has been minimized.

Copy link

@wmaurer wmaurer commented Jun 19, 2017

@ChrisWorks it should also work with objects:

this.observePropertyCurrentValue<ChartConfig>('chartConfig')
            .subscribe(changedConfig => console.log(changedConfig));

I do this very often, so if this doesn't work, I'd guess there's a problem with the input to your component somewhere.

@ChrisWorks

This comment has been minimized.

Copy link

@ChrisWorks ChrisWorks commented Jun 19, 2017

Hi @wmaurer, thanks for the reply. Would you be able to expand your sample with a working version where you use a "config" object? I simply cant get mine to work. A sample Git repo? :)

@wmaurer

This comment has been minimized.

Copy link

@wmaurer wmaurer commented Jun 20, 2017

@ChrisWorks it really should just work the way it is. observePropertyCurrentValue doesn't differentiate between a string input and an object.
Here's a really old ng2 beta 0 project I made where inputs are of all different types, not just strings:
https://github.com/wmaurer/todomvc-ng2-reactive
e.g. https://github.com/wmaurer/todomvc-ng2-reactive/blob/master/src/app/todo-item/todo-item.component.ts

@Dashue

This comment has been minimized.

Copy link

@Dashue Dashue commented Jul 12, 2017

+1 Such a fundamental use-case, can't believe it hasn't been sorted yet!

@ntziolis

This comment has been minimized.

Copy link

@ntziolis ntziolis commented Oct 22, 2018

Any update on when this will be released so far the observable-input package seems the like most elegant without breaking anything + no subclassing. @ohjames How stable / well tested is this at this point?

@agalazis

This comment has been minimized.

Copy link

@agalazis agalazis commented Nov 13, 2018

I think official support would be more mature, will guarantee continuous support across versions and smoother dev experience/will encourage developers to go full rxjs* and optimize with on push change detection (this should be a core groundbreaking feature to my opinion)

*without compromizing compatibility with non observable inputs or needing extra boilerplate for hybrid inputs ie: setter calling next on subject

@ohjames

This comment has been minimized.

Copy link

@ohjames ohjames commented Nov 13, 2018

Used observable-input in angular 4, 5 and 7 with and without aot. It seems to work well. I'm pretty sure I vaguely knew what I was doing when I wrote it.

@agalazis

This comment has been minimized.

Copy link

@agalazis agalazis commented Nov 13, 2018

hope they 'll add it in the framework and encourage its usage 2 weekly downloads npm is not enough compared to the advantages it offers xD if they add it I bet people will see that it's simple to follow that approach

@abdulkareemnalband

This comment has been minimized.

Copy link

@abdulkareemnalband abdulkareemnalband commented Jan 8, 2019

I propose following API for async input with user defined observer

decalre function AsyncInput(bindName?:string) : <T extends Observer>(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => any

users could just use it as

@AsyncInput()
asynchronusProperty1 = new Subject();

@AsyncInput()
asynchronusProperty2 = new ReplySubject(1);

@AsyncInput()
asynchronusProperty3 = new UserObserver(); // where UserObserver implement `Observer` interface

Angular internal can just call observer.next(newValue) whenever value changes.

@simeyla

This comment has been minimized.

Copy link

@simeyla simeyla commented Mar 11, 2019

Don't forget @HostBinding support too!

@benneq

This comment has been minimized.

Copy link

@benneq benneq commented May 30, 2019

This should be combined with constructor @input injection to make it even more awesome. This way we could fix "strictPropertyInitialization" in a very elegant way:

class MyComponent {
  inputData$: Observable<Data>;
  constant: string;

  constructor(
    @Input() inputData$: Observable<Data>,
    @Input() constantString: string
  ) {
    this.inputData$ = inputData$;
    this.constant = constantString;
  }

}
@maxime1992

This comment has been minimized.

Copy link

@maxime1992 maxime1992 commented Jun 3, 2019

@benneq just worth noting it could be as small as:

class MyComponent {
  constructor(
    @Input() private inputData$: Observable<Data>,
    @Input() private constantString: string,
  ) {}
}
@ohjames

This comment has been minimized.

Copy link

@ohjames ohjames commented Jun 3, 2019

@maxime1992 yep, although you'd need a separate decorator for inputs as observables to distinguish between the case of observables being passed from one component to another.

@trotyl

This comment has been minimized.

Copy link
Contributor

@trotyl trotyl commented Jun 3, 2019

@benneq @maxime1992 Parameter names are not part of decorator metadata, how do you get it mapped?

@benneq

This comment has been minimized.

Copy link

@benneq benneq commented Jun 3, 2019

@trotyl I guess you could then use @Input('paramName') private myParam: Observable<Data>

@maxime1992

This comment has been minimized.

Copy link

@maxime1992 maxime1992 commented Jun 3, 2019

@trotyl @benneq yes my bad. Never mind 🤐

@ElianCordoba

This comment has been minimized.

Copy link

@ElianCordoba ElianCordoba commented Jun 6, 2019

I don't know if someone already posted this solution but this is a fairly good workaround

@benneq

This comment has been minimized.

Copy link

@benneq benneq commented Jun 6, 2019

@ElianCordoba This is not about HTML <input>. It's about angular @Input()

@fxck

This comment has been minimized.

Copy link

@fxck fxck commented Jun 6, 2019

That's a different, but similarly painful issue @ElianCordoba. #13248

@sxlwar

This comment has been minimized.

Copy link

@sxlwar sxlwar commented Jun 25, 2019

I think this is a pseudo-requirement. Firstly, the input decorator can accept an Observable type value, secondly, if the component requires an observable as input, it should be explicitly throw error when received a non-observable value instead of quietly convert it to Observable type value.

It is quite similar to implicit type conversion in javascript, may be confused even cause bug and difficult to found.

@agalazis

This comment has been minimized.

Copy link

@agalazis agalazis commented Jun 25, 2019

That's not about silencing wrong input. It's about exposing a common interface across components (which is non-observable input). This gives you the freedom to introduce rx functionality without breaking its interface in the outside world. Another benefit is mainly lifting mutability and internal state since everything will be a transformation of the input which makes on push change detection from that point onwards a no brainer.

On the contrary, exposing, an interface that requires observables sounds clunky and forcing people to provide of(value) just because your component said so sounds weird to me.

@ohjames

This comment has been minimized.

Copy link

@ohjames ohjames commented Jun 26, 2019

Plus it's not about silent conversion, it's an API to subscribe to changes via rxjs. Given angular.http and the router provide observables it's super awkward that input change handling does not, unifying the worlds of callbacks and RxJs requires too much boiler plate.

@simeyla

This comment has been minimized.

Copy link

@simeyla simeyla commented Jun 30, 2019

Not that I don't love some of the clever solutions above, but sometimes just a slight reorganization of the 'standard' way to do it is enough to solve the real problem here which is lack of clarity / clumsiness. Plus I'm still hoping for an 'official' post-ivy way to do this someday.

@Input('isOuterPanel')
set isOuterPanel(value: CheckoutPanelHeaderSettings)
{
    this.inputs.outerPanel$.next(value);
}

@Input('config')
set config(value: CheckoutPanelHeaderSettings)
{
    this.inputs.config$.next(value);
}

// observables for @Inputs
inputs = {
    config$: new BehaviorSubject<CheckoutPanelHeaderSettings>(1),
    outerPanel$: new BehaviorSubject<CheckoutPanelHeaderSettings>(1)
};

Then you can just put something like combineLatest(this.service.magicInput$, this.inputs, config$)...... to combine inputs with RxJS.

Either BehaviorSubject or ReplaySubject works. BehaviorSubject is usually safer and more predictable.

  • BehaviorSubject - use if you have a defaults
  • ReplaySubject - observable won't emit if you forget to set the input value

This not only helps with code clarity but because I'm grouping all the inputs together I can easily see what's a 'pure' observable input by just typing this.inputs. and getting auto-complete.


You can go further with type safety with this (mostly this is just for fun).

// define this globally to 'unwrap' a property 'inputs' with an object of ReplaySubject / BehaviorSubject
export type InputTypes<T extends { inputs: { [key: string]: ReplaySubject<any> | BehaviorSubject<any> } }> = {
    [P in keyof T['inputs']]: T['inputs'][P] extends Observable<infer X> ? X : unknown;
}
// define a local 'InputType' helper above each component
type InputType = InputTypes<CheckoutSmartHeaderComponent>;

Then you don't need to explicitly specify the type of the @input property.

@Input('config')
set config(value: InputType['config$'])
{
    this.inputs.config$.next(value);
}

I could have made an ObservableInputs interface for the component to enforce inputs but decided not to since it won't compile anyway if you screw this up.

@ohjames

This comment has been minimized.

Copy link

@ohjames ohjames commented Jul 2, 2019

@simeyla Too much boilerplate.

@mfp22

This comment has been minimized.

Copy link

@mfp22 mfp22 commented Jul 11, 2019

I decided to put my own decorator out there. It's my first npm package so I'm sure there's something I'm missing, but here it is: https://www.npmjs.com/package/@ng-reactive/async-input

Installation

npm install @ng-reactive/async-input --save

Usage

import { Component, Input } from '@angular/core';
import { AsyncInput } from '@ng-reactive/async-input';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css']
})
export class HelloComponent {
  @Input() name: string;
  @AsyncInput() name$ = new BehaviorSubject('Default Name');

  constructor() {
    this.name$.subscribe(name => console.log('from async input', name));
  }
}
@chriszrc

This comment has been minimized.

Copy link

@chriszrc chriszrc commented Nov 8, 2019

@mfp22 really nice, I see no reason why something like this shouldn't be built in. FYI, the github link on npm for the package is outdated, it goes to here:

https://github.com/mfp22/async-input/tree/master/projects/async-input

@gund

This comment has been minimized.

Copy link

@gund gund commented Nov 20, 2019

I see this is pretty old issue but the feature is pretty amazing and I'm really exited to see it in Angular.

What is even cooler is that with observable inputs you do not need OnChanges hook - you can use pairwise rxjs operator to get previous and current values of input observable:

@Component({...})
class MyReactiveComponent {
  @ObservableInput() prop: Observable<string>; // Whatever syntax may be...

  // emits [prevValue, currValue] and no OnChanges hook yay!!
  propChanges$ = this.prop.pipe(pairwise());
}
@JounQin

This comment has been minimized.

Copy link

@JounQin JounQin commented Nov 24, 2019

https://github.com/rx-ts/ngrx/blob/master/src/utils/decorators.ts#L56-L124

It's my personal implementation, it works perfectly and pretty awesome, if we can have it build-in, that is really cool.

@Futhark

This comment has been minimized.

Copy link

@Futhark Futhark commented Nov 30, 2019

I've published the solution I've used personally in my projects as a npm package:

https://github.com/futhark/ngx-observable-input

Installation

npm install ngx-observable-input

Usage

...
<image-item [url]="currentImageUrl"></image-item>
import { Component, Input } from "@angular/core";
import { ObservableInput } from "ngx-observable-input";
import { Observable } from "rxjs";

@Component({
    selector: "image-item",
    template: `<img [src]="url$ | async" />`
})
export class GalleryComponent {
    @ObservableInput() @Input("url") public url$: Observable<string>;

    ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.