Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular display template if observable is falsey with async pipe

I have an API call that may return some data or may return something falsey if no data exists. If there is data, I want to tap out of the stream and do some side effects, but if falsey, I want no side effects to happen but still display my template code.

I am using an async pipe to get that data in the template, but if the data is falsey, it will not display.

I have seen the technique to wrap the ngIf with an object so it evaluates to truthy, but it doesn't seem the correct solution for my code.

My template:

<form *ngIf="loadedData$ | async">
  ...
</form>

My class:

loadedData$: Observable<boolean>;

ngOnInit(): void {
  this.loadedData$ = this.getCurrentBranding().pipe(
    tap(branding => {
      if (branding) {
        // Do side effects
      }
    }),
    // current hack to make the template show
    map(() => true)
  );
}

private getCurrentBranding(): Observable<Branding> {
  // API call, may return an object, or null
}
like image 363
Sam Willis Avatar asked Dec 15 '21 17:12

Sam Willis


People also ask

How do I use async pipe in angular?

Async Pipe and the Share Operator Another way to bind to async data in Angular is to use the async pipe. With the Async pipe, we get the benefit of Angular auto-subscribing and unsubscribing with our Observables when the component is created and destroyed. To use the async pipe, we bind our Observable directly to our component.

How do you handle asynchronous data in angular?

When building Angular applications, it’s likely you are working with Observables (specifically RxJS) to handle asynchronous data. Typically we get this async data through Angular’s Http service which returns an Observable with our data response.

What is the pipe method of the angular observable?

The pipe method of the Angular Observable is used to chain multiple operators together. We can use the pipe as a standalone method, which helps us to reuse it at multiple places or as an instance method. In this tutorial, we will take a look at the pipe and learn how to use it in an Angular Application.

How to unwrap falsy values from an observable using ngif?

As simple as it sounds, there's one (native) functionality of ngIf that not many people seems to be aware of. How to unwrap falsy values from an observable? Use ngIf itself from the template! As Angular plays really nicely with RxJs, you can take advantage of the ngIf directive when you have an observable by using the async pipe and the as syntax:


Video Answer


3 Answers

I've been getting in the habit of having a single observable for my component that merges multiple streams, combining them into one state. Usually this involves the scan operator, but in this case it's not necessary (though maybe you want to merge in these side effects too).

In the code below getCurrentBranding will execute when it is subscribed to by the async pipe in the component. To prevent multiple executions you can have one root element that uses Angular's as expression to convert the emission into a template variable. Alternatively, you could use shareReplay.

The startWith operator is used to provide an initial value, so the root element will always be displayed. The form will initially be hidden until the result is returned from the api. The data will come from the same observable and access from the template variable created at the root element.

this.state$ = this.getCurrentBranding().pipe(
  tap(branding => { /* Do side effects */ ),
  map(data => ({ data, isLoaded: true}),
  startWith(({ data: [], isLoaded: false})
);
<ng-container *ngIf="(state$ | async) as state">
  <form *ngIf="state.isLoaded">
    <!-- ... -->
  </form>
  <ol *ngFor="let o of state.data">
    <!-- ... -->
  </ol>
</ng-container>
like image 196
Daniel Gimenez Avatar answered Oct 16 '22 22:10

Daniel Gimenez


the "typical" is use an object as "if" in the way

<!--see that the if is always "true" -is an object- -->
<form *ngIf="{response:loadedData$ | async} as res">
   <!--you has the response in res.response-->
   <ng-container *ngIf="res.response===false">
      the result of the call is false
   </ng-container>
   <ng-container *ngIf="res.response!===false">
    {{res.response|json}}
   </ng-container>
</form>
like image 40
Eliseo Avatar answered Oct 16 '22 23:10

Eliseo


You can always try to keep it as semantic as possible. Your approach using map is not a bad idea, it's just that you actually lose the access to your data. It's probably better to split it up in two Observables:

readonly data$: Observable<BrandingData> = this.getCurrentBranding().pipe(
  tap((branding) => { /* Do side effects */ }),
  shareReplay({ refCount: true, bufferSize: 1 })
);

readonly loadingData$: Observable<boolean> = this.data$.pipe(
  mapTo(false),
  startWith(true)
);

You can then show your form by checking if the data is being loaded:

<form *ngIf="(loadingData$ | async) === false"></form>

What happens here is that by subscribing to the loadingData$ in the template, actually also subscribes to the data$ osbervable, which will trigger the API request. By using shareReplay, you make sure that this request is only done once on every subscribe.

like image 1
Poul Kruijt Avatar answered Oct 16 '22 23:10

Poul Kruijt