So. I'm trying to pre-populate a reactive form with async data that will be changing a lot. The asyncData$ is fed from the AppComponent to an @input() in a child component - FormComponent - which handles the form.
Subscribing inside the tap() operator of the dataForm$ was the ONLY way i could make valueChanges work and actually emit values, BUT nested subscriptions is a no no, so i would like to find a better/cleaner solution which avoids that - preferably by using observables. It may jsut be that I am missing something obvious
Does anybody have a suggestion?
The form itself has to be an observable
<div>
<app-form [asyncData]="asyncData$ | async"></app-form>
</div>
export class AppComponent implements OnInit {
title = 'observableForm';
asyncData$: ReplaySubject<Data> = new ReplaySubject<Data>(1);
ngOnInit(): void {
setTimeout(() => this.asyncData$.next( {
id: 42,
name: 'Han Solo',
heShotFirst: true
} as Data), 2000);
}
}
<div *ngIf="(dataForm$ | async) as dataForm">
<form [formGroup]="dataForm" style="padding: 5rem; display: grid; place-items: center;">
<div>
<label>
Id
<input type="text" [formControlName]="'id'">
</label>
<label>
Name
<input type="text" [formControlName]="'name'">
</label>
<br>
<label>
He shot first
<input type="radio" [value]="true" [formControlName]="'heShotFirst'">
</label>
<label>
He did not
<input type="radio" [value]="false" [formControlName]="'heShotFirst'">
</label>
</div>
</form>
<div *ngIf="(lies$ | async) === false" style="display: grid; place-items: center;">
<h1>I Must Not Tell Lies</h1>
</div>
</div>
export class FormComponent implements OnInit, OnDestroy {
@Input() set asyncData(data: Data) { this._asyncData$.next(data); }
_asyncData$: ReplaySubject<Data> = new ReplaySubject<Data>(1);
dataForm$: Observable<FormGroup>;
valueChangesSub$: Subscription;
lies$: Subject<boolean> = new Subject<boolean>();
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
this.dataForm$ = this._asyncData$.pipe(
map(data => {
return this.fb.group({
id: [data?.id],
name: [data?.name],
heShotFirst: [data?.heShotFirst]
});
}),
tap(form => {
if (this.valueChangesSub$ != null) {
this.valueChangesSub$.unsubscribe();
}
return form.valueChanges.subscribe(changes => {
console.log(changes)
this.lies$.next(changes.heShotFirst);
});
})
);
}
ngOnDestroy(): void {
this.valueChangesSub$.unsubscribe();
}
}
I wasn't certain why the form needed to be an Observable. I instead used OnChanges
to watch for changes from the parent component and valueChanges
to watch for changes to the form.
This is the resulting code:
import { Component, Input, OnInit, OnDestroy, OnChanges, SimpleChanges } from "@angular/core";
import { ReplaySubject, Observable, Subscription, Subject } from "rxjs";
import { Data } from "./data";
import { FormGroup, FormBuilder } from "@angular/forms";
import { map, tap } from "rxjs/operators";
@Component({
selector: "app-form",
templateUrl: "./form.component.html"
})
export class FormComponent implements OnInit, OnDestroy, OnChanges {
@Input() asyncData: Data;
dataForm$: Observable<FormGroup>;
dataForm: FormGroup;
valueChangesSub: Subscription;
lies$: Subject<boolean> = new Subject<boolean>();
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.dataForm = this.fb.group({
id: "",
name: "",
heShotFirst: false
});
this.valueChangesSub = this.dataForm.valueChanges.subscribe(changes => {
console.log(changes);
this.lies$.next(changes.heShotFirst);
});
}
ngOnChanges(changes: SimpleChanges) {
let currentValue: Data = changes.asyncData.currentValue;
console.log("changes", currentValue);
if (currentValue !== null) {
this.dataForm.patchValue({
id: currentValue.id,
name: currentValue.name,
heShotFirst: currentValue.heShotFirst
});
}
}
ngOnDestroy(): void {
this.valueChangesSub.unsubscribe();
}
}
I have a stackblitz of this here: https://stackblitz.com/edit/angular-async-form-deborahk
Using the OnChanges
, the child component gets notified each time a new value is emitted into the parent's stream. (I attempted to mock this by setting a second setTimeout
in the parent component.)
Then the valueChanges
on the form seems to work as expected.
If this isn't quite what you were looking for, feel free to fork the stackblitz and modify it as needed to demo your issue.