"async" pipe not rendering the stream updates
Asked Answered
T

3

18

Trying to render the window size on window resize through a stream in an angular 2 component utilizing an async pipe:

<h2>Size: {{size$ | async | json}}</h2>

const windowSize$ = new BehaviorSubject(getWindowSize());
Observable.fromEvent(window, 'resize')
  .map(getWindowSize)
  .subscribe(windowSize$);

function getWindowSize() {
  return {
    height: window.innerHeight,
    width: window.innerWidth
  };
}

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div>
      <h2>Size: {{size$ | async | json}}</h2>
    </div>
  `,
  directives: []
})
export class App {
  size$ = windowSize$.do(o => console.log('size:', o));
  constructor() {  }
}

But the component only renders the initial state and ignores the stream updates. If you open the console, on window resize, you'll see the updates from that same stream.

Can't understand what i am missing here.

Here's a plunker

Toadstool answered 19/2, 2016 at 19:5 Comment(0)
T
15

The event handler is running outside the Angular zone, so Angular change detection doesn't run when an event fires. Put the event handler inside your component and then it will get monkey-patched along with all of the other asynchronous events, hence Angular change detection will execute after each event (and update the view):

ngOnInit() {
    Observable.fromEvent(window, 'resize')
     .map(getWindowSize)
     .subscribe(windowSize$);
}

Plunker


Another option, discussed in the comments, is to manually run change detection when a view model is updated:

import {Component, ChangeDetectorRef} from 'angular2/core'
...
export class App {
  size$ = windowSize$.do(o => {
     console.log('size:', o);
     // since the resize event was not registered while inside the Angular zone,
     // we need to manually run change detection so that the view will update
     this._cdr.detectChanges();
  });

  constructor(private _cdr: ChangeDetectorRef) {}
}

Plunker

Note that you might instead want to try running ApplicationRef.tick() once, say in your root component, which will run change detection on all of the components – rather than running ChangeDetectorRef.detectChanges() in each component. (And you might need to wrap tick() inside a setTimeout() method, to ensure that all of the component view models were updated... I'm not sure when all of the do() callback methods will be executed -- i.e., if they all run in one turn of the JavaScript VM, or if multiple turns are involved.)

Theurer answered 19/2, 2016 at 19:26 Comment(2)
Heh, this definitely solves it. A little zones read is overdue.. But in my implementation i want to abstract away the size stream in a different file (no component, no ngOnInit) and expose mapped height$ and width$ streams for all components to attach on as needed. How do i let the zones know about those stream-updates?Toadstool
@Birowsky, one way would be to manually run change detection after you make a view model change: https://mcmap.net/q/67333/-how-to-force-a-component-39-s-re-rendering-in-angular-2, Here's a plunker showing that: plnkr.co/edit/eiua3K9aRRFEyxV3vbtP?p=previewTheurer
T
14

Since my goal was to be able to abstract the window size streams in a different module, apparently just wrapping the streams in a class sealed the deal:

"This is the future" version:

import {Observable, BehaviorSubject} from 'rxjs';  

export class WindowSize {
  width$: Observable<number>;
  height$: Observable<number>;

  constructor() {
    let windowSize$ = createWindowSize$();
    this.width$ = (windowSize$.pluck('width') as Observable<number>).distinctUntilChanged();
    this.height$ = (windowSize$.pluck('height') as Observable<number>).distinctUntilChanged();
  }
}

const createWindowSize$ = () =>
  Observable.fromEvent(window, 'resize')
    .map(getWindowSize)
    .startWith(getWindowSize())
    .publishReplay(1)
    .refCount();

const getWindowSize = () => {
  return {
    height: window.innerHeight,
    width: window.innerWidth
  }
};

"Granny" version:

import {Observable, BehaviorSubject} from 'rxjs';

export class WindowSize {
    width$: Observable<number>;
    height$: Observable<number>;

    constructor() {
        let windowSize$ = new BehaviorSubject(getWindowSize());
        this.width$ = (windowSize$.pluck('width') as Observable<number>).distinctUntilChanged();
        this.height$ = (windowSize$.pluck('height') as Observable<number>).distinctUntilChanged();

        Observable.fromEvent(window, 'resize')
            .map(getWindowSize)
            .subscribe(windowSize$);
    }
}

function getWindowSize() {
    return {
        height: window.innerHeight,
        width: window.innerWidth
    };
}

Although I didn't want class/service in this module, just clear/platform-independent constructs, this was the only clean way that worked for angular without needing to care about triggering zone updates.

Toadstool answered 20/2, 2016 at 13:50 Comment(15)
Nice solution. I'm curious, is @Injectable() required to get this to work?Theurer
@MarkRajcok its required, because he has added it inside bootstrap array as an service..Vaporization
@PankajParkar, if a service does not depend on other services, the @Injectable() decorator is not required (even if you put it inside the bootstrap() array). So my question still stands. I am still wondering/asking if this use case is any different. My hunch is that @Injectable is not required here. (However, I am aware that it is a best practice to always include the @Injectable decorator.)Theurer
@MarkRajcok sorry, i thought it was inferred: now that i have this as a service, the rest of the components DO depend on this, and I DO pull it through dependency injection which is what makes these streams run in the angular execution context (the zone). So now it is in da zone!Toadstool
@PankajParkar and Birowsky, I should have clarified: @Injectable() isn't required if you want to inject a service into a component or another service. It is only required if the service itself has a dependency -- i.e., if its constructor specifies dependencies. Since Device doesn't depend on anything, I assume @Injectable() isn't required, but I wanted to know if you removed it, would the service still be created in the Angular zone? (I think so, but I don't know for sure.)Theurer
@MarkRajcok you should've put money on it. It works without @Injectable()Toadstool
@MarkRajcok thanks man for letting us know.. you know, you are awesome !!Vaporization
It wouldn't compile for me without the as Observable<number> and I removed the @Injectable as per the comments saying it worked without it.Bersagliere
Why the BehaviorSubject, width$ and height$? It can work just with the Observable.fromEventSympathizer
@AlexJ it allows for some subscriber to subscribe after the resize event has happened, and still get the latest emitted values.Toadstool
@Birowsky Could you explain why use "publishReplay" and "refCount"? It works fine without em tooMerrifield
@Merrifield without them, Observable.fromEvent(..) would register an event handler for every subscriber separately. With them, there is only one event handler behind the scenes, the latest value is being kept and just passed to every new subscriber to that stream. Let me know if this explanation is not enough.Toadstool
@Birowsky if you could explain a little more, I think I understand but all this observable stuff is kind of new to me and I'm still getting my head on it...Merrifield
can someone show how to subscribe to this stream so that I get the latest value? Pretty new to RxjsSalty
@GiridharKarnik WindowSize.width$.subscribe(value => console.log(value));Tipcat
G
0

@Marks solution didnt work for me, what did work is something like following hack :

size$ = windowSize$.do(o => {
     console.log('size:', o);
     // since the resize event was not registered while inside the Angular zone,
     // we need to manually run change detection so that the view will update
     setTimeout(()=>this._cdr.detectChanges())
});  

But then i really thought about Mark's note "The event handler is running outside the Angular zone" and realized that in my use case i was using javascript's firestore instead of angular's firestore which made snapshot changes be observed on a diffrenet event handler. after switching back to angular's firestore - it worked!

Genetics answered 13/9, 2021 at 15:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.