Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refresh Data in Fully Reactive Way using angular async pipe

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.

Solution 1 - BehaviourSubject / SwitchMap

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);
  }

Solution 2 - Subject / SwitchMap / initialValue

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>

Solution 3 - fromEvent

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.

like image 209
Robin Webb Avatar asked Jul 10 '20 18:07

Robin Webb


People also ask

Does async pipe triggering change detection?

Async pipe does't trigger changeDetection and do not redraw value in UI.

What is sync and async pipe in angular?

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.

What does Asyncpipe do in angular?

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.

What type of data works with async pipe in angular?

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.


1 Answers

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

like image 173
hanan Avatar answered Sep 20 '22 15:09

hanan