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
Because of the way Promise s work, Angular's async pipe has to be impure (meaning that it can return different outputs without any change in input). The transform method on Pipe s is synchronous, so when an async pipe gets a Promise , the pipe adds a callback to the Promise and returns null.
Every time a new value is emitted the async pipe will automatically mark your component to be checked for changes. And best of all, when the component is destroyed the async pipe will automatically unsubscribe for you. No need to do this manually.
The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.
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.)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With