I feel a bit embarrassed asking this but I'm sure I'm missing something. Spent ages looking/researching but only come up with complex and convoluted solutions requiring subject or behavioursubjects or pipe through mergeMap, etc.
I'm moving from an imperative HTTP observable approach (subscribing manually) to a reactive HTTP observable approach (observable$ | async) in the component HTML template.
No problems getting it working with the async pipe when the page is initialised.
However, what is the best way to refresh the template "on the fly", i.e. I know that I've inserted a record into the underlying database and want this to be reflected in my template.
With the imperative approach I could simply subscribe again (from a click handler for example) and reassign the result to the property associated with the ngfor directive and change detection would fire and DOM updated.
The only solution I have come up with is to simply get the observable again from the service and this triggers change detection.
Just to make it clearer here is an exert of code (very simple just to focus on the solution).
There has to be a simple solution?
<div *ngIf="(model$ | async)?.length > 0" class="card mb-3 shadow">
<div class="card-header">Actions Notes</div>
<div class="card-body">
<mat-list *ngFor="let action of model$ | async">
<div class="small">
<strong>
{{action.createdDate | date:'dd/MM/yyyy HH:mm'}}
{{action.createdBy}}
</strong>
</div>
<div>{{action.notes}}</div>
</mat-list>
</div>
</div>
<button (click)="onClick()">name</button>
model$: Observable<any>;
constructor(
private changeTransferActionsService: ChangeTransferActionsService
) {}
ngOnInit(): void {
this.getObservable();
}
private getObservable(): void {
this.model$ = this.changeTransferActionsService
.getActionsWithNotesById(this.model.id)
.pipe(
map((x) => x.result),
share()
);
}
onClick(): void {
//insert row into database here (via HTTP post service call)
this.getObservable();
}
Just following up on my original question, here are three working solutions with explanations. Thanks to everybody that has responded.
Use an RxJS BehaviorSubject
with an arbitrary type value and initial value. Combine this with a higher order observable using flattening operator SwitchMap
.
When the button is clicked the BehaviourSubject
emits (next) a value (0 in this case), which is fed to the HTTP observable (passed value ignored) thus causing the inner observable to emit a value and change detection kicks in.
As a BehaviourSubject is used the observable emits a value on subscription.
In the component HTML template.
<button (click)="onClick()">Click Me</button>
In the component Typescript.
import { BehaviorSubject, Observable } from 'rxjs';
.
.
.
subject = new BehaviorSubject(0);
.
.
.
this.model$ = this.subject.asObservable().pipe(
// startWith(0),
switchMap(() => this.changeTransferActionsService
.getActionsWithNotesById(this.model.id)
.pipe(
map((x) => x.result)
)));
.
.
.
onClick(): void {
this.subject.next(0);
}
Use an RxJS Subject
Combine this with a higher order observable using flattening operator SwitchMap
.
When the button is clicked the Subject
emits (next) a value (undefined in this case), which is fed to the HTTP observable (passed value ignored) thus causing the inner observable to emit a value and change detection kicks in.
The Subject requires a startWith(0)
otherwise the observable won't emit any values when the component is first initialised and first subscribed to by async pipe.
import {Observable, Subject } from 'rxjs';
.
.
.
subject = new Subject();
.
.
.
this.model$ = this.subject.asObservable().pipe(
startWith(0),
switchMap(() => this.changeTransferActionsService
.getActionsWithNotesById(this.model.id)
.pipe(
map((x) => x.result)
)));
.
.
.
onClick(): void {
this.subject.next(0);
}
<button (click)="onClick()">Click Me</button>
Note that in both cases above the next method on the subject could have been invoked directly in the HTML component template thus had the subject had a public access modifier (the default anyway). This would have negated the need to have an onCLick method in the Typescript.
<button (click)="subject.next(0)">Click Me</button>
As suggested by hanan
Add a template variable name to the button and use @ViewChild
to get element reference. Create an RxJS observable using fromEvent
and then use switchMap
higher order observable approach.
.
.
.
@ViewChild('refreshButton') refreshButton: ElementRef;
.
.
.
ngAfterViewInit(): void {
const click$ = fromEvent(this.refreshButton.nativeElement, 'click');
this.model$ = click$.pipe(
startWith(0),
switchMap(_ => {
return this.changeTransferActionsService
.getActionsWithNotesById(this.model.id)
.pipe(
map((x) => x.result),
);
}));
}
.
.
.
<button #refreshButton>Click Me</button>
.
.
.
In conclusion, solution 3 makes the most sense if a refresh is to be kicked off at will by the user via a click as it hasn't necessitated the need to introduce a "dummy" subject.
However, the subject approach makes sense of if the observable should emit programmatically from anywhere...by simply calling the next method on the subject the observable can emit values on command.
You know, there is much more to this topic and a truly reactive approach may start off your thinking process about using shared injectable singleton services and subscriptions based around subjects/behavioursubjects/asyncsubjects and so on. These are architectural and design decisions though. Thanks for all your suggestions.
Async pipe does't trigger changeDetection and do not redraw value in UI.
The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.
Angular's async pipe is a tool to resolve the value of a subscribable in the template. A subscribable can be an Observable , an EventEmitter , or a Promise . The pipe listens for promises to resolve and observables and event emitters to emit values.
We can use the async pipe in Angular application by including the CommonModule which exports all the basic Angular directives and pipes, such as NgIf, NgForOf, DecimalPipe, and so on.
You can get refresh Btn reference and create stream from its click event and then start it with a dummy entry. Now you have a refresh stream, then switch it to your data stream like this:
Component
@ViewChild('refreshButton') refreshBtn:ElementRef;
model$: Observable<any>;
ngAfterViewInit() {
let click$ = fromEvent(this.refreshBtn.nativeElement, 'click')
this.model$ = click$.pipe(startWith(0),
switchMap(_=> {
return this.changeTransferActionsService
.getActionsWithNotesById(this.model.id)
.pipe(
map((x) => x.result),
);
}));
}
Template
Instead of using 2 async pipes use only 1 with ngLet
it will simplify your template and you don't have to use share()
:
<button #refreshButton >Refresh</button>
<ng-container *ngLet="(model$ | async) as model">
<div *ngIf="model.length > 0" class="card mb-3 shadow">
<div class="card-header">Actions Notes</div>
<div class="card-body">
<mat-list *ngFor="let action of model">
<div class="small">
<strong>
{{action.createdDate | date:'dd/MM/yyyy HH:mm'}}
{{action.createdBy}}
</strong>
</div>
<div>{{action.notes}}</div>
</mat-list>
</div>
</div>
<button (click)="onClick()">name</button>
</ng-container>
BUT Be pragmatic, don't use too much effort to make it fully reactive. Your earlier solution was also good, you just need improvements in your template.
Note: You can use *ngIf
in this case instead of *ngLet
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With