In an Angular project, I have made a component to encapsulate the logic of a submit request page (code below).
This page has two subcomponents that define specific forms (firstForm and secondForm) and a submit button.
Each subcomponent has an input for the disabled state
@Input() set disabled(value: boolean | null) {
if (value === null) {
value = false;
}
this._disabled = value;
this._onDisabledChange();
}
and an output to emit if the form is valid or not:
@Output() onValidChange: EventEmitter<boolean> = new EventEmitter();
I want that to enable the following form, the previous ones have to be valid. These would be the enabled dependencies:
first-form -(depends-on)-> no one
second-form -(depends-on)-> first-form
submit button -(depends-on)-> first-form & second-form
When first-form and second-form are completed (submit enabled), and I delete one of first-form's required input (so first-form is invalid), this happens:
I would like that submit disabled style updated instantly.
I am using changeDetection: ChangeDetectionStrategy.OnPush
. As a test I used Default
strategy, and the console showed the error ExpressionChangedAfterItHasBeenCheckedError
.
As this is a complex problem, I have simplified the code, leaving only the relevant parts.
Components have another EventEmitter to emit the form values, but here I want to focus on the valid state.
<div>
<first-form (onValidChange)="firstFormValid = $event"></first-form>
<second-form
[disabled]="!(isSecondStepEnabled$ | async)"
(onValidChange)="secondFormValid = $event"
></second-form>
<div [ngClass]="{ disabled: !(isSubmitStepEnabled$ | async) }">
<ul>
<li>
<button
id="submit"
[disabled]="!(isSubmitStepEnabled$ | async)"
(click)="submit()"
>
Submit
</button>
</li>
</ul>
</div>
</div>
export class PageComponent {
private _isFirstFormValid$: BehaviorSubject<boolean> = new BehaviorSubject<
boolean
>(false);
private _isSecondFormValid$: BehaviorSubject<boolean> = new BehaviorSubject<
boolean
>(false);
set firstFormValid(valid: boolean) {
this._isFirstFormValid$.next(valid);
}
set secondFormValid(valid: boolean) {
this._isSecondFormValid$.next(valid);
}
get isSecondFormValid$(): Observable<boolean> {
return this._isSecondFormValid$.asObservable();
}
isFirstStepCompleted$: Observable<
boolean
> = this._isFirstFormValid$.asObservable();
isSecondStepEnabled$: Observable<
boolean
> = this._isFirstFormValid$.asObservable();
isSecondStepCompleted$: Observable<boolean> = combineLatest([
this.isSecondStepEnabled$,
this.isSecondFormValid$
]).pipe(map(this._areAllTrue()));
isSubmitStepEnabled$: Observable<boolean> = combineLatest([
this.isFirstStepCompleted$,
this.isSecondStepCompleted$
]).pipe(map(this._areAllTrue()));
private _areAllTrue(): (value: boolean[]) => boolean {
return (values: boolean[]) => {
return values.every(valid => valid === true);
};
}
submit() {
// call external service
}
}
Angular CLI: 9.0.4
Node: 13.6.0
OS: darwin x64
Angular: 9.0.4
rxjs: 6.5.4
typescript: 3.7.5
webpack: 4.41.6
The only solution I found was using delay(0)
on the Observable isSubmitStepEnabled$
.
isSubmitStepEnabled$: Observable<boolean> = combineLatest([
this.isFirstStepCompleted$,
this.isSecondStepCompleted$
]).pipe(delay(0), map(this._areAllTrue()));
I think it is not a good solution and maybe I have a structure problem.
Thanks everyone!
Here's the solution I have found (without using delay
function):
Simply replace asObservable
(when I want to get the stream from the behaviourSubject) with .pipe(shareReplay({ refCount: true, bufferSize: ONCE }))
.
The difference between these two are:
asObservable
a new subscription is being created each time it is invoked (by a "listener").shareReplay
, it's like you have one subscription and its data is broadcasted to all your "listeners"With asObservable
, my code didn't run change detection until I clicked outside the input.