3

Note this is simplified question of Angular template binding with Observable async pipe issue

template:

<div>{{foo()$ | async}}</div>

source code:

import { Component } from "@angular/core";
import { BehaviorSubject, of, Observable } from "rxjs";
import { tap, delay, map, switchMap, concatMap } from "rxjs/operators";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  private index = 0;
  foo$(): Observable<any> {
    console.log("aaa")
    return of("Delayed");
  }
}

The above code works as expected:

enter image description here

However if I added .pipe(delay(1)) to the foo$():

  foo$(): Observable<any> {
    return of("Delayed").pipe(delay(1));
  }

it won't work and keep "aaa" in the console log.

See https://stackblitz.com/edit/angular-qbhkg3

Gelin Luo
  • 14,035
  • 27
  • 86
  • 139
  • I really didn't get your point. It seems to me that it's working as expected. `console.log` is being executed right after the function call no matter what you have in your returning observable (BTW, I'm supposing you forget it on your second example, the one using `pipe(delay))`). – julianobrasil May 13 '20 at 11:05

1 Answers1

11

A method called from the template, is called every change detection cycle. Because you are using the async pipe, the change detection is triggered with every emit. So basically you are creating an infinite loop and that's why it will never show a value, because the change detection never finishes:

  1. on view init template calls foo$()
  2. foo$() creates a -new- observable and delays the emit
  3. the observable emits after the delay
  4. the emit triggers a change detection from within the async pipe
  5. change detection calls foo$() from the template, and we're back to step 2

It's not really clear what you are trying to achieve, but I don't think you should return Observables from methods to be consumed in templates. These should be readonly class field:

readonly foo$ = of("Delayed").pipe(delay(1));

It is however possible to use a method, but you have to make sure this method returns the same Observable:

private readonly _foo$: Observable<any> = of("Delayed").pipe(delay(1));

foo$(): Observable<any> {
  console.log('here');
  return this._foo$;
}

example

Because the observable object stays the same (===), it's all good in the hood. Once you add a pipe to the Observable you create a new reference and you come back into the infinite loop.


The reason it does not reach the infinite loop if you just return of('delayed'), is because the Observable is not asynchronous this way. The Observable will return a value immediately to the async pipe, and when the async pipe calls detectChanges() nothing really happens, because it's still in the same cycle as the change detection cycle which triggered the foo$() template call.


I see you also linked a previous question you posted which involves the use of a decorator. You should change that decorator to a class field decorator, and you can then do the following:

@NeedsElement(sp(115621), ap(116215))
readonly insuredType$!: Observable<string>;

I think I can think of a way to make it work with a method call, but before I dive into that, I first want to know why you want it to be a method call in the first place

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • @ArunMohan you can unremove it if you want. The proposition of using `OnPush` is still correct and should be encouraged. Just because an answer is 'wrong', doesn't mean it might not help others :D – Poul Kruijt May 13 '20 at 11:12
  • @PoulKruijt I actually started with the property decorator, but I found it cannot access the host information, that I fall back to the method decorator – Gelin Luo May 13 '20 at 11:33
  • @GelinLuo have you found a way to do it with a property decorator? – Poul Kruijt May 13 '20 at 12:25
  • @PoulKruijt yes I get it done with Property Decorator. All good now. Thank you very much! Honestly I think typescript should enhance Property decorator mechanism so it can access the host data, e.g. to access other properties of the host – Gelin Luo May 13 '20 at 22:10
  • Hi @PoulKruijt, in case I am doing a dynamic page rendering, say all fields coming from a configurable JSON/YAML file, then I will need to make the obserable returning from a method. Do you have any idea on how to make that work? – Gelin Luo May 14 '20 at 10:11
  • What would you like to access from the 'host data'. What do you mean with 'host', and what with 'data'? And dynamic page rendering sounds kinda anti pattern in angular, but I could be wrong. And to answer your question with returning from method, on the other question, satanTime has the right answer, you have to declare the observable outside of the descriptor to be sure the same instance is returned – Poul Kruijt May 14 '20 at 10:23
  • @PoulKruijt Thanks for comment. I've accept satanTime's answer and applied it in my code. with regarding to host, it is the object instance that host the property with property decorator. E.g. suppose I have a decorator on `Foo.x` in which I want to access `foo.y`, the problem is I don't have access to the instance `foo` in the property decorator of `Foo.x`. I didn't know that dynamic page rendering is an anti pattern, I am new comer to angular. What I want to do is to define a JSON spec of a page, and render the page based on the spec – Gelin Luo May 14 '20 at 23:53
  • Could you elobarate on why can't async pipe "paint" a received value from `foo$()` in the step 5, before move back to the step 2? Why does it just start to wait the next observable instead, "forgetting" about the first value? – Ilya Loskutov Aug 09 '22 at 16:03
  • @IlyaLoskutov because using the `pipe` function will return a new `Observable` object. On change detection angular checks if a value has changed, and the `getter` will return a new object, so angular sees this as a change. If you don't add a pipe, it returns the same observable object – Poul Kruijt Aug 11 '22 at 09:30