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
}
CodePudding user response:
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(
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.
CodePudding user response:
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>
CodePudding user response:
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>