1

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
  }

Edit

Apparently, commenting out the second async makes the first behave appropriately.


smilebomb
  • 5,123
  • 8
  • 49
  • 81

3 Answers3

2

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?

Sergey Sosunov
  • 4,124
  • 2
  • 11
  • 15
1

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.

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
0

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.

GreyBeardedGeek
  • 29,460
  • 2
  • 47
  • 67
  • That works, but you have to remember to manually unsubscribe for certain observables. The ng-let package has the simplest solution, it lets you declare a template variable directly in the `ng-container` tag. So you can wrap your whole template in an ng-container and put your async pipes there. Then you've got a shared template variable with auto unsubscribing. It's probably fairly easy to write your own version of this too. Honestly not sure why this isn't a native feature rather than the clunky outlet context. – Chris Hamilton Jan 31 '23 at 16:43
  • or, you can use the takeUntil(destroyed) pattern, and then you don't have to add a library. Again, I think that folks are trying to cram too much logic into the 'view' (the template). That's what the component is for. – GreyBeardedGeek Jan 31 '23 at 18:08