A little late but yes, this is possible. You can expose all of your component state through a single Observable<ViewModel>
and then use RxJs operators in the component to aggregate whatever is needed by your template. In your template you use the | async as model
and it feels a lot like 'normal' values.
Here's an example of a form that populates a div with data from a service on button click. Notice that there are no RxJs Subscriptions
or hooks into the angular lifecycle like OnInit
. Nothing happens until subscription, which angular handles, so its fine to do all this in our constructors. I'd go so far to say that if you can't make your template work with readonly
members you aren't really reactive.
Also note the single submitting$
drives everything: spinners, http request, enabled states, etc all 'react' to that one input. That means all those things stay in sync by default and we can do things like debounce in that one place. In this case combineLatest
makes it easy but there's usually some operator that does what you need :)
import { Injectable, Component, ChangeDetectionStrategy } from '@angular/core';
import { BehaviorSubject, combineLatest, filter, map, switchMap, tap, Observable, startWith, of, delay } from 'rxjs';
import { FormBuilder } from '@angular/forms';
@Injectable({ providedIn: 'root' })
export class ExampleService {
private count = 0;
getExampleData(request: ExampleRequest): Observable<ExampleData> {
return of({
userName: request.userName,
secret: "" + this.count++
}).pipe(delay(2000));
}
}
@Component({
selector: 'app-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ng-container *ngIf="viewModel$ | async as model">
<form [formGroup]="exampleForm"
(ngSubmit)="onSubmit()">
<div>
<label>
User Name:
<input formControlName="userName"
placeholder="User Name">
</label>
</div>
<button type="submit">Send</button>
</form>
<hr>
<ng-template *ngIf="model.submitting;else foo">
Submitting...
</ng-template>
<hr>
<ng-template #foo>
User Details:
<ul>
<li>User Name: {{model.exampleData.userName}}</li>
<li>User Secret: {{model.exampleData.secret}}</li>
</ul>
</ng-template>
</ng-container>
`,
styleUrls: ['./example.component.css']
})
export class ExampleComponent {
constructor(private fb: FormBuilder,
private exampleService: ExampleService) { }
exampleForm = this.fb.nonNullable.group({
userName: ''
})
private readonly submitting$ = new BehaviorSubject(false)
private readonly exampleResponse$ = this.submitting$.pipe(
filter(Boolean),
map(_ => this.exampleForm.value as ExampleRequest),
switchMap(exampleRequest => this.exampleService.getExampleData(exampleRequest)),
tap(_ => this.submitting$.next(false))
)
readonly viewModel$: Observable<ExampleComponentViewModel> = combineLatest(
[this.exampleResponse$.pipe(startWith({ userName: "", secret: "" })),
this.submitting$],
(exampleData, submitting) => ({ exampleData, submitting }))
onSubmit() {
this.submitting$.next(true)
}
}
export interface ExampleComponentViewModel {
exampleData: ExampleData
submitting: boolean
}
export interface ExampleRequest {
userName: string,
}
export interface ExampleData {
userName: string,
secret: string
}
I've obviously ignored things like errors, etc but this hopefully points you in the right direction.