7

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
}
2
  • 1
    As the pipe is the correct place to resolve side effects of the observable that is not a hack, it's just a legit approach to it. Or you could do the wrapping inside the map operator. You can use something like : map((result) => ({isSuccess : !!result, result})) Commented Dec 15, 2021 at 17:39
  • 1
    I think your approach is fine. You might want to use mapTo(true) to save a few characters and show off your rxjs knowledge ;) Commented Dec 15, 2021 at 21:59

4 Answers 4

5

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>
Sign up to request clarification or add additional context in comments.

Comments

3

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>

Comments

1

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.

3 Comments

Where in your example should I run the side effects if the getCurrentBranding() returns something truthy?
@Poul, another approach is use *ngIf to store the result in a variable, see the docs
@SamWillis I've added the tap
0

Similar to eliseo answer, I have wrapped the boolean in an Optional (Java style):

featureFlag$: Observable<Optional<boolean>>;

Then in template:

<div *ngIf="featureFlag$ | async as featureFlag">
  <div ngIf="featureFlag.get()">
</div>

In my case, my main publisher was a boolean promise.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.