Reactive Extensions for Angular
This document is a proposal for a fully reactive architecture in Angular. Its main goal is to serve as the glue between your reactive code and the framework.
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. 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.
Here we will try to list all areas in angular where such helper primitives would be needed. The first step is to list all possible situations and a very simple solution for a reactive approach. Each area may have different requirements to be more convenient to use reactively.
Every section explains the current imperative approach as well as the reactive approach in a simple way. This should help to understand the problems and get a good overview of the options and needs for a reactive architecture in angular.
Based on the collected information we can try to use the explored options to create an elegant solution for the explored needs.
Following topics are documented below:
DomElements is everything you can query from document
.
The goal is to list vanilla js versions as well as the angular way and list options on how to make property values and events working with angular.
<elem attr=""></elem>
To set a value for an input attribute you can query the dom get the item and set the value.
const elem = document.getElementById('elem-1');
const elem.value = 42;
This case is easy to cover in a general case as there are many built-in directives that enable us to set attributes on elements with angular.
Imperative approach:
@Component({
selector: 'my-app',
template: `
<button id="p1" [disabled]="disabled">Btn</button>
`
})
export class AppComponent {
disabled = false;
}
Reactive approach:
Angular provides a set of decorators for all standard dom attributes. This the suggested way to go and explained in detail in the AngularComponent section.
Reactive approach:
const elem = document.getElementById('elem-1');
fromEvent(elem, 'click')
.subscribe(e => {
console.log('click event:', e);
});
Needs: As Angular covered this already this section can be ignored for the suggested extensions.
The goal is to list vanilla js versions as well as the angular way and list options on how to make property values and events working with angular.
<elem attr=""></elem>
Imperative approach:
TBD
Reactive approach:
TBD
Needs: TBD
elem.addEventListener()
Imperative approach:
TBD
Reactive approach:
TBD
Needs: TBD
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:
And consider the following bindings:
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
withbufferSize
of1
to emit notifications
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
asEventEmitters
there is no need for as extension
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
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
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 forwardundefined
as value as on value ever was emitted- when initially passed
null
the pipe should forwardnull
as value asnull
was emitted- when initially passed
of(undefined)
the pipe should forwardundefined
as value asundefined
was emitted- when initially passed
of(null)
the pipe should forwardnull
as value asnull
was emitted- when initially passed
EMPTY
the pipe should forwardundefined
as value as on value ever was emitted- when initially passed
NEVER
the pipe should forwardundefined
as value as on value ever was emitted- when reassigned a new
Observable
the pipe should forwardundefined
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:
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:
*ngIf
directive*ngIf
directive is triggered be falsy values, but we don't want to conditionally show or hiding content,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.
In some cases the ngIfElse
directive and ng-template
helps, but in some situations we can't use it.
@Component({
selector: 'my-app',
template: `
<a *ngIf="random$ | async as random" [routerLink]="[{outlets:{aside: random}}]">toggle + {{random ? 'aside' : ''}}</a>
`})
export class AppComponent {
random$ = interval(1000)
.pipe(
map(_ => Math.random() > 0.5 ? 1 : 0)
);
}
Here we could try to use *ngFor
to solve the problem.
Context variable over the *ngFor
directive
@Component({
selector: 'my-app',
template: `
<a *ngFor="let random of [random$ | async]" [routerLink]="[{outlets:{aside: random}}]">{{random ? 'show' : 'hide'}}</a>
`})
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
.
There is another problem which we should consider. Nested scopes.
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:
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
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')
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:
@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:
@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.
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
The goal here is to evaluate approaches of encapsulating state-management into a service. As this is trivial in angular we directly go to the example.
Simple approach:
@Component({
selector: 'app-child',
template: ``,
providers: [LocalProvidedService]
})
export class ChildComponent implements OnChanges {
constructor(private localStateService: LocalStateService) {
this.localStateService.next(42);
}
}
@Injctable({
providedIn: 'root'
})
export class LocalProvidedService implements OnDestroy {
subject = new Subject();
next(value) {
this.subject.next(value);
}
}
Needs As Angular already provides a DI layer there is nothing to solve here
The goal here is to come up with a good slim solution for managing the components state as an object. The state should be immutable by default and easy to change and receive changes.
Simple approach:
@Component({
selector: 'app-child',
template: `
<button (click)="increment()">click</button>
state: {{state$ | async | json}}
`
})
export class ChildComponent implements OnChanges {
command$ = Subject();
state$ = this.command$
.pipe(
scan((state, command) => ({...state, ...command}), {})
);
increment() {
this.command$.next({value: 1})
}
}
Needs We need to manage the components state as an object. The logic should make immutable changes so we don't have care about it. The state should be easy to receive and change to the state should be able with as minimal code as possible.
Manage state as an object Form the above explorations following things are needed to organize our state
- an object to identify every stored value over a key
- setup a
Subject
to have the observers.next()
Method available to send values- accumulate the values in an object over the
scan
operator- at least immutable changes to the shallow
done by i.e. the TypeScript spread operator...
should be automated by the logic- having a layer of validation for the commands
Situations, where interested parties join a part of your application later on in time, are called late subscribers. Some meaningful examples could be:
constructor
or the ngOnChanges
method.Such situations are handled over getter in imperative programming. In reactive programming, we solve this over thin caching layer.
Problem of being too late
@Component({
selector: 'app-late-subscriber',
template: `
<h2>Late Subscriber Child</h2>
{{state$ | async | json}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LateSubscriberComponent {
state$ = new Subject();
@Input()
set state(v) {
this.state$.next(v);
}
}
Replaying latest (n) values
@Component({
selector: 'app-late-subscriber',
template: `
<h2>Late Subscriber Child</h2>
{{state$ | async | json}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LateSubscriberComponent {
state$ = new ReplaySubject(1);
@Input()
set state(v) {
this.state$.next(v);
}
}
Needs
We need to abstract timing issues away from the consumer. In the case of late subscribers, it is easily possible with subjects like BehaviorSubject
or ReplaySubject
or operators like share
or shareReplay
.
Replaying latest Values
- use
ReplaySubject
to cache the latest (n) values. In most of the cases, this is the way to go.- there are rare cases where we want to use an initial value and are not able to use
startWith
, here aBehaviorSubject
can be used- In cases where we have no control of the source we can also use
shareReplay
- In stateful services the replayed values are always limited to
1
. The actual value and all future ones.
There are situations where we have to compose multiple streams, compute new state and create a reference to some object, i.e. a FromGroup
. This reference is then later on shared with multiple subscribers.
Such situations are handled by mutation a component property in imperative programming. In reactive programming, we solve this multi-casting.
Problem of being shared references
@Component({
selector: 'app-sharing-a-reference',
template: `
<h2>Sharing a reference</h2>
<p><b>default$:</b></p>
<form *ngIf="(formGroup$ | async$) as formGroup" [formGroup]="formGroup">
<div *ngFor="let c of formGroup.controls | keyvalue">
<label>{{c.key}}</label>
<input [formControlName]="c.key"/>
</div>
</form>
`
})
export class SharingAReferenceComponent {
state$ = new ReplaySubject(1);
@Input()
set formGroupModel(value) {
this.state$.next(value);
}
@Output() formValueChange = new EventEmitter();
formGroup$: Observable<FormGroup> = combineLatest(this.state$, this.router.params)
.pipe(
map(this.preparingFormGroupConfig),
map(config => this.fb.group(config))
);
constructor(
private fb: FormBuilder,
private router: ActivatedRoute
) {
this.formGroup$
.pipe(
switchMap((fg: FormGroup) => fg.valueChanges)
)
.subscribe(v => this.formValueChange.emit(v));
}
preparingFormGroupConfig([modelFromInput, modelFromRouterParams]) {
// override defaults with router params if exist
return Object.entries({...modelFromInput, ...modelFromRouterParams})
.reduce((c, [name, initialValue]) => ({...c, [name]: [initialValue]}), {});
}
}
As we see the provided example is not working. The reason for this is we subscribe multiple times to the formGroup$
.
One time in the template to render the form, the second time in the constructor to forward form value changes to the EventEmitter
.
Because the formGroup$
observable is cold (every subscriber receives a unique producer) we instantiate the FormGroup
once per subscription.
Sharing a reference problem
@Component({
selector: 'app-sharing-a-reference',
template: `
<h2>Sharing a reference</h2>
<p><b>newObject$:</b></p>
<div>
{{newObject$ | async | json}}
</div>
`
})
export class SharingAReferenceComponent {
newObject$ = of(Math.random());
constructor() {
this.newObject$
.subscribe(console.log);
}
}
As we see we end up with 2 different numbers. This is equivalent to having 2 different instances of and Object like the mentioned FormGroup
. Whenever we have to share a reference we need to make shure to have it multicasted.
Multicast the reference solution
@Component({
selector: 'app-sharing-a-reference',
template: `
<h2>Sharing a reference</h2>
<p><b>newObject$:</b></p>
<div>
{{newObject$ | async | json}}
</div>
`
})
export class SharingAReferenceComponent {
newObject$ = of(Math.random())
.pipe(share());
constructor() {
this.newObject$
.subscribe(console.log);
}
}
Here we use shareReplay(1)
to make sure all subscriber receives the same reference.
Needs
To be able to share references creates on the flyover observables we have to make sure the observables are multicasted.
Sharing a reference over observables
- use
shareReplay(1)
make sure all subscribers receive the same reference- forward last instance for a late subscriber. Also done with
shareReplay(1)
Situations, where we have early producers, are things where we have values produces and transported over cold Observables. In this case, as we have no subscriber yet on the mentioned Observable, we are losing all values until the first subscriber.
The goal would be to find a solution that can get encapsulated and abstracted away from the actual code.
Problem of early production
@Component({
selector: 'app-early-producer',
template: `
{{state$ | async | json}}
`
})
export class LateSubscriberComponent {
command$ = new Subject();
state$ = this.command
.pipe(
scan((s,c)=>({...s,...c}), {})
);
@Input()
set state(v) {
this.command$.next(v);
}
constructor() {
this.state = {slice1: 7};
this.state = {slice2: 42};
}
}
Making the accumulation hot
@Component({
selector: 'app-late-subscriber',
template: `
<h2>Late Subscriber Child</h2>
{{state$ | async | json}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LateSubscriberComponent {
command$ = new Subject();
state$ = this.command
.pipe(
scan( (s,c) => ({...s,...c}), {} ),
publish()
);
@Input()
set state(v) {
this.command$.next(v);
}
constructor() {
this.state.connect();
this.state = { slice1: 7 };
this.state = { slice2: 42 };
}
}
Needs We need to have the observable to be hot with component constructor. Additional pipes or similar should be abstractedd away.
Providing State as Hot To ensure all values get processed we need to:
- call the
.connect()
on the observable created overpublish
at component construction- use
publishReplay(1)
to provide state for late subcriber
Normally we use the component's lifecycle to subscribe and unsubscribe from certain observables in the component. We already know some solutions for getting a clean and elegant subscription handling by turning OnDestroy into an Observable. But it still feels a bit repetitive. Furthermore, it is up to the user to do subscription handling right.
The goal would be to find a way to move subscription handling outside of the component code.
Subscription Handling Inside of Component
@Component({
selector: 'app-early-producer',
template: ``
})
export class LateSubscriberComponent {
ngOnDestroy$ = new Subject();
command$ = new Subject();
state$ = this.command
.pipe(
scan( (s,c) => ({...s,...c}), {} ),
publish()
);
constructor() {
this.state$
.pipe(takeUntil(this.ngOnDestroy$))
.subscribe(console.log);
}
ngOnDestroy() {
this.ngOnDestroy$.next();
}
}
Subscription Handling over View Provider
@Component({
selector: 'app-early-producer',
template: ``,
providers: [LocalStateService]
})
export class LateSubscriberComponent implements OnDestroy {
constructor(private localState: LocalStateService) {
this.localState.state$
.subscribe(console.log);
}
}
class LocalStateService implements OnDestroy {
ngOnDestroy$ = new Subject();
command$ = new Subject();
state$ = this.command
.pipe(
scan( (s,c) => ({...s,...c}), {} ),
publish()
);
ngOnDestroy() {
this.ngOnDestroy$.next();
}
}
Needs We need to find an elegant way of controlling subscriptions in a component.
Automated subscription handling To ensure all subscriptions are cleaned up outside ou the component we need to:
- encapsulate state managed in the component into a service
- leverage the services
OnDestroy
life cycle hook to handle subscription inside the service- provide it under the component's providers to bind its lifetime to the components lifetime
- use dependency injection us instantiate the service with component construction
To run a angular project zone-less following steps should be made:
main.ts
of your project and pass 'noop'
as NgZone
instance under the compilerOptions
in the bootstrap call:...
// !!! CHAGNE HERE !!!
platformBrowserDynamic().bootstrapModule(AppModule, {ngZone: 'noop'})
.catch(err => console.error(err));
NgZone
into app.component.ts
and log the instance: constructor(private ngZone: NgZone) {
console.log('ngZone', this.ngZone);
}
You should see NoopNgZone
as class name. That's it. Now we run zone-less.
Now let's setup build and serve scripts to switch between zone-full and zone-less.
main.ts
and rename it to main.zone-less.ts
. Then revert main.ts
to the original state.polyfills.ts
and rename it to polyfills.zone-less.ts
./***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
// !!! CHANGES HERE !!!
// import 'zone.js/dist/zone'; // Included with Angular CLI.
angular.json
and add following configurations: {
...
"projects": {
...
"PROJECT_NAME_HERE": {
...
"architect": {
"build": {
...
"configurations": {
... !!! CHANGES HERE !!!
"zoneLess": {
"main": "pathToProject/src/main.zone-less.ts",
"polyfills": "pathToProject/src/polyfills.zone-less.ts"
}
}
},
"serve": {
...
"configurations": {
... !!! CHANGES HERE !!!
"zoneLess": {
"browserTarget": "elements:build:zoneLess"
}
}
}
}
}
},
...
}
package.json
in the root folder to use your new configuration.{
...
"scripts": {
... !!! CHANGES HERE !!!
"start:zone-less": "ng serve --project YOUR-PROJECT-NAME -c=zoneLess"
},
...
}
npm run start:zone-less
and npm run start
and check the difference in the console log's.Next to some edge cases with zones and web components it works change detection pretty seamless in angular. However, when exiting zone and approaching a fully reactive zone-less setup we run into several scenarios that end up problematic.
Let's see what scenarios we should take a closer look:
If we test some basic template bindings, displaying a primitive or a simple object, we see no changes in the view.
p: {{primitive}}, o: {{o | json}}
``
Also with the `async` pipe nothing gets rendered.
This is the case because the async pipe only triggers `ChangeDetectorRef.markForCheck()`,
but change detection does not look for changes, as it is disabled.
```html
p$: {{primitive$ | async}}, o$: {{o$ | async | json}}
To solve it we need to trigger ChangeDetectorRef.detectChanges()
whenever we want to render.
One of the first approaches would be to implement a tap
operator and trigger cd there before it is rendered in the view.
But this would end up in a off by one issue and therefore is no solution.
A proper solution is trigger detectChanges()
over a pipe,
so rendering runs after the value arrives in the template.
Primitive Workaround To achieve it in a quick an dirty way is following:
push.pipe.ts
into your projects src
folder.async
pipe from the angular repo. Here the link to the file async_pipe.ts. this._ref.detectChanges();
@Pipe({name: 'push', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
app.module.ts
declarations:...
import {PushPipe} from "./push.pipe";
@NgModule({
declarations: [
...
PushPipe
],
...
})
export class AppModule {
...
}
{{observable$ | push}}
Needs
We refactor async pipe to fulfill strict and consistent undefined handling as described in Input Bindings.
Also the mentioned call of detectChanges()
needs to be done inside.
Optional we could schedule side effects over requestAnimationFrame
.
Input Bindings don't fire after the initial render. The code below shows a setup where we can test this.
...
@Component({
selector: 'minimal',
template: `
<p>@Input() value: {{value$ | async}}</p>
`
})
export class MinimalComponent {
value$ = new ReplaySubject<string>(1);
@Input() set value(v: string) {
console.log('setter fired with:', v);
this.value$.next(v);
};
}
The setter for the value
property is only called once, no matter how often we change the input value.
Also a switch to ChangeDetectionStrategy.OnPush
behaves in the same way.
The solution is to use the above explained push pipe to trigger change detection after the value arrived an the input binding:
<minimal
[value]="observable$ | push">
</minimal>
NOTICE Important to not here is that this external value change gets rendered also into the child components view. This means every change trigger over a input binding automatically works run zone-less.
Output Bindings fire even without zone.js. Below you see the sample:
@Component({
selector: 'minimal',
template: `
<button (click)="update$.next($event)">trigger output</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MinimalComponent {
update$ = new Subject();
@Output() update = this.update$;
}
This means the function is fired in the parent. You can process it but if you want to render the value you again have to use the push
pipe:
@Component({
selector: 'app-root',
template: `
<code>{{update$ | push}}</code>
<minimal
(update)="log($event)">
</minimal>
`
})
export class AppComponent {
update$ = new Subject();
log(v) {
console.log('processing possible:', v);
this.update$.next(v.clientY);
}
}
Similar to output bindings dom events work without zone.js.
In the following example we see that the DOM event get's processed in setFocus
.
@Component({
...
template: `
<input
(focus)="setFocus(true)"
(blur)="setFocus(false)">
`
...
})
export class MinimalComponent {
focus = false;
setFocus($event) {
console.log('setFocus triggered', $event);
this.focus = $event;
}
}
If we want to render it we solve it in the same way as we did with output bindings.
We simple use the push
pipe.
@Component({
...,
template: `
focus: {{focused | push}}
<input
(focus)="setFocus(true)"
(blur)="setFocus(false)">
`,
...
})
export class MinimalComponent {
focused = new BehaviorSubject<boolean>(false);
setFocus($event) {
this.focused.next($event);
}
}
Another thing we could do is forward the state directly as output binding. Here we already know we dont need to care about change detection.
@Component({
...,
template: `
{{focused | async}}
<input
(focus)="focused.next(true)"
(blur)="focused.next(false)">
`,
...
})
export class MinimalComponent {
@Output() focused = new BehaviorSubject<boolean>(false);
}
Now the parent has to take care about chane detection and we are out of the game ;). We can even use the async pipe because if change detection get's triggered from parent it will update.
Animations in general work fine. They only need to be triggered. When we process any value coming from input bindings everything animates properly. As mentioned before values from input bindings are our save path.
Here a example opening/closing a box over height transition:
@Component({
selector: 'app-open-close',
animations: [
trigger('openClose', [
state('open', style({height: '200px'})),
state('closed', style({height: '100px'})),
transition('open => closed', [animate('1s')]),
transition('closed => open', [animate('0.5s')]),
]),
],
template: `
<div [@openClose]="isOpen ? 'open' : 'closed'" class="open-close-container">
<p>The box is now {{ _isOpen ? 'Open' : 'Closed' }}!</p>
</div>
`,
styles: [`
.open-close-container {
background-color: green;
}
`]
})
export class OpenCloseComponent {
@Input() isOpen: boolean;
}
As you can see to trigger the animation we use normal template bindings [@openClose]="_isOpen ? 'open' : 'closed'"
,
no push
pipe needed.
If we want to trigger the animation on click without values over input bindings is stops working:
...
@Component({
selector: 'app-open-close',
animations: [...],
template: `
<div (click)="toggle()" [@openClose]="isOpen ? 'open' : 'closed'" class="open-close-container">
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
</div>
`,
styles: [...]
})
export class OpenCloseComponent {
isOpen: boolean = true;
toggle() {
this.isOpen = !this.isOpen;
}
}
Here we need to introduce some changes.
We could use the push
pipe or trigger change detection based on dom events.
With the push
pipe it looks like this:
...
@Component({
...
template: `
<div (click)="toggle()" [@openClose]="(isOpen$ | push$) ? 'open' : 'closed'" class="open-close-container">
<p>The box is now {{ (isOpen$ | push$) ? 'Open' : 'Closed' }}!</p>
</div>
`,
...
})
export class OpenCloseComponent {
isOpen$$ = new BehaviorSubject<any>('');
isOpen$ = this.isOpen$$
.pipe(scan(acc => !acc, false));
toggle() {
this.isOpen$$.next('');
}
}
The approach with events depends on the given situation.
In best case we have access to the component and can implement a call of .detectChanges()
if needed:
...
@Component({
...
template: `
<div (click)="toggle()" [@openClose]="isOpen ? 'open' : 'closed'" class="open-close-container">
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
</div>
`,
...
})
export class OpenCloseComponent {
isOpen: boolean = true;
toggle() {
this.isOpen = !this.isOpen;
this.cd.detectChanges();
}
constructor(private cd: ChangeDetectorRef) {
}
}
If this is not possible, think about third party components or directives, we need to go another path.
As the animation is triggered over a click event we can apply a event binding for the same event to our component from the parent view and trigger change detection from there.
@Component({
selector: 'app-root',
template: `
<app-open-close (click)="cd.detectChanges()"></app-open-close>
`
})
export class AppComponent {
constructor(private ngZone: NgZone, private cd: ChangeDetectorRef) {
console.log('ngZone', this.ngZone);
}
}
To things are worth to mention here. First we can imaging when we have many different events that trigger animations (or other internal processes) we end up in a very long and bulky snippet.
Second this workaround is not working for all cases. Imagine a focus event would be a trigger. This would simply not work with the above solution.
Needs: Abstract change detection triggering of multiple events into a directive. The component can stay free from any additional imports or logic.
We can run any kind of logic internally and don't have to think about zone.js
.
Communicate with services, other parts of the component. Only if we want to render
something to the view we have to consider change detection.
As we already know how to use observables we put the data to render in a stream and use the push
pipe to trigger rendering.
This is exactly the same thing we nearly always did so far.
Let's look at a simple exampes where we render the actual time to the view:
@Component({
...,
template: `{{time$ | push}}`,
...
})
export class MinimalComponent {
time$ = new interval(1000)
.pipe(map(_ => {
const d = new Date();
return d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
}));
}
That easy. :)
@TODO
WebComponent Template bindings, output bindings, internal logic, dom events as well as animations behave in the same way as they do for normal Angular components.
The differences:
Input bindings however, behave a bit different. We see no values entering until we trigger change detection.
But this is not a big deal as we are already used to use the push
pipe.
If we use it for input bindings it works: <web-component [value]="observable$ | push"><web-component>
As a lot of problems are related to timing issues this section is here to give a comlete overview of all the different types of issues.
Two different problems are occurring in multiple different situations:
Incoming values arrive before the subscription has happened.
For example state over @Input()
decorators arrives before the view gets rendered and a used pipe could receive the value.
@Component({
selector: 'app-late-subscriber',
template: `
{{state$ | async | json}}
`
})
export class LateSubscriberComponent {
state$ = new Subject();
@Input()
set state(v) {
this.state$.next(v);
}
}
We call this situation late subscriber problem. In this case, the view is a late subscribe to the values from '@Input()' properties. There are several situations from our previous explorations that have this problem:
@Input
to AfterViewInit
hook@Input
to the view@Input
to the constructorOnChanges
to the viewSolutions
All those problems boil down to 2 different solutions depending on the particular problem.
ReplaySubjects
with bufferSize
of 1
to cache the latest sent valueshareReplay
for referential sharing as shown in Sharing References over Observables
The subscription happens before any value can arrive.
For example, subscriptions to view elements the constructor happen before they ever exist. We call this situation early subscriber problem. In this case, the component constructor is an early subscribe to the events from '(click)' bindings.
All above decorators should rely on a generic way of wrapping a function or property as well as a way to configure the used Subject for multi-casting similar to multicast
In this way, it is easy to have a simplified public API but flexibility internally.
Decorators that:
multicast
operator.TBD
As discussed in Automate boilerplate a lot of things that are related to angular can be solved by the right decorator. But there are other areas where we need to provide some solutions. A more general one than just life cycle hooks of a single component.
The problem of connecting all component bindings, global state, locally provided services, view events and The main reason here is getting the values over View elements that are instantiated later.
TBD
Automate boilerplate of setting up a subject and connecting it to a producer.
Here a configuration method for the type of Subject
similar to the one from multicast would be nice.
In a majority of the cases, there was a need for abstracting away the boilerplate of setting up a subject and connecting it to the producer. A normal Subject
was used in most of the cases. Some cases used a ReplaySunject
or BeHaviorSubject
to initialize the value. This was used to provide the latest value for a new subscriber.
Here we think one or many component property/method decorator can help.
Decorators that:
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:
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:
AnimationFrameScheduler
(on by default)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:
*ngIf="{}"
normally effects it)async
pipeAnimationFrameScheduler
(on by default)A property decorator which turns a lifecycle method into an observable and assigns it to the related property.
The decorator should work as a proxy for all life cycle hooks @hook$('onInit') onInit$;
as well as forward passed values i.e. changes
in from the OnChanges
hook.
@hook$('onInit') onInit$;
@hook$('onDestroy') onDestroy$;
this.onInit$
.pipe(
switchMapTo(interval(1000)),
map(_ => Date.now()),
takeUntil(this.onDestroy$)
)
.subscribe();
Included Features
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 {
@hook$('onChanges')
onChanges$: Observable<SimpleChanges>;
@Input() state;
state$ = this.onChanges$.pipe(getChange('state'));
}
Following things are done under the hood:
currentValue
from SimpleChanges
objectA 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 {
@Input$()
input$;
}
Following things are done under the hood:
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 #elem">clicks: {{count$ | async}}</button>`,
})
export class ChildComponent {
@FromView$('#elem', 'click')
click$;
count$ = this.click$.pipe(scan(a => ++a, 0));
}
Following things are done under the hood:
Here a link to a similar already existing ideas from @elmd_: https://www.npmjs.com/package/@typebytes/ngx-template-streams
This extension is maybe the most interesting one. While we can
A tiny logic that combines:
A way to connect events from the view and component as observable.
constructor(private lS: LocalState<MyState>) {
this.lS
.connectSlice('num', interval(1000));
this.lS
.connectSlice('isNew', this.isNew$);
this.lS
.connectSlice('buttons', this.buttons$);
}
A flexible way to query one or many state slices. It considers also a late subscriber.
An operators selectSlice
to select a specific slice from the managed state.
This operator can be used to get slices from this.lS$
.
buttons$ = this.lS.state$
.pipe(
selectChange(['state', 'substate'])
);
Following things are done under the hood:
shareReplay(1)
The cdOn
directive serves a convenient way of triggering change detection for multiple events.
It is only used to solve edge cases in zone-less applications,
by taking away bulky templates and externalizing ChangeDetectionRef
handling.
The current way of workaround looks like that:
<my-component
(click)="cd.detectChanges()"
(focus)="cd.detectChanges()"
(blur)="cd.detectChanges()"
(input)="cd.detectChanges()">
</my-component>
The cdOn
directive take over the multiple bindings and reduce them to a single input binding.
[cdOn]="['eventName']"
. Also the import of ChangeDetectionRef can be deleted now.
<my-component
[cdOn]="['click','focus','blur','input']">
</my-component>
Included Features:
AnimationFrameScheduler
TBD
There are situations where we have to compose multiple streams, compute new state and create a reference to some object, i.e. a FromGroup
. This reference is then later on shared with multiple subscribers.
Such situations are handled by mutation a component property in imperative programming. In reactive programming, we solve this multicasting.
Problem of beeing shared references
@Component({
selector: 'app-sharing-a-reference',
template: `
<h2>Sharing a reference</h2>
<p><b>default$:</b></p>
<form *ngIf="(formGroup$ | async$) as formGroup" [formGroup]="formGroup">
<div *ngFor="let c of formGroup.controls | keyvalue">
<label>{{c.key}}</label>
<input [formControlName]="c.key"/>
</div>
</form>
`
})
export class SharingAReferenceComponent {
state$ = new ReplaySubject(1);
@Input()
set formGroupModel(value) {
this.state$.next(value);
}
@Output() formValueChange = new EventEmitter();
formGroup$: Observable<FormGroup> = combineLatest(this.state$, this.router.params)
.pipe(
map(this.preparingFormGroupConfig),
map(config => this.fb.group(config))
);
constructor(
private fb: FormBuilder,
private router: ActivatedRoute
) {
this.formGroup$
.pipe(
switchMap((fg: FormGroup) => fg.valueChanges)
)
.subscribe(v => this.formValueChange.emit(v));
}
preparingFormGroupConfig([modelFromInput, modelFromRouterParams]) {
// override defaults with router params if exist
return Object.entries({...modelFromInput, ...modelFromRouterParams})
.reduce((c, [name, initialValue]) => ({...c, [name]: [initialValue]}), {});
}
}
As we see the provided example is not working. The reason for this is we subscribe multiple times to the formGroup$
.
One time in the template to render the form, the second time in the constructor to forward form value changes to the EventEmitter
.
Because the formGroup$
observable is cold (every subscriber receives a unique producer) we instantiate the FormGroup
once per subscription.