Reputation: 5493
I have an Observable
defined in my component file. It is updating appropriately when interpolated with double curlys ({{example}}
). But it is not updating inside the template directive, even though I am using an async
pipe.
component.html
<ng-container *ngIf="isLoading$ | async as isLoading; else elseBlock">
is loading
</ng-container>
<ng-template #elseBlock> Add</ng-template> <--- constantly showing elseblock; not working!
is loading: {{ isLoading$ | async }} <--- is working correctly
component.ts
updateIsLoading: any;
isLoading$ = new Observable((observer) => {
observer.next(false);
this.updateIsLoading = function (newValue: boolean) {
observer.next(newValue);
observer.complete();
};
});
handleClick() {
this.updateIsLoading(true); <--- running this line updates interpolated value, but not the if statement
}
Apparently, commenting out the second async
makes the first behave appropriately.
Upvotes: 1
Views: 1912
Reputation: 30088
This will probably be an unpopular answer, but I prefer to keep the templates really simple, and put any necessary complexity in the component, where the full expressiveness of ts/js is available.
In this case, I think that it would be easier / more understandable / maintainable to subscribe in the component, and update a property that can easily be used multiple times in the template without affecting the subscription.
IMHO, the async pipe is just unnecessarily complicating things here.
TLDR: Keep templates simple, and keep their logic simple.
Upvotes: 0
Reputation: 10994
This is just a misunderstanding of the async
pipe and/or Observables.
Each instance of isLoading$ | async
creates a separate subscription.
This subscription will execute the callback function, overwriting this.updateIsLoading
with a new function.
So your click handler will only ever fire observer.next(newValue)
for the last isLoading$ | async
subscription.
Ideally you just want to call isLoading$ | async
once and put it into a template variable.
Unfortunately Angular doesn't have a built in directive to just declare a single template variable. Although you can write your own, and there are some packages out there like ng-let
.
You can wrap everything in *ngIf
with as
to get a template variable, but that doesn't work if you want to allow falsey values through.
You can use the ng-template
let-*
syntax to accomplish it. The idea is to define a template, and pass in your async variables as parameters via ngTemplateOutletContext
. The actual rendering is done by an ng-container
.
<ng-container
[ngTemplateOutlet]="myAsyncTemplate"
[ngTemplateOutletContext]="{isLoading: isLoading$ | async}"
></ng-container>
<ng-template #myAsyncTemplate let-isLoading="isLoading">
<ng-container *ngIf="isLoading; else elseBlock">
<p>is loading: {{isLoading}}</p>
</ng-container>
<ng-template #elseBlock> Else Block</ng-template>
<p>is loading: {{isLoading}}</p>
<button (click)="handleClick()">SET TO TRUE</button>
</ng-template>
Stackblitz: https://stackblitz.com/edit/angular-x4msbz?file=src/main.html
Alternatively you can turn the observable into a shared stream like Sergey suggested. That'll let you make any number of subscriptions which all share the same value. Depends on your use case.
Upvotes: 1
Reputation: 4600
Answer is a bit simple and shady at the same time. Here is a small hint:
updateIsLoading: any;
isLoading$ = new Observable((observer) => {
console.log("Created!");
observer.next(false);
this.updateIsLoading = function (newValue: boolean) {
observer.next(newValue);
observer.complete();
};
});
Created
will be logged twice in the console. So each time you call | async
on this Observable - the function you passed to the constructor is executed, and updateIsLoading
is overwritten, so only last |async
binding is working.
So if you want to have 2 async
pipes - use Subject
.
isLoading$ = new Subject<boolean>();
updateIsLoading = (value: boolean) => this.isLoading$.next(value);
Note: there is no initial value in Subject, so in the is loading: (value)
the value will be empty string.
OR you can use share()
operator:
isLoading$ = new Observable((observer) => {
this.updateIsLoading = function (newValue: boolean) {
observer.next(newValue);
observer.complete();
};
}).pipe(share(), startWith(false));
Will work as "expected".
Additional details: What is the difference between Observable and a Subject in rxjs?
Upvotes: 2