Skip to content

RFC: Component: Proposal for a new package component #2052

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

Closed
BioPhoton opened this issue Aug 7, 2019 · 101 comments
Closed

RFC: Component: Proposal for a new package component #2052

BioPhoton opened this issue Aug 7, 2019 · 101 comments

Comments

@BioPhoton
Copy link
Contributor

BioPhoton commented Aug 7, 2019

RFC: Component: Proposal for a new package component

Reactive primitives for components

#2046


  • Start Date: 2019-08-07
  • RFC: Component: Proposal for a new package component

Table of Content

  • Summary
  • Motivation
  • Overview
    • Input Decorator
    • Output Decorator
    • HostListener Decorator
    • HostBinding Decorator
    • Input Binding
    • Template Bindings
    • Output Binding
    • Component and Directive Life Cycle Hooks
    • Service Life Cycle Hooks
  • Suggested Extensions under @ngRx/component Package
    • Push Pipe
    • Let Structural Directive
    • Observable Life Cycle Hooks
      • [electChanges RxJS Operator
    • Observable Input Bindings
    • Observable Output Bindings
    • Local State Management
      • selectSlices RxJS Operator
  • How We Teach This
  • Drawbacks
  • Alternatives

Summary

This RFC proposes a nice reactive integration of components/directives in Angular.
It's main goal is to provide a set of primitives that serve as the glue between custom reactive code and the framework.

Motivation

Parts of Angular like the ReactiveFormsModule, RouterModule, HttpClientModule etc. are already reactive.
And especially when composing them together we see the benefit of observables. i.e. http composed with router params.
For those who prefer imperative code, it's little effort to restrict it to a single subscription.

On the other hand for those who prefer reactive code, it's not that easy.
A lot of conveniences is missing, and beside the async pipe there is pretty much nothing there to take away the manual mapping to observables.
Furthermore, an increasing number of packages start to be fully observable based.
A very popular and widely used example is ngRx/store. It enables us to maintain global push-based state management based on observables.
Also, other well-known libraries, angular material provide a reactive way of usage.

This creates even more interest and for so-called reactive primitives for the Angular framework, like the async and other template syntax, decorators and services.

The first step would be to give an overview of the needs and a suggested a set of extensions to make it more convenient to work in a reactive architecture.
In the second step, We will show the best usage and common problems in a fully reactive architecture.

This proposal

  • give an overview of the needs

  • suggests a set of extensions to make it more convenient to work reactive with angular components

Overview

As the main requirement for a reactive architecture in current component-oriented
frameworks are handling properties and events of components as well as several specifics for
rendering and composition of observables.

In angular, we have an equivalent to properties and events, input and output bindings_.
But we also have several other options available to interact with components.

The goal is to list all features in angular that need a better integration.
We cover an imperative as well as a reactive approach for each option.

We consider the following decorators:

  • Input Decorator
  • Output Decorator
  • HostListener Decorator
  • HostBinding Decorator

And consider the following bindings:

  • Input Binding
  • Output Binding

Input Decorator

Inside of a component or directive we can connect properties with the components in it bindings over the @Input() decorator.
This enables us to access the values of the incoming in the component.

Receive property values over @Input('state')

Imperative approach:

@Component({
  selector: 'app-child',
  template: `<p>State: {{state | json}}</p>`
})
export class ChildComponent  {
  @Input() state;
}

Reactive approach:

Here we have to consider to cache the latest value from state-input binding.
As changes fires before AfterViewInit, we normally would lose the first value sent. Using some caching mechanism prevents this.
Furthermore and most importantly this makes it independent from the lifecycle hooks.

@Component({
  selector: 'app-child',
  template: `<p>State: {{state$ | async | json}}</p>`
})
export class ChildComponent  {
  state$ = new ReplaySubject(1);
  @Input() 
  set state(v) {
      this.state$.next(v);
  };
}

Needs:

Some decorator that automates the boilerplate of settings up the subject and connection it with the property.
Here ReplaySubject is critical because of the life cycle hooks.
@Input is fired first on OnChange where the first moment where the view is ready would be AfterViewInit

Boilerplate Automation
For every binding following steps could be automated:

  • setting up a Subject
  • hooking into the setter of the input binding and .next() the incoming value

Early Producer
All input bindings are so-called "early producer". A cache mechanism is needed as followed:

  • Use a ReplaySubject with bufferSize of 1 to emit notifications

Output Decorator

Send event over eventEmitter.emit(42)

Inside of a component or directive, we can connect events with the components output bindings over the @Output() decorator.
This enables us to emit values to its parent component.

Imperative approach:

@Component({
  selector: 'app-child',
  template: `<button (click)="onClick($event)">Btn</button>`
})
export class ChildComponent  {
  @Output()
  clickEmitter = new EventEmitter();
 
  onClick(e) {
    this.clickEmitter.next(e.timeStamp); 
  }
}

Reactive approach:

Here we change 2 things.
We use a Subject to retrieve the button click event and
provide an observable instead of an EventEmitter for @output().

@Component({
  selector: 'app-child',
  template: `<button (click)="clickEmitter.next($event)">Btn</button>`
})
export class ChildComponent  {
  btnClick = new Subject();
  
  @Output()
  clickEmitter = this.btnClick
    .pipe(
      map(e => e.timeStamp)
    );
}

Needs:
No need for an extension.

No need for custom extensions
Due to the fact that we can also provide an Observable as EventEmitters there is no need for as extension


HostListener Decorator

Receive event from the host over @HostListener('click', ['$event'])

Inside of a component or directive, we can connect host events with a component method over the @HostListener() decorator.
This enables us to retrieve the host's events.

Imperative approach:

@Component({
  selector: 'app-child',
  template: `<p>Num: {{num}}</p>`
})
export class ChildComponent  {
  num = 0;
  @HostListener('click', ['$event'])
  onClick(e) {
    this.num = ++this.num;
  }
}

Reactive approach:

@Component({
  selector: 'app-child',
  template: `<p>Num: {{num$ | async}}</p>`
})
export class ChildComponent  {
  numSubj = new Subject();
  num$ = this.numSubj.pipe(scan(a => ++a));

  @HostListener('click', ['$event'])
  onCllick(e) {
    this.numSubj.next(e);
  }
}

Needs:

We would need a decorator automates the boilerplate of the Subject creation and connect it with the property.
As subscriptions can occur earlier than the Host could send a value we speak about "early subscribers".
This problem can be solved as the subject is created in with instance construction.

Boilerplate Automation
For every binding following steps could be automated:

  • setting up a Subject
  • hooking into the setter of the input binding and .next() the incoming value

Early Producer
Make sure the created Subject it present early enough


HostBinding Decorator

Receive property changes from the host over @HostBinding('class')

Inside of a component or directive, we can connect the DOM attribute as from the host with the component property.
Angular automatically updates the host element over change detection.
In this way, we can retrieve the host's properties changes.

Imperative approach:

@Component({
  selector: 'app-child',
  template: `<p>color: {{className}}</p>`,
})
export class ChildComponent  {
  className = 'visible';
  
  @HostBinding('class')
  get background() {
   return this.className;
  }
}

Reactive approach:

TBD

Needs:

Provide an observable instead of a function.

Here again, we would need a decorator that automates the Subject creation and connection.
As subscriptions can occur earlier than the Host could be ready we speak about "early subscribers".
This problem can be solved as the subject is created in with instance construction.

Boilerplate Automation
For every binding following steps could be automated:

  • setting up a Subject
  • hooking into the setter of the input binding and .next() the incoming value

Early Subscribers
Make sure the created Subject it present early enough


Input Binding

Send value changes to child compoent input [state]="state"

In the parent component, we can connect component properties to the child
component inputs over specific template syntax, the square brackets [state].
Angular automatically updates the child component over change detection.
In this way, we can send component properties changes.

Imperative approach:

@Component({
  selector: 'my-app',
  template: `
    <app-child [state]="state"></app-child>
  `
})
export class AppComponent  {
  state = 42;
}

Reactive approach:

Important to say is that with this case we can ignore the life cycle hooks as the subscription happens always right in time.
We cal rely on trust that subscription to state$ happens after AfterViewInit.

Inconsistent handling of undefined variables
It is important to mention the inconsistent handling of undefined variables and observables that didn't send a value yet.

@Component({
  selector: 'my-app',
  template: `
    <app-child [state]="state$ | async"></app-child>
  `
})
export class AppComponent  {
  state$ = of(42);
}

Needs:
As we know exactly when changes happen we can trigger change detection manually. Knowing the advantages of subscriptions over the template and lifecycle hooks the solution should be similar to async pipe.

NgZone could be detached
As all changes can get detected we could detach the pipe from the ChangeDetection and trigger it on every value change

Performance optimisations

  • consider scheduling over AnimationFrameScheduler the output is always for the view

Implement strict and consistent handling of undefined for pipes
A pipe similar to async that should act as follows:

  • when initially passed undefined the pipe should forward undefined as value as on value ever was emitted
  • when initially passed null the pipe should forward null as value as on value ever was emitted
  • when initially passed of(undefined) the pipe should forward undefined as value as undefined was emitted
  • when initially passed of(null) the pipe should forward null as value as null was emitted
  • when initially passed EMPTY the pipe should forward undefined as value as on value ever was emitted
  • when initially passed NEVER the pipe should forward undefined as value as on value ever was emitted
  • when reassigned a new Observable the pipe should forward undefined as value as no value was emitted from the new
  • when completed the pipe should keep the last value in the view until reassigned another observable
  • when sending a value the pipe should forward the value without changing it

Already existing similar packages:


Template Bindings

In the following, we try to explore the different needs when working with observables in the view.

Lets examen different situations when binding observables to the view and see how the template syntax that Angular already provides solves this. Let's start with a simple example.

Multiple usages of async pipe
Here we have to use the async pipe twice. This leads to a polluted template and introduces another problem with subscriptions.
As observables are mostly unicasted we would receive 2 different values, one for each subscription.
This pushes more complexity into the component code because we have to make sure the observable is multicasted.

@Component({
  selector: 'my-app',
  template: `
    {{random$ | async}}
    <comp-b [value]="random$ | async">
    </comp-b>
  `})
export class AppComponent  {
  random$ = interval(1000)
    .pipe(
      map(_ => Math.random()),
      // needed to be multicasted
      share()
    );
}

Binding over the as syntax
To avoid such scenarios we could use the as syntax to bind the observable
to a variable and use this variable multiple times instead of using the async pipe multiple times.

@Component({
  selector: 'my-app',
  template: `
    <ng-container *ngIf="random$ | async as random">
        {{random}}
        <comp-b [value]="random">
        </comp-b>
    </ng-container>
  `})
export class AppComponent  {
  random$ = interval(1000)
    .pipe(
      map(_ => Math.random())
    );
}

Binding over the let syntax
Another way to avoid multiple usages of the async pipe is the let syntax to bind the observable to a variable.

@Component({
  selector: 'my-app',
  template: `
    <ng-container *ngIf="random$ | async; let random = ngIf">
        {{random}}
        <comp-b [value]="random">
        </comp-b>
    </ng-container>
  `})
export class AppComponent  {
  random$ = interval(1000)
    .pipe(
      map(_ => Math.random())
    );
}

Both ways misuse the *ngIf directive to introduce a context variable and not to display or hide a part of the template.
This comes with several downsides:

  • we lose the meaning of the *ngIf directive
  • the functionality of hiding displaying itself.
    The *ngIf directive is triggered be falsy values, but we don't want to conditionally show or hiding content,
    but just introduce a context variable. This could lead to problems in several situations.
  • The functionality of subscribing has to be done separately over the async pipe

*ngIf directive triggered by falsy values

@Component({
  selector: 'my-app',
  template: `
    <ng-container *ngIf="random$ | async as random">
        {{random}}
        <comp-b [value]="random">
        </comp-b>
    </ng-container>
  `})
export class AppComponent  {
  random$ = interval(1000)
    .pipe(
      map(_ => Math.random() > 0.5 ? 1 : 0)
    );
}

As we can see, in this example the ng-container would only be visible if the value is 1 and therefore truthy.
All falsy values like 0 would be hidden. This is a problem in some situations.

We could try to use *ngFor to avoid this.

Context variable over the *ngFor directive

@Component({
  selector: 'my-app',
  template: `
    <ng-container *ngFor="let random of [random$ | async]">
        {{random}}
        <comp-b [value]="random">
        </comp-b>
    </ng-container>
  `})
export class AppComponent  {
  random$ = interval(1000)
    .pipe(
      map(_ => Math.random() > 0.5 ? 1 : 0)
    );
}

By using *ngFor to create a context variable we avoid the problem with *ngIf and falsy values.
But we still misuse a directive. Additionally *ngFor is less performant than *ngIf.

Nested ng-container problem

@Component({
  selector: 'my-app',
  template: `
  <ng-container *ngIf="observable1$ | async as color">
    <ng-container *ngIf="observable2$ | async as shape">
      <ng-container *ngIf="observable3$ | async as name">
        {{color}}-{{shape}}-{{name}}
        <app-color [color]="color" [shape]="shape" [name]="name">
        </app-color>
       </ng-container>
     <ng-container>
  </ng-container>
  `})
export class AppComponent  {
  observable1$ = interval(1000);
  observable2$ = interval(1500);
  observable3$ = interval(2000);
}

Here we nest ng-container which is a useless template code.
A solution could be to compose an object out of the individual observables.
This can be done in the view or the component.

Composing Object in the View

@Component({
  selector: 'my-app',
  template: `
  <ng-container
    *ngIf="{
      color: observable1$ | async,
      shape: observable2$ | async,
      name:  observable3$ | async
    } as c">
    {{color}}-{{shape}}-{{name}}
    <app-other-thing [color]="c.color" [shape]="c.shape" [name]="c.name">
    </app-other-thing>
  </ng-container>
  `})
export class AppComponent  {
  observable1$ = interval(1000);
  observable2$ = interval(1500);
  observable3$ = interval(2000);
}

Here we can use *ngIf again because and object is always truthy. However, the downside here is
we have to use the async pipe for each observable. `Furthermore we have less control over the single observables.
A better way would be to move the composition into the template and only export final compositions to the template.

Composition in the Component

@Component({
  selector: 'my-app',
  template: `
  <ng-container *ngIf="composition$ | async as c">
    {{color}}-{{shape}}-{{name}}
    <app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
    </app-color>
  </ng-container>
  `})
export class AppComponent  {
  observable1$ = interval(1000);
  observable2$ = interval(1500);
  observable3$ = interval(2000);

  composition$ = combineLatest(
    this.observable1$.pipe(startWith(null), distinctUntilChanged()),
    this.observable2$.pipe(startWith(null), distinctUntilChanged()),
    this.observable3$.pipe(startWith(null), distinctUntilChanged()),
    (color, shape, name) => ({color, shape, name})
  )
  .pipe(
    share()
  );

}

As we see in this example in the component we have full control over the composition.

Needs:
We need a directive that just defines a context variable without any interaction of the actual dom structure.
The syntax should be simple and short like the as syntax. It should take over basic performance optimizations.
Also, the consistent handling of null and undefined should be handled.

Implement more convenient binding syntax
To improve usability we should fulfill the following:

  • the context should be always present. *ngIf="{}" would do that already
  • avoid multiple usages of the `async pipe
  • move subscription handling in the directive
  • better control over the context. Maybe we could get rid of the as as variable??
  • implement an internal layer to handle null vs undefined etc
  • implement the option to put additional logic for complete and error of an observable

Basic performance optimisations

  • consider scheduling over AnimationFrameScheduler the output is always for the view
  • handling changes could be done programmatically. Good for running zone-less

Implement strict and consistent handling of null/undefined for the bound value
Please visit the section Input Binding for a full list of requirements

Already existing similar packages:


Output Binding

Receive events from child component over (stateChange)="fn($event)"

In the parent component, we can receive events from child components over specific template syntax, the round brackets (stateChange).
Angular automatically updates fires the provides function over change detection.
In this way, we can receive component events.

Imperative approach:

@Component({
  selector: 'my-app',
  template: `
    state: {{state}}
    <app-child (stateChange)="onStateChange($event)"></app-child>
  `
})
export class AppComponent  {
  state;
  onStateChange(e) {
    this.state = e; 
  }
}

Reactive approach:

@Component({
  selector: 'my-app',
  template: `
    state: {{state$ | async}}<br>
    <app-child (stateChange)="state$.next($event)"></app-child>
  `
})
export class AppComponent  {
  state$ = new Subject();
}

Needs:
As it is minimal overhead we can stick with creating a Subject on our own.

No need for custom extensions
Due to the fact of the minimal overhead and the resources of creating a custom Decorator for it there no need for as extension


Component and Directive Life Cycle Hooks

As the component's logic can partially rely on the components life cycle hooks we also need to consider the in-out evaluation.

Angular fires a variety of lifecycle hooks. Some of them a single time some of them only once a components lifetime.

Angulars life cycle hooks are listed ere in order:
(Here the Interface name is used. The implemented method starts with the prefix 'ng')

  • OnChanges (ongoing, transports changes)
  • OnInit (single shot)
  • DoCheck (ongoing)
  • AfterContentInit (single shot)
  • AfterContentChecked (ongoing)
  • AfterViewInit (single shot)
  • AfterViewChecked (ongoing)
  • OnDestroy (single shot)

The goal here is to find a unified way to have single shot, as well as ongoing life cycle hooks, and observable.

Imperative approach:

@Component({
  selector: 'app-child',
  template: `<p>change: {{changes | json}}</p>`
})
export class ChildComponent implements OnChanges {
   @Input()
   state;

   changes;

  ngOnChanges(changes) {
    this.changes= changes;
  }
}

Reactive approach:
As above mentioned in section Input Decorator we replay the latest value to avoid timing issues related to life cycle hooks.

@Component({
  selector: 'app-child',
  template: `<p>change: {{changes$ | async | json}}</p>`
})
export class ChildComponent implements OnChanges {
  @Input() state;
   
  onChanges$ = new ReplaySubject(1);
   
  changes$ = this.onChanges$
      .pipe(map(changes => changes));

  ngOnChanges(changes) {
    this.onChanges$.next(changes);
  }
}

Handle general things for hooks:

Following things need to be done for every lifecycle hook:

  • every life cycle replays the last value and completion
  • errors are swallowed and complete is returned instead
  • every hook should be tied to the lifecycle of the component
@Component({
  selector: 'app-child',
  template: `<p>change: {{changes$ | async | json}}</p>`
})
export class ChildComponent implements OnChanges {
  @Input() state;
   
  onDestroy$$ = new ReplaySubject(1);
  onDestroy$ = this.onDestroy$$.pipe(catchError(e => EMPTY));
  
  onChanges$$ = new ReplaySubject(1);
  onChanges$ = this.onChanges$$.pipe(catchError(e => EMPTY), takeUntil(this.onDestroy$));
  
   
  ngOnChanges(changes) {
    this.onChanges$.next(changes);
  }
  
  ngOnDestroy(changes) {
    this.onDestroy$.next(changes);
  }
}

Handle hook specific stuff:

To handle the differences in lifecycle hooks we follow the following rules:

  • single shot life cycle hooks complete after their first emission
  • single shot life cycle hooks swallow errors and emit the last void
  • on-going life cycle hooks just complete on error
@Component({
  selector: 'app-child',
  template: `<p>change: {{changes$ | async | json}}</p>`
})
export class ChildComponent implements OnChanges {
  @Input() state;
  
  const singleShotOperators = pipe(
    take(1),
    catchError(e => of(void)),
    takeUntil(this.onDestroy$)
  );
  const ongoingOperators = pipe(
    catchError(e => EMPTY),
    takeUntil(this.onDestroy$)
  );
  
  onChanges$ = this.onChanges$$.pipe(this.ongoingOperators);
  onInit$ = this.onInit$$.pipe(this.singleShotOperators);
  doCheck$ = this.doCheck$$.pipe(this.ongoingOperators);
  afterContentInit$ = this.afterContentInit$$.pipe(this.singleShotOperators);
  afterContentChecked$ = this.afterContentChecked$$.pipe(this.ongoingOperators);
  afterViewInit$ = this.afterViewInit$$.pipe(this.singleShotOperators);
  afterViewChecked$ = this.afterViewChecked$$.pipe(this.ongoingOperators);
  onDestroy$ = this.onDestroy$$.pipe(take(1));
   
  ngOnChanges(changes) {
    this.onChanges$.next(changes);
  }
  
  ngOnDestroy(changes) {
    this.onDestroy$.next(changes);
  }
}

Needs
We need a decorator to automates the boilerplate of the Subject creation and connect it with the property away.

Also subscriptions can occur earlier than the Host could send a value we speak about "early subscribers".
This problem can be solved as the subject is created in with instance construction.

Boilerplate Automation
For every binding following steps could be automated:

  • setting up a Subject
  • hooking into the setter of the input binding and .next() the incoming value
  • hiding observer methods form external usage

Respect Lifetime and State of Lifecycles

  • subscription handling tied to component lifetime
  • single shot observables complete after their first call

Late Subscribers

  • As subscriptions could happen before values are present (subscribing to OnInit in the constructor)
    we have to make sure the Subject is created early enough for all life cycle hooks
  • on subscription to already completed observable of a lifecycle it should return the last event and complete again.

Service Life Cycle Hooks

In general, services are global or even when lazy-loaded the are not unregistered at some point in time.
The only exception is Services in the Components providers
Their parts of the services logic could rely on the life of the service, which is exactly the lifetime of the component.

Angular for such scenarios angular provides the OnDestroy life cycle hook for classes decorated with @Injectable.

The goal here is to find a unified way to have the services OnDestroy life cycle hooks as observable.

Imperative approach:

@Component({
  selector: 'app-child',
  template: ``,
  providers: [LocalProvidedService]
})
export class ChildComponent implements OnChanges {
  constructor(private s: LocalProvidedService) {
  }
}

export class LocalProvidedService implements OnDestroy {
  
  constructor() {
  }

  ngOnDestroy(changes) {
    console.log('LocalProvidedService OnDestroy');
  }
}

Reactive approach:

@Component({
  selector: 'app-child',
  template: ``,
  providers: [LocalProvidedService]
})
export class ChildComponent implements OnChanges {
  constructor(private s: LocalProvidedService) {
  }
}
@Injctable({
  providedIn: 'root'
})
export class LocalProvidedService implements OnDestroy {
  onDestroy$ = new Subject();
   
  constructor() {
     this.onDestroy$subscribe(_ => console.log('LocalProvidedService OnDestroy');)
  }

  ngOnDestroy(changes) {
    this.onDestroy$.next();
  }
}

Needs
We need a decorator to automates the boilerplate of the Subject creation and connect it with the property away.

Boilerplate Automation
For every binding following steps could be automated:

  • setting up a Subject
  • hooking into the setter of the input binding and .next() the incoming value
  • we should NOT override but EXTEND the potentially already existing functions

Suggested Extensions under @ngRx/component Package

We propose adding an additional package to ngRx to support a better reactive experience in components.

We will manage releases of these packages in three phases:

  • discussion phase: Based on this RFC we will figure out the needs and possible solutions
  • discovery phase: A set of POC's and releases under experimental will help to have first working drafts
    • here I would go with view relate parts first as they are smaller in scope
    • then i would move forward with component internal parts like @Input()
  • stabilization: once we have a complete set of the packages, its parts and their features stabilized, we can go for the final API design.

Based on the above listing and their needs we suggest a set of Angular extensions that should make it easier to set up a fully reactive architecture.


Extensions suggested:

  • Push Pipe
  • Let Structural Directive
  • Observable Life Cycle Hooks
    • selectChange RxJS Operator
  • Observable Input Bindings
  • Observable Output Bindings
  • Observable Host Bindings
  • Observable Host Listener

Push Pipe

An angular pipe similar to the async pipe but triggers detectChanges instead of markForCheck.
This is required to run zone-less. We render on every pushed message.
(currently, there is an isssue with the ChangeDetectorRef in ivy so we have to wait for the fix.

The pipe should work as template binding {{thing$ | push}}
as well as input binding [color]="thing$ | push" and trigger the changes of the host component.

<div *ngIf="(thing$ | push) as thing">
  color: {{thing.color}}
  shape: {{thing.shape}}
<div>

<app-color [color]="(thing$ | push).color">
</app-color>

Included Features:

  • subscription handling overview life cycle
  • a unified way of handling null and undefined with streams
  • optional flag to turn off scheduling over AnimationFrameScheduler (on by default)
  • change detection is done manually which allows it to work zone-less too

Let Structural Directive

The *let directive serves a convenient way of binding multiple observables in the same view context.
It also helps with several default processing under the hood.

The current way of handling subscriptions in the view looks like that:

<ng-container *ngIf="observable1$ | async as c">
  <app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
  </app-color>  
</ng-container>

The *let directive take over several things and makes it more convenient and save to work with streams in the template
*let="{o: o$, t: t$} as s;"

<!-- observables = { color: observable1$, shape: observable2$, name:  observable3$ } -->

<ng-container *let="observable as c">
  <app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
  </app-color>
</ng-container>

<ng-container *let="observable; let c">
  <app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
  </app-color>
</ng-container>

<ng-container *let="observable; color as c; shape as s; name as n">
  <app-color [color]="c" [shape]="s" [name]="n">
  </app-color>
</ng-container>

Included Features:

  • binding is always present. (*ngIf="{}" normally effects it)
  • it takes away the multiple usages of the async pipe
  • propper handling of null and undefined values
  • removes state slices if bound observable completes or errors
  • an option to disable scheduling over AnimationFrameScheduler (on by default)
  • control change detection and therefore can run zone-less

Observable Life Cycle Hooks

A thing which turns a lifecycle method into an observable and assigns it to the related property.

The thing should work as a proxy for all life cycle hooks
as well as forward passed values i.e. changes coming from the OnChanges hook.

 onInit$; // ??? very elegant and intuitive way to get an observable from a life-cycle hook
 onDestroy$;  // ??? very elegant and intuitive way to get an observable from a life-cycle hook

  this.onInit$
    .pipe(
      switchMapTo(interval(1000)),
      map(_ => Date.now()),
      takeUntil(this.onDestroy$)
    )
    .subscribe();

Included Features

  • it handles late subscribers.
  • exposes only observables
  • respects single shot vs ongoing life-cycles
  • subscription handling over the component lifetime
  • return the latest value when resubscribing

selectChanges RxJS Operator

An operators selectChanges to select one or many specific slices from SimpleChange.
This operator can be used in combination with onChanges$.

It also provides a very early option to control the forwarded values.

Example of selectSlice operator

export class MyComponent {
 // ??? very elegant and intuitive way to get an observable from a life-cycle hook
  onChanges$: Observable<SimpleChanges>;

  @Input() state;
  state$ = this.onChanges$.pipe(getChange('state'));
}

Following things are done under the hood:

  • pull out currentValue from SimpleChanges object
  • optional it could have a parma for a custom comparison function

Observable Input Bindings

A property decorator which turns component or directive input binding into an observable and assigned it to the related property.

@Component({
  selector: 'app-child',
  template: `<p>input: {{input$ | async}}</p>`,
})
export class ChildComponent  {
  // ??? very elegant and intuitive way to get an observable from a life-cycle hook
  input$;
}

Following things are done under the hood:

  • It caches to consider late subscribers (life cycle hook related)
  • It is multicasted to avoid multiple subscriptions
  • It works with WebComponents and AngularComponents

Observable Output Bindings

A property decorator which turns a view event into an observable and assigns it to the related property.

The solution should work do most of his work in the component itself.
Only a small piece in the template should be needed to link the view with the component property.

@Component({
  selector: 'app-child',
  template: `<button>clicks: {{count$ | async}}</button>`,
})
export class ChildComponent  {
   // ??? very elegant and intuitive way to get an observable from a life-cycle hook
  click$;
  
  count$ = this.click$.pipe(scan(a => ++a, 0));
}

Following things are done under the hood:

  • It makes it possible to subscribe to the property even before the view is rendered
  • It is multicasted to avoid multiple subscriptions
  • It works with DomElements, WebComponents, and AngularComponents

Here a link to a similar already existing ideas from @elmd_:
https://www.npmjs.com/package/@typebytes/ngx-template-streams

How We Teach This

The most important message we need to teach developers is the basic usage of the new primitives.
The rest is up to pure RxJS knowledge.

Drawbacks

This new library carries:

  • potential dependencies to angular packages
  • support load of docs
  • issue triage
  • design reviews
  • pull request reviews

Alternatives

  • We could wait until angular implements it :)
@evgenyfedorenko
Copy link
Contributor

Is this going to be extending angular component implementation? Not completely follow.

@Tibing
Copy link

Tibing commented Aug 8, 2019

Thanks for sharing this RFC, I really like your ideas 😄

@BioPhoton
Copy link
Contributor Author

@Tibing what exactly? Is there a feature that is missing?

@Tibing
Copy link

Tibing commented Aug 8, 2019

@BioPhoton, I mean I like your idea to make Angular truly reactive. I still didn't get too deep to the RFC, whereas from the first sight, I have the following questions:

  • @HostBinding. You stated that reactive approach as TBD, but, do you have currently any ideas on how to do it? I mean, maybe you already have some drafts but don't want to share them? 😄
  • @hook$ I don't like the idea of having one decorator with the lifecycle hook name parameter. I would personally prefer to have multiple decorators. That approach will allow us to avoid mistyping and have autocompletion in IDE (Yep, I know we can make it strictly typed even with strings but anyway). Also, it'll lead to more concise code:
  @OnChanges$() onChanges$: Observable<SimpleChanges>;
  • getChanges I would personally prefer to have getChange accepting a selector function instead of a string (or we can have an overload, and it can do both). Because it could be useful in cases when you need to select more that one simple change at a time.
  • selectChanges operator. You stated that it has to do the following under the hood:

pull out currentValue from SimpleChanges object

But why do we need it if we have Observable inputs? As I see it properly, Observable inputs have to fire before onChanges$. I think I didn't get something 🙃

  • @FromView$ I don't like the semantics of that decorator the same way as I don't like to use @ViewChild() which selects an HTML element by id. Frankly speaking, I have no idea what to do with that. But, maybe, an idea will come in your mind.

As I stated before, I didn't get in your RFC too deep, so, please, don't treat my comments too serious. I'm going to play with the reactive approach in Angular during the weekend and then I'll be able to add more constructive comments.

@ValentinFunk
Copy link

This is a really great idea! These things would make it frictionless to work with Observables properly in Angular, it's always a pain to write so much boilerplate when it would be so natural to use observables (@input(), ngOnChanges practically screams Observable value to me).

Some previous discussion on the input topic can be found here as well: angular/angular#5689

@lVlyke
Copy link

lVlyke commented Aug 10, 2019

+1, these are great ideas that definitely make reactive programming easier with Angular.

Since it's highly related, I also want to give a shout-out to my own library @lithiumjs/angular that implements many of these ideas through new decorators that can be applied to component properties, inputs, outputs, host listeners, lifecycle events, etc.

@only2dhir
Copy link

A great idea indeed!

@bryanrideshark
Copy link

What if you just start with the directives? I feel like multiple packages might be better than a single, large package containing everything.

We actually forgo ngOnChanges entirely now - at my workplace we use the following directives to hook up @input() to a BehaviorSubject:


@ObservablePropertyAccessor()
_sampleProp$ = new BehaviorSubject<number>(undefined);
@Input()
sampleProp: number;

The decorator essentially adds a getter / setter which allow reading / writing to the underlying BehaviorSubject.

The above code desugars to:



_sampleProp$ = new BehaviorSubject<number>(undefined);
@Input()
get sampleProp(): number { return this._sampleProp$.value; }
set sampleProp (value: number) { this._sampleProp$.next(value); }

We'd definitely use something which provided improvements to the templates. However, the decorators for lifecycle hooks might prove to be superfluous.

@BioPhoton
Copy link
Contributor Author

@Tibing

@HostBinding. You stated that reactive approach as TBD, but, do you have currently any ideas on
how to do it? I mean, maybe you already have some drafts but don't want to share them? 😄
No ATM I didn't investigate in int.
@hook$ I don't like the idea of having one decorator with the lifecycle hook name parameter. I
would personally prefer to have multiple decorators. That approach will allow us to avoid
mistyping and have autocompletion in IDE (Yep, I know we can make it strictly typed even with
strings but anyway). Also, it'll lead to more concise code:
@OnChanges$() onChanges$: Observable;
getChanges I would personally prefer to have getChange accepting a selector function instead of
a string (or we can have an overload, and it can do both). Because it could be useful in cases when
you need to select more that one simple change at a time.

I also like the idea of having one decorator per hook.

But why do we need it if we have Observable inputs?

We may need lifecycle hooks in addition to observable input bindings

@FromView$ I don't like the semantics of that decorator the same way as I don't like to use
@ViewChild() which selects an HTML element by id. Frankly speaking, I have no idea what to do
with that. But, maybe, an idea will come in your mind.

The goal here is to get observables from the view. Button clicks, inputs, any dom events, any angular event binding i.e. (click)

@BioPhoton
Copy link
Contributor Author

@bryanrideshark

What if you just start with the directives? I feel like multiple packages might be better than a single, large package containing everything.

The package contains only stuff for components/directives

We actually forgo ngOnChanges

Actually all lifacycles hooks are considered under section "Component and Directive Life Cycle Hooks"

at my workplace we use the following directives

@ObservablePropertyAccessor()
_sampleProp$ = new BehaviorSubject<number>(undefined);
@Input()
sampleProp: number;

2 things I can say here:
A) you should use a ReplaySubject(1) instead of a BehaviorSubject(initValue).
You don't need an initial, but just the latest value.
B) pulling out a value form an Observable over .value or having any getter function that returns a value or a new Observable is definitely wrong. We just want to subscribe to Observables.
We want to avoid imperative programming

@BioPhoton
Copy link
Contributor Author

I would be interested in feedback on 2 extensions:

  • push pipe
  • *ngrxLet directive

Here a review of the suggested features, as well as general feedback, would be nice. :)

@LayZeeDK
Copy link
Contributor

LayZeeDK commented Aug 11, 2019

I know that the NgRx team will agree that we should not add additional decorators. Angular uses decorators heavily, but they are removed at compile time, since they are only used to instruct the compiler, create injectors, and so on.

Decorators is still a non-standard feature. It might never make it into ECMAScript. Even if it does, syntax and semantics might change.

Several of the use cases you suggest should be doable using Ivy features such as this fromStore feature I created as a proof of concept.

@LayZeeDK
Copy link
Contributor

LayZeeDK commented Aug 11, 2019

@BioPhoton For the push pipe, we should make sure not to trigger/queue multiple change detection cycles from the same change detection cycle. Ivy's markForCheck does this.

Meaning, if two components depend on the same observable and a new value is emitted, only a single change detection cycle should occur right after the value emission as a result of the push pipe.

This could be done by calling ApplicationRef#tick exactly once after every change detection cycle where ChangeDetectorRef#markForCheck was called by push pipe.

@vpashkov
Copy link

@BioPhoton in the "Output Decorator" section do you mean btnClick.next($event) instead of
clickEmitter.next($event)?

@MikeRyanDev
Copy link
Member

@LayZeeDK This would only be for Ivy applications and it would be using markForCheck under the hood. We should be getting batching/scheduling for free as a result.

@MikeRyanDev
Copy link
Member

A high level constraint I'd like to place on the RFC: we should avoid decorators as much as possible. You can't type check them and they break lazy loading. If we have to use decorators for some of these APIs then we should take a similar approach to the AOT compiler or ngx-template-streams: apply code transforms to de-sugar them and strip the decorators.

@wesleygrimes
Copy link
Contributor

Nice work @BioPhoton! Just had sometime to read through this.

I am sure more thoughts will come to me as time goes on and I digest more, but the first thought I am having is that I would love to see a take on the lifecycle hooks that doesn’t require a decorator.

For example, a more functional solution like:

‘onInit(()=>{...});’ etc...

These could be wired up in the constructor of the component.

This could gain inspiration from the new Vue 3.0 proposal.

Thoughts? Is it even technically possible?

@BioPhoton
Copy link
Contributor Author

@wesleygrimes, as far as I know, it is possible. However, setting up everything al the time in the CTOR is something repetitive that I try to avoid.

I guess we should investigate in the suggestion from @MikeRyanDev here

@MikeRyanDev regarding the change-detection, It should have a flag to detach CD and trigger it manually (zone-less), but by default, it should work as the async pipe.

@wesleygrimes
Copy link
Contributor

@wesleygrimes, as far as I know, it is possible. However, setting up everything al the time in the CTOR is something repetitive that I try to avoid.

I guess we should investigate in the suggestion from @MikeRyanDev here

@MikeRyanDev regarding the change-detection, It should have a flag to detach CD and trigger it manually (zone-less), but by default, it should work as the async pipe.

Agreed on the CTOR part, could just be a class field initialized with onInit$ = onInit(() => {...})

@bryanrideshark
Copy link

I've come back to this because I wanted the *let structural directive to exist, but alas, it does not at this time.

I still feel like there are a lot of wins to be had, just by releasing things piecemeal.

Is there an initial proof of concept for the *let directive? If there is, I wouldn't mind being able to see it.

@BioPhoton
Copy link
Contributor Author

Hi @bryanrideshark

The let directive is more or less ready. We have to clarity some open questions etc..

The poc is in the component branch.

Let's me know what you think about the context infos of the stream (error, complete)

@dummdidumm
Copy link
Contributor

I think the concept is great. The only thing I don't understand: Why make this part of ngrx? I feel this is something that is not related to ngrx in any way apart from "they both use rxjs heavily". On the other hand I understand that in this repo you will find more feedback/activity/attention.

@wesleygrimes
Copy link
Contributor

Hi @dummdidumm, from the start NgRx has always been about more than just state management. We are a platform for reactive technologies. Ng = Angular + Rx = Reactive. With this new addition we are continuing with that trend to expand our platform even more. We want to provide reactive ways for angular applications to be constructed.

@stupidawesome
Copy link

stupidawesome commented Sep 22, 2019

I created NgObservable as a way to tackle most of these issues within with current compiler constraints.

The suggestions I make below however also include hypothetical APIs that would require changes to Angular itself.

Input Decorator

Inputs should not be turned into observables. It's better to use the ngOnChanges lifecycle hook. Angular already gives you a lot of information here (current and previous value of keys that changes), so to get an observable stream of a particular value you can filter the ngOnChanges stream to select the value you want.

interface Props {
    title: string
}

@Component()
class Component extends NgObservable implements Props {
    @Input()
    public title: string

    constructor() {
        ngOnChanges<Props>(this).pipe(
            select((changes) => changes.title)
        ).subscribe((title) => {
            // strongly typed value
            console.log(title.currentValue)
        })
    }
}

With changes to the framework it shouldn't be necessary to extend a base class just to provide lifecycle hooks

interface Props {
    title: string
}

@Component({
    features: [OnChanges] // based on hostFeatures in Ivy
})
class Component implements Props {
    @Input()
    public title: string

    constructor() {
        ngOnChanges<Props>(this).pipe(
            select((changes) => changes.title)
        ).subscribe((title) => {
            // strongly typed value
            console.log(title.currentValue)
        })
    }
}

Similar to markDirty(), it should be possible to implement similar methods for each of the lifecycle hooks in Angular. You could do this without breaking or changing the existing imperative API.
ngOnChanges() in this example returns an observable that behaves exactly like the class method would. features: [OnChanges] is the suggested change to the @Component() decorator needed to tell
the compiler to include the code needed to run the lifecycle hook.

Output Decorator

No API change. Same as OP.

HostListener Decorator

Using an RxJS subject that can also be called like a function, it is possible to adapt some of the existing Angular APIs to turn them into observable streams without changing any of the framework code.

// Invoke subject signature
export interface InvokeSubject<T> extends Subject<T> {
    (next: T): void
    (...next: T extends Array<infer U> ? T : never[]): void
}

class Component {
    @HostListener("click", ["$event"])
    public listener = new InvokeSubject<Event>

    constructor() {
        this.listener.subscribe((event) => {
            console.log(event)
        })
    }
}

If changing the framework is feasible, this could be implemented as a hook too in a way that mirrors the fromEvent() operator from RxJS.

class Component {
    constructor() {
        hostListener(this, "click", ["$event"], { useCapture: true })subscribe((event) => {
            console.log(event)
        })
    }
}

HostBinding Decorator

No API Change. Host bindings are just part of the component snapshot which are updated on change detection runs anyway, so there's no need to make this an observable.

Input Binding

To perform change detection automatically, we need to know when the state of the component changes. The best way to do this is with a dedicated "State" subject. This subject would be to components what Router is to the Angular routes (we then treat the component instance as a "stateSnapshot").

Currently there's a bit of ceremony needed to achieve this using the library I developed.

@Component({
    providers: [StateFactory, Stream]
})
class Component extends NgObservable {
    @Input()
    title: string // the current or "snapshot" value

    constructor(@Self() stateFactory: StateFactory, @Self() stream: Stream) {
        const state = stateFactory.create(this)

        // imperative API
        // queues change detection to run next application tick()
        // works like React setState basically
        state.next({ title: "Angular" })

        // reactive API
        // automatically cleans up subscription when component destroyed
        stream(state)(ngOnChanges(this).pipe(
            select((changes) => changes.title),
            map((title) => ({ title }))
        ))

        // bonus: observe changes to entire component
        state.subscribe(snapshot => console.log(snapshot))
    }
}

With Angular Ivy and other framework changes this could be simplified.

@Component({
    features: [NgOnChanges]
})
class Component {
    @Input()
    title: string // the current or "snapshot" value

    constructor(stateFactory: StateFactory) {
        const state = stateFactory.create(this)
        state.next({ title: "Angular" })

        stream(state)(ngOnChanges(this).pipe(
            select((changes) => changes.title),
            map((title) => ({ title }))
        ))
        
        state.subscribe(snapshot => console.log(snapshot))
    }
}

Template Bindings

Remove all async logic from the template and everything becomes much simpler. Treat the component instance as a snapshot of the current state, then set good defaults and handle undefined behaviour with *ngIf or safe navigation operators ?.

There are {{totalActiveUsers}} online now.

<ng-container *ngIf="user">
    Name: {{user.firstName}}
    Surname: {{user.lastName}}
</ng-container>

Hi my name is {{user?.firstName}} {{user?.lastName}}
interface User {
    firstName: string
    lastName: string
}

@Component()
class Component {
    user: User
    totalActiveUsers: number

    constructor(userSvc: UserService, stateFactory: StateFactory) {
        const state = stateFactory.create(this)

        this.totalActiveUsers = 0
        this.user = null

        stream(state)({ user: userSvc.getCurrentUser(), totalActiveUsers: userSvc.getActiveUsersCount() })
    }
}

How this works is that whenever a new value is streamed to the state subject, state.next(partialValue) is called which then patches the values on the component instance (aka. the state snapshot), and then schedules change detection to update the template.

Output Binding

The same technique mentioned in HostListener can be used here as well.

@Component({
    template: `
        <app-child (stateChange)="onStateChange($event)"></app-child>
    `
})
class Component {
    onStateChange = new InvokeSubject<StateChange>

    constructor() {
        this.onStateChange.subscribe((stateChange) => console.log(stateChange))
    }
}

Component and Directive Life Cycle Hooks

These are currently implemented in NgObservable using a base class that implements all of the lifecycle hook methods and maps them to a subject. The "hook" methods then look for those subjects
on the component class instance to do their magic.

In Ivy and beyond, I think having feature flags is the best way to express our intent to use lifecycle hooks without explicitly putting them on the component class. This would require changes from
Angular's end.

See Input Decorator for what I mean.

Service Life Cycle Hooks

Services created at the module level could just inject a provider that registers a single call to ngOnDestroy or ngModuleRef.onDestroy(). Services created at the component level could benefit from
a ngOnDestroy() hook that works the same as with components, or should just be completely stateless (let the component clean up subscriptions).

Suggested Extensions under @ngRx/component Package

  • Observable Lifecycle Hooks
  • State Subject
  • Invoke Subject
  • Observable HostListener
  • Stream/Sink Utility

I think that more work should be done on Angular's side to enable the desired behaviour in a way that's performant and ergonomic without breaking or mutating existing APIs.

The NgObservable library was the best I could do given Angular's current compiler limitations, I hope this gives you some ideas.

@BioPhoton
Copy link
Contributor Author

Hi @stupidawesome!

Thanks, soo much for your feedback!

Input Decorator vs ngChanges:

Inputs should not be turned into observables. It's better to use the ngOnChanges lifecycle hook. > Angular already gives you a lot of information here (current and previous value of keys that
changes), so to get an observable stream of a particular value you can filter the ngOnChanges
stream to select the value you want.

In the section "selectChanges RxJS Operator" it is mentioned that this would be similar to input.
I proposed separate, very specific things because of 2 reasons:

  • easier to use for beginners
  • smaller production code as the rest can be tree-shaked away

The above document is here to speed up an old discussion and I hope to get some feedback from @robwormald in the next week(s).

Output Binding
I really like the InvokeSubject a lot because it also solves the HostListener thing with just a bit more code than nessacary for events from templates. This is really the best/smallest approach that I saw so far because it's just a function.

If I understand it correctly this is also smaller and way easier to implement than (ngx-template-streams)[https://github.com/typebytes/ngx-template-streams]. If true, I would go with this instead of the approach from @typebytes.

I think that more work should be done on Angular's side to enable the desired
behavior in a way that's performant and ergonomic without breaking or mutating existing APIs.

The NgObservable library was the best I could do given Angular's current compiler limitations,
I hope this gives you some ideas.

I think so too @stupidawesome!
I spent and still spend a lot of time on this topic and get some change.
This RFC is the outcome of many discussions. IMHO, unfortunately, Angular will not ship anything related to this topic in the near future.
(@robwormald please correct if wrong)

Service Life Cycle Hooks and State Subject
I explicitly excluded from this RFC but wrote a lot about it in another document are:

  • Local State Management
    • selectSlices RxJS Operator

This service helps to manage the component internal state (local state).

It does implement:

  • a setState(value) method (not that good but I guess it helps beginners)
  • a connectState(observable) method (this is how ngRx/store should word if you ask me)
    • automatic state-handling on error
    • automatic state-handling on complete
  • subscription handling (ngOnDestroy of service provided in component)
  • and other stuff...

We decided against pulling in this topic at the current state because it would just bring up too many discussions. Therefore I skipped all the information related to it here.

@dolanmiu
Copy link

dolanmiu commented Sep 25, 2019

I'm a little late to the party, but how about an API like this:

https://github.com/dolanmiu/sewers/blob/master/src/app/card/card.component.ts#L5-L8

Essentially it's a type safe decorator to handle all your Observables, called a "Sink"

@Sink<CardComponent>({
  obs: 'obs$',
  data: 'data$'
})
@Component({
  selector: 'app-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.scss'],
})
export class CardComponent {
  @Input() public obs$: Observable<string>;
  public readonly data$: Observable<string>;
  public data: string;
  public obs: string;

  constructor() {
    this.data$ = of('hello');
  }
}

So in the above example, it will auto handle the observable myObservable$ and data$, and create variables in the component called ordinaryVariable and data. It even work's with @Input, which I think can be handy when piping observables into a component!

Source: https://github.com/dolanmiu/sewers/blob/master/projects/sewers/src/lib/sink.decorator.ts

Based on this talk by @MikeRyanDev:
https://github.com/MikeRyanDev/rethinking-reactivity-angularconnect2019

@dummdidumm
Copy link
Contributor

While I like that the decorator-approach is more natural for mixins, I feel that the concrete implementation is a little too verbose and one doesn't know whats going on if he does not know the conventions.

I think, with how Angular internally works (needing the LifeCycleHooks as methods on the class, not able to add them later on or tell the compiler to call it because we know it's there), the best approach is inheritance. In order to have reusability, one can also use inheritance with mixins. Based on the talk by @MikeRyanDev and using mixins:

import {
  Component,
  OnInit,
  OnDestroy,
  ɵmarkDirty as markDirty
} from '@angular/core';
import { Subject, Observable, from, ReplaySubject, concat } from 'rxjs';
import { scan, startWith, mergeMap, tap, takeUntil } from 'rxjs/operators';

type ObservableDictionary<T> = {
  [P in keyof T]: Observable<T[P]>;
};

type Constructor<T = {}> = new (...args: any[]) => T;

const OnInitSubject = Symbol('OnInitSubject');

export function WithOnInit$<TBase extends Constructor>(Base: TBase) {
  return class extends Base implements OnInit {
    private [OnInitSubject] = new ReplaySubject<true>(1);
    onInit$ = this[OnInitSubject].asObservable();

    ngOnInit() {
      this[OnInitSubject].next(true);
      this[OnInitSubject].complete();
    }
  };
}

const OnDestroySubject = Symbol('OnDestroySubject');

export function WithOnDestroy$<TBase extends Constructor>(Base: TBase) {
  return class extends Base implements OnDestroy {
    private [OnDestroySubject] = new ReplaySubject<true>(1);
    onDestroy$ = this[OnDestroySubject].asObservable();

    ngOnDestroy() {
      this[OnDestroySubject].next(true);
      this[OnDestroySubject].complete();
    }
  };
}

export function WithConnect<
  TBase extends Constructor &
    ReturnType<typeof WithOnDestroy$> &
    ReturnType<typeof WithOnInit$>
>(Base: TBase) {
  return class extends Base {
    connect<T>(sources: ObservableDictionary<T>): T {
      const sink = {} as T;
      const sourceKeys = Object.keys(sources) as (keyof T)[];
      const updateSink$ = from(sourceKeys).pipe(
        mergeMap(sourceKey => {
          const source$ = sources[sourceKey];

          return source$.pipe(
            tap((sinkValue: any) => {
              sink[sourceKey] = sinkValue;
            })
          );
        })
      );

      concat(this.onInit$, updateSink$)
        .pipe(takeUntil(this.onDestroy$))
        .subscribe(() => markDirty(this));

      return sink;
    }
  };
}

export class Base {}

const ReactiveComponent = WithConnect(WithOnDestroy$(WithOnInit$(Base)));

@Component({
  selector: 'app-root',
  template: `
    <div class="count">{{ state.count }}</div>
    <div class="countLabel">Count</div>
    <button class="decrement" (click)="values$.next(-1)">
      <i class="material-icons">
        remove
      </i>
    </button>
    <button class="increment" (click)="values$.next(+1)">
      <i class="material-icons">
        add
      </i>
    </button>
  `
})
export class AppComponent extends ReactiveComponent {
  values$ = new Subject<number>();
  state = this.connect({
    count: this.values$.pipe(
      startWith(0),
      scan((count, next) => count + next, 0)
    )
  });

  pushValue(value: number) {
    this.values$.next(value);
  }
}

This gives us the ability to define convenience-classes like ReactiveComponent while giving the user the ability to mixin other functionality as needed, mitigating the multiple inheritance problem. So he could do stuff like this:

export class SomeOtherBaseClass {
   // ...
}

const MyCustomBaseClass = WithOnChanges$(WithConnect(WithOnDestroy$(WithOnInit$(SomeOtherBaseClass))))

@Component({...})
export class MyComponent extends MyCustomBaseClass {
  // ...
}

@bryanrideshark
Copy link

@BioPhoton thanks for the tip about the ReplaySubject. I'll certainly look into doing that.

@OlaviSau
Copy link

OlaviSau commented Sep 30, 2019

Hello :) I took a little peek into the angular compiler - components are core to angular functionality, it would take significant effort to create an alternative component. The current compiled data structure does not support bindings as inputs.

Information available about directives after compilation

export interface CompileDirectiveSummary extends CompileTypeSummary {
  type: CompileTypeMetadata;
  isComponent: boolean;
  selector: string|null;
  exportAs: string|null;
  inputs: {[key: string]: string}; // !!! Inputs are stored here !!!
  outputs: {[key: string]: string};
  hostListeners: {[key: string]: string};
  hostProperties: {[key: string]: string};
  hostAttributes: {[key: string]: string};
  providers: CompileProviderMetadata[];
  viewProviders: CompileProviderMetadata[];
  queries: CompileQueryMetadata[];
  guards: {[key: string]: any};
  viewQueries: CompileQueryMetadata[];
  entryComponents: CompileEntryComponentMetadata[];
  changeDetection: ChangeDetectionStrategy|null;
  template: CompileTemplateSummary|null;
  componentViewType: StaticSymbol|ProxyClass|null;
  rendererType: StaticSymbol|object|null;
  componentFactory: StaticSymbol|object|null;
}

Inputs are expressed as key value pairs - the key represents the property name on the component and the value represents the property in the template. Angular will have no way to distinguish between an input observable and a regular one. They cannot change how regular inputs work either for backwards compatibility.

A possible solution with realistic timelines
Make one property input state.

@Input({
    [propertyName]: templatePropertyName
}) readonly input$ = new BehaiviorSubject/ReplaySubject<...>({});

Why?

  1. This is a solution that would leverage the existing angular codebase - the only difference is that it would add inputPropertyName (or some other key name) to the data.
    checkAndUpdateDirectiveInline would have to be modified slightly and some other areas need to pass the inputPropertyName to it.
  2. This allows the input observable to essentially behave as ngOnChanges.
    showButton$ = input$.pipe(tap(() => <onChangesLogic>))
  3. The input subject can easily be split into multiple observables without tampering with the original ( it stays a subject, thus it keeps next).
showButton$ = input$.pipe(map(input => coerceBooleanProperty(input.showButton)));

regularBacon$ = input$.pipe(map(input=> input.spicy));

atomBomb$ = input$.pipe(tap(input => {
    this.propagate(input.atom);
}));

customer$  =  input$.pipe(map(input => input.customer));
order$ = combineLatest([
    this.customer$,
    this.store.pipe(select(state => state.order.registry))
]).pipe(map(([customer, registry]) => registry.find(order => order.customerID === customer.id))))

Who?
I would love if someone was willing to do handle the angular politics to get it implemented - not in my priorities right now.

Hope this helps - may you be happy.

@BioPhoton
Copy link
Contributor Author

Hi @OlaviSau.

Thanks for the answer!
Regarding your "who?" section,

I would love if someone was willing to do handle the angular politics to get it implemented - not in my priorities right now.
actually

@robwormald is handling this. He is also in the ngrx core team.

@BioPhoton
Copy link
Contributor Author

Here the design doc for the ngrxPush pipe:
https://hackmd.io/Uus0vFu3RmWRVGgmtzuUWQ?both=

@stupidawesome
Copy link

stupidawesome commented Feb 2, 2020

I'm experimenting with a combination of base class + service injection + proxies.

Stackblitz example is here: https://stackblitz.com/edit/angular-9-0-0-rc-1-1qxbvs

All reactive state logic is handled by an effects class. The two parts to note here are the Reactive base class, and the effects provider.

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [effects(ChildEffects)]
})
export class ChildComponent extends Reactive {
    @Input()
    public name: string

    @Input()
    public age: number

    @Output()
    public ageChange = new EventEmitter()

    public clicked: MouseEvent
}

In the effects class, each property marked by the @Effect() decorator should map onto the property of the same name to be updated in the component (except side effects).

Each effect method is a factory function that receives two arguments: a state object that maps each component property to an observable, and the component instance itself. Each method should return an observable of the same type as the corresponding state object (or a subscription if its a side effect). These methods are invoked during ngAfterViewInit to allow the component to be fully initialised.

Change detection can be controlled per property by setting markDirty: true, this could be extended to trigger detectChanges instead (thinking of zoneless change detection).

@Injectable()
export class ChildEffects implements Effects<ChildComponent> {
    constructor(private http: HttpClient) {}

    @Effect({ markDirty: false })
    public name(state: State<ChildComponent>, component: ChildComponent) {
        /** this.http.get("someUrl") // could do something with http here **/
        return of("Stupidawesome")
    }

    @Effect({ markDirty: true })
    public age(state: State<ChildComponent>, component: ChildComponent) {
      return interval(1000).pipe(
        withLatestFrom(state.age),
        map(([_, age]) => age + 1),
        tap(age => component.ageChange.emit(age)) // could be streamlined
      )
    }

    // side effect
    @Effect()
    public sideEffect(state) {
      return state.age
        .subscribe(age => console.log(`age changed: ${age}`))
    }

    // Template events
    // <p (click)="clicked = $event"></p>
    @Effect()
    public clicked(state, component) {
      return state.clicked
        .subscribe(event => console.log(`click:`, event))
    }
}

The Reactive base class executes the effects and does cleanup after the component is destroyed.

@BioPhoton
Copy link
Contributor Author

Here the design doc for the *ngrxLet directive:
https://hackmd.io/8_3rp0A7RweSYJiulsifbQ?both

@BioPhoton
Copy link
Contributor Author

BioPhoton commented Feb 2, 2020

Here a collection of terms used in the document and their explanation:
https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10

Please let me know if I miss something!

@Rush
Copy link

Rush commented Feb 3, 2020

Looks good @BioPhoton

btw. not sure if it's relevant but I feel having inputs as observables natively would be of help for the overall solution. I recently found this nifty library https://github.com/Futhark/ngx-observable-input

@LayZeeDK
Copy link
Contributor

LayZeeDK commented Feb 3, 2020

btw. not sure if it's relevant but I feel having inputs as observables natively would be of help for the overall solution. I recently found this nifty library Futhark/ngx-observable-input

Why this? Just pass an observable to the input property. Even change detection works by default when new values are emitted through input observables.

@fxck
Copy link

fxck commented Feb 3, 2020

Why this? Just pass an observable to the input property. Even change detection works by default when new values are emitted through input observables.

Because you might be passing data down from a dumb component to a component that wants to utilise reactivity.

@LayZeeDK
Copy link
Contributor

LayZeeDK commented Feb 3, 2020

Because you might be passing data down from a dumb component to a component that wants to utilise reactivity.

You decide the data binding API, not your parent components 👴

@stupidawesome
Copy link

stupidawesome commented Feb 6, 2020

Here's take 2 on my last post with comments on how it deals with the various features desired from a reactive component. For brevity I will only refer to components here, but this pattern could also be applied to directives and modules (anywhere you can add providers).

Summary

The main goal of this implementation is to develop a reactive API for Angular components with the following characteristics:

  1. It does not complicate components with base classes.
  2. It extracts state management from the component into a separate service.
  3. It does not depend on lifecycle hooks.
  4. It shares the same injector as the component it is decorating.
  5. It automatically cleans up subscriptions when the component is destroyed.
  6. Any own property on the component can be observed and changed, including inputs.
  7. Component templates should be simple and synchronous.

Overview

The API takes inspiration from NgRx Effects and NGXS. This example demonstrates a component utilising various angular features that we would like to make observable:

  1. Input bindings
  2. Template bindings
  3. ViewChild (or ContentChild) decorators
  4. ViewChildren (or ContentChildren) decorators
  5. HostListener decorators
@Component({
    selector: "my-component",
    template: `
        <div (click)="event = $event" #viewChildRef>Test</div>
    `,
    providers: withEffects(MyEffects),
    host: { "(mouseover)": "event = $event" }
})
@RunEffects()
export class MyComponent {
    @Input() count: number

    @Output() countChange: EventEmitter<number>

    @ViewChild("viewChildRef") viewChild: ElementRef | null

    @ViewChildren("viewChildRef") viewChildren: QueryList<ElementRef> | null

    public event: Event | null

    constructor(@UseEffects() effects: Effects) {
        this.count = 0
        this.countChange = new EventEmitter()
        this.viewChild = null
        this.viewChildren = null
        this.event = null
    }
}

Binding the effects class is a three step process.

  1. withEffects(Effects1, [Effects2, [...Effects3]])

One or more classes are provided to the component that will provide the effects. Effects are decoupled from the component and can be reused.

  1. @UseEffects() effects: Effects

Every component using effects must inject the Effects class since there is no way to automatically instantiate a provider without it being injected.

  1. @RunEffects()

This decorator does two things. First it decorates all own properties of the component with getter/setter hooks so that changes can be observed. It then calls the Effects to run the effects. This happens after the constructor is called.

We can work with one or more effects classes to describe how the state should change, or what side effects should be executed.

@Injectable()
export class MyEffects implements Effect<MyComponent> {
    constructor(private http: HttpClient) {
        console.log("injector works", http)
    }

    @Effect({ markDirty: true })
    count(state: State<MyComponent>) {
        return state.count.pipe(delay(1000), increment(1))
    }

    @Effect()
    countChanged(state: State<MyComponent>, context: MyComponent) {
        return state.count.subscribe(context.countChanged)
    }

    @Effect()
    logViewChild(state: State<MyComponent>) {
        return state.viewChild.changes.subscribe(viewChild => console.log(viewChild))
    }

    @Effect()
    logViewChildren(state: State<MyComponent>) {
        return queryList(state.viewChildren).subscribe(viewChildren => console.log(viewChildren))
    }

    @Effect()
    logEvent(state: State<MyComponent>) {
        return state.event.subscribe(event => console.log(event))
    }
}

Anatomy of an effect

In this implementation, each method decorated by the @Effect() decorator will receive two arguments.

  1. state: State<MyComponent>

The first argument is a map of observable properties corresponding to the component that is being decorated. If the component has own property count: number, then state.count will be of type Observable<number>. Subscribing to this value will immediately emit the current value of the property and every time it changes thereafter. For convenience, the initial value can be skipped by subscribing to state.count.changes instead.

  1. context: MyComponent

The second argument is the component instance. This value always reflects the current value of the component at the time it is being read. This is very convenient for reading other properties without going through the problem of subscribing to them. It also makes it very easy to connect to @Output().

There are three possible behaviours for each effect depending on its return value:

  1. Return an Observable.
@Effect({ markDirty: true })
count(state: State<MyComponent>) {
    return state.count.pipe(delay(1000), increment(1))
}

When an observable is returned, the intention is to create a stream that updates the value on the component whenever a new value is emitted. Returning an observable to a property that is not an own property on the class should throw an error.

  1. Return a Subscription
@Effect()
logEvent(state: State<MyComponent>) {
    return state.event.subscribe(event => console.log(event))
}

When a subscription is returned, the intention is to execute a side effect. Values returned from the subscription are ignored, and the subscription is cleaned up automatically when the effect is destroyed.

  1. Return void

When nothing is returned, it is assumed that you are performing a one-time side-effect that does not need any cleanup afterwards.

Each effect method is only executed once. Each stream should be crafted so that it can encapsulate all possible values of the property being observed or mutated.

Because each effect class is an injectable service, we have full access to the component injector including special tokens such as ElementRef.

constructor(http: HttpClient) {
    console.log("injector works", http)
}

We can delegate almost all component dependencies to the effects class and have pure reactive state. This mode of development will produce very sparse components that are almost purely declarative.

Lastly, the @Effect() decorator itself can be configured.

interface EffectOptions { 
    markDirty?: boolean
    detectChanges?: boolean // not implemented yet
    whenRendered?: boolean // not implemented yet
}

The first two options only apply when the effect returns an observable value, and controls how change detection is performed when the value changes. By default no change detection is performed.

The last option is speculative based on new Ivy features. Setting this option to true would defer the execution of the effect until the component is fully initialised. This would be useful when doing manual DOM manipation.

Do we even need lifecycle hooks?

You might have noticed that there are no lifecycle hooks in this example. Let's analyse what a few of these lifecycle hooks are for and how this solution might absolve the need for them.

  1. OnInit

Purpose: To allow the initial values of inputs passed in to the component and static queries to be processed before doing any logic with them.

Since we can just observe those values when they change, we can discard this hook.

  1. OnChanges

Purpose: To be notified whenever the inputs of a component change.

Since we can just observe those values when they change, we can discard this hook.

  1. AfterContentInit

Purpose: To wait for content children to be initialised before doing any logic with them.

We can observe both @ContentChild() and @ContentChildren() since they are just properties on the component. We can discard this hook.

  1. AfterViewInit

Purpose: To wait for view children to be initialised before doing any logic with them. Additionally, this is the moment at which the component is fully initialised and DOM manipulation becomes safe to do.

We can observe both @ViewChild() and @ViewChildren() since they are just properties on the component. If that's all we are concerned about, we can discard this hook.

For manual DOM manipulation, there is another option. Angular Ivy exposes a private whenRendered API that is executed after the component is mounted to the DOM. This is complimentary to the markDirty and detectChanges API that are also available, but not required for this solution. At this point in time there is no example to demonstrate how this might be used, but it is my opinion that once a reasonable solution is found we can discard this lifecycle hook too.

  1. NgOnDestroy

Purpose: To clean up variables for garbage collection after the component is destroyed and prevent memory leaks.

Since this hook is used a lot to deal with manual subscriptions, you might not need this hook. The good thing is that services also support this hook, do you could move this into the Effect class instead.

Conclusions

In the purely reactive world, components become much simpler constructs. With the power to extract complex logic into reusable functions this would result in components that are much more robust, reusable, simpler to test and easier to follow.

I am working on the implementation here: https://github.com/stupidawesome/ng-effects

@LayZeeDK
Copy link
Contributor

LayZeeDK commented Feb 6, 2020

Please, no more decorators 🙁

@BioPhoton
Copy link
Contributor Author

Imho reactive hooks is definitely a component feature in the future.

@stupidawesome
Copy link

stupidawesome commented Feb 7, 2020

Please, no more decorators 🙁

interface MyState {
    count: number
}

class MyEffects implements Effects<MyComponent> {
    count = createEffect(
        (state: State<MyState>, ctx: MyComponent) => interval(1000)),
        { markDirty: true }
    )
}

@Component({
    providers: [effects([MyEffects])]
})
class MyComponent implements MyState {
    count: number
    constructor(connect: Connect) {
        this.count= 0
        connect(this) // init any "connectable" services
    }
}

How about this? Example code here

@BioPhoton
Copy link
Contributor Author

Welcome to the party:
https://dev.to/rxjs/research-on-reactive-ephemeral-state-in-component-oriented-frameworks-38lk

@stupidawesome
Copy link

stupidawesome commented Feb 8, 2020

First pass at implementing a whenRendered hook:

It is invoked once immediately after the first round of change detection finishes.

@Effect({ whenRendered: true })
public viewChildren(state: State<TestState>) {
    // is executed after view children have been attached
    return state.viewChildren.subscribe(value => console.log("viewChildren available:", value))
}

@BioPhoton
Copy link
Contributor Author

BioPhoton commented Feb 8, 2020

@stupidawesome

This comment has been minimized.

@stupidawesome
Copy link

Misko's Ivy design docs are interesting

Looks like zones are going away eventually.

@stupidawesome

This comment has been minimized.

@stupidawesome

This comment has been minimized.

@BioPhoton
Copy link
Contributor Author

I try to get an opinion on passing the context. What are the pros and cons for you?

@stupidawesome
Copy link

stupidawesome commented Feb 12, 2020

I try to get an opinion on passing the context. What are the pros and cons for you?

Local effects should be able to compose or connect Observables/EventEmitters in the component instance such as FormGroup and @Ouput(). This can be combined with the observable state (when component properties change) and observable services. This example demonstrates all the observable sources at play. Outputs in particular must be assigned in the constructor of a component otherwise Angular throws an error.

Overall however this is just a convenience. You could also obtain the reference by State and then switch mapping to it.

Passing in the context is something that is common practice using providers.

@Component({
    providers: [{ provide: Parent, useExisting: ParentComponent }]
})
export class ParentComponent {}

@Component()
export class ChildDirective {
    constructor(parent: Parent) {}
}

So there is nothing particularly novel about passing in the context. The library currently provides this in two ways: 1. as an argument to each effect method and 2. via the HostRef provider. The idea is to treat the context as just another observable source.

I haven't used or tested this library extensively enough to understand all of the downsides, but the biggest con is probably going to be the use of property interception to construct the State observables.

The downsides are that it won't be easy to extend a "connected" class. If we only connect "final" classes this can be mitigated (explore the prototype for effects higher up the chain?). The Connect mechanism itself is only there as a workaround to the lack of some sort of HOST_INITIALIZER token in the same vein as APP_INITIALIZER.

A side effect of using Object.defineProperty to do property interception is that this only works if the property is already initialized. I use proxies in dev mode to catch invalid property access early, but more importantly this solution necessitates that all properties be definitely assigned (even if set to undefined) in the constructor before calling connect. This might be jarring for some users even if it is generally a good practice to follow.

The plus side is that this lets us easily observe all property changes on the component regardless of where those changes originate (ie. template events, @Input() bindings, queries, dynamic component loaders, etc). If all properties can be observed, then we don't need to rely on lifecycle hooks or base classes to trigger updates. We can solve problems such as late subscribers (State is hot) and hot/cold/unicast/multicast observables (State is multicast, effects are only subscribed to once). Being able to read current values from the context inside effects is a big convenience compared to unwieldy alternatives such as withLatestFrom or combineLatest.

Additionally, we get fine tune control over when to trigger change detection. We can make change detection occur when some or all properties change, or we can use sampling/throttling to limit or disable change detection for a period of time. We can also ditch zones.

@stupidawesome

This comment has been minimized.

@brandonroberts
Copy link
Member

@stupidawesome thank you for putting these ideas into a library, but we're getting off-topic here with the updates. If you would like to discuss that library further, open an issue in your repo so people can continue discussing it there.

@AdditionAddict
Copy link
Contributor

My 2 cents as a new programmer (just over a year now) for a small business as a one person team. One of the reasons I’ve fell in love with the ngrx modules has been that the code writers are not afraid to address complex ideas to newcomers through ng-conf talks, tonnes of examples and are willing to be explicit about what is good code and what is misuse and bad form. The time to learn is a much under appreciated measure shortened by an awesome and supportive community. So thank you. ❤️ Please keep sharing as you go and keep ability to test at the forefront.

I write a lot of boilerplate code to get a component to be reactive and sometimes leave it off in favour of using the angular hooks for smaller use cases which often feels clunky. Especially if I opt for the OnChanges hook which does feels ugly guarding with if statements to get the input I care about (obviously could use get and set but this adds a huge number of lines early in a component when all I want is to be able to immediately see the main component methods at a glance e.g. buildForm() and the formvalue emitter). I experimented with this medium post which felt natural to use but use of decorator was a massive no no. And repeating the same code to get a takeUntil(this.destroyed$). Had some success using class mixin for this but felt like overkill for a single benefit. I really can see the upside to this module and look forward to it. 🤓

@Rush
Copy link

Rush commented Jul 21, 2020

I've implemented connectState as a simple function and exposed it as a library. I'm using it in production projects and it's working just fine. Decorator is based off @ngneat/until-destroy. Feel free to use in your project!

https://github.com/Rush/ng-connect-state/

import { ConnectState, connectState } from 'ng-connect-state';
import { interval } from 'rxjs';

@ConnectState()
@Component({ template: `
  {{ state.timer }}
  <button (click)="state.reload()">Reload</button>
  Loading: {{ state.loading.timer }}
  `
})
export class InboxComponent {
  ngOnDestroy() { }

  // this exposes state.timer as a synchronous value state.timer and automatically unsubscribes on destroy
  // + it has a .loading indictor, exposes optional observable access as well as can resubscribe to observables on demand
  state = connectState(this, {
    timer: interval(1000),
  })
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests