tl;dr
Sometimes we need to execute JavaScript when an element is resized.
Current solutions are based on the viewport dimension, not on element dimensions.
ResizeObserver is a new API which allows us to react to element resizing.
There are a few steps required to use it properly with Angular. You have to make sure:
- to unobserve on destroy
- that change detection is triggered
I found it to cumbersome to do it on every component. That’s why I’ve created a library to simplify the usage with Angular. 🚀
✨React to element dimension changes
Many changes in screen size or element size can be handled with pure CSS. But sometimes we need to know when an element is resized and execute some logic in JavaScript.
This is usually implemented with either window.onchange
or matchMedia
. Both solutions are based on the viewport dimension, not the element dimension.
ResizeObserver ResizeObserver - Web APIs | MDN is a new API to solve exactly this problem. In this article we will have a look at how it works and how we can use this new API together with Angular.
Let’s start with why we need a new API.
💣 What’s the problem with window.onchange?
We are only interested in events where our component changes its width. Unfortunately window.onchange sometimes fires too often or not at all.
onchange fires too often
This happens when the viewport changes but our component doesn’t. Do you see the first window.onresize (colored in red)? We are not interested in this event. Running to much code on every onresize could lead to performance problems.
onchange doesn’t fire (but should)
This happens when the viewport doesn’t change but the elements within change.
Examples
- New elements are added dynamically
- Elements are collapsed or expanded (e.g. Sidebar)
In the graphic below the viewport doesn’t change and the sidebar gets expanded. The ResizeObserver triggers but the window.onresize doesn’t.
Now that we know why we need the new ResizeObserver Api we will take a closer look at it.
🚀 ResizeObserver in a nutshell
Here is an example on how to use ResizeObserver to subscribe to a resize event of an element.
You can observe multiple elements with one ResizeObserver. That’s why we have an array of entries.
1const observer = new ResizeObserver(entries => {2 entries.forEach(entry => {3 console.log("width", entry.contentRect.width);4 console.log("height", entry.contentRect.height);5 });6});78observer.observe(document.querySelector(".my-element"));
This is how an entry looks like:
1{2 "target": _div_,3 "contentRect": {4 "x": 0,5 "y": 0,6 "width": 200,7 "height": 100,8 "top": 0,9 "right": 200,10 "bottom": 100,11 "left": 012 }13}
Since we subscribed to an observer, we need to unsubscribe as well:
1const myEl = document.querySelector(".my-element");23// Create observer4const observer = new ResizeObserver(() => {});56// Add element (observe)7observer.observe(myEl);89// Remove element (unobserve)10observer.unobserve(myEl);
That’s ResizeObserver in a nutshell. For a full overview of what you can do with ResizeObserver, check out ResizeObserver - Web APIs | MDN
🏁 Status ResizeObserver
At the time of writing (Feb 2020), ResizeObserver is a EditorsDraft Resize Observer. This means it is still in a very early phase World Wide Web Consortium Process Document
Chrome and Firefox support ResizeObserver, Edge and Safari don’t. A ponyfill is available.
🛠 How to use it with Angular
Let’s create a component which displays its width.
1: Create the component
1@Component({2 selector: "my-component",3 template: "{{ width }}px"4})5export class MyComponent {6 width = 500;7}
2: Add Observer
Now let’s observe the nativeElement of our component and log the current width. Works like a charm (in Chrome and Firefox 😉)
1export class MyComponent implements OnInit {2 width = 500;34 constructor(private host: ElementRef) {}56 ngOnInit() {7 const observer = new ResizeObserver(entries => {8 const width = entries[0].contentRect.width;9 console.log(width);10 });1112 observer.observe(this.host.nativeElement);13 }14}
3: Trigger change detection
If you are following this example you may have tried to bind the width directly to the class property. Unfortunately the template is not rerendered and keeps the initial value.
The reason is that Angular has monkey-patched most of the events but not (yet) ResizeObserver. This means that this callback runs outside of the zone.
We can easily fix that by manually running it in the zone.
1export class MyComponent implements OnInit {2 width = 500;34 constructor(5 private host: ElementRef,6 private zone: NgZone7 ) {}89 ngOnInit() {10 const observer = new ResizeObserver(entries => {11 this.zone.run(() => {12 this.width = entries[0].contentRect.width;13 });14 });1516 observer.observe(this.host.nativeElement);17 }18}
4: Unobserve on destroy
To prevent memory leaks and to avoid unexpected behaviour we should unobserve on destroy:
1export class MyComponent implements OnInit, OnDestroy {2 width = 500;3 observer;45 constructor(6 private host: ElementRef,7 private zone: NgZone8 ) {}910 ngOnInit() {11 this.observer = new ResizeObserver(entries => {12 this.zone.run(() => {13 this.width = entries[0].contentRect.width;14 });15 });1617 this.observer.observe(this.host.nativeElement);18 }1920 ngOnDestroy() {21 this.observer.unobserve(this.host.nativeElement);22 }23}
Want to try it out? Here is a live example.
5: Protip: Create a stream with RxJS
1export class MyComponent implements OnInit, OnDestroy {2 width$ = new BehaviorSubject<number>(0);3 observer;45 constructor(6 private host: ElementRef,7 private zone: NgZone8 ) {}910 ngOnInit() {11 this.observer = new ResizeObserver(entries => {12 this.zone.run(() => {13 this.width$.next(entries[0].contentRect.width);14 });15 });1617 this.observer.observe(this.host.nativeElement);18 }1920 ngOnDestroy() {21 this.observer.unobserve(this.host.nativeElement);22 }23}
Follow me on 🐦 twitter for more blogposts about Angular and web technologies
☀️ Use ng-resize-observer to simplify the usage of ResizeObserver
💻 https://github.com/ChristianKohler/ng-resize-observer
📦 https://www.npmjs.com/package/ng-resize-observer
- Install
ng-resize-observer
- Import and use the providers
- Inject the NgResizeObserver stream
1import { NgModule, Component } from "@angular/core";2import {3 ngResizeObserverProviders,4 NgResizeObserver5} from "ng-resize-observer";67@Component({8 selector: "my-component",9 template: "{{ width$ | async }} px",10 providers: [...ngResizeObserverProviders]11})12export class MyComponent {13 width$ = this.resize$.pipe(14 map(entry => entry.contentRect.width)15 );1617 constructor(private resize$: NgResizeObserver) {}18}
NgResizeObserver is created per component and will automatically unsubscribe when the component is destroyed. It’s a RxJS observable and you can use all operators with it.
Want to try it out? Here is a live example on Stackblitz
Make the web resizable 🙌
ResizeObservers allow us to run code exactly when we need it. I hope I could give you an overview over this new API.
If you want to use it in your Angular application, give ng-resize-observer a try and let me know what you think.
If you liked the article 🙌, spread the word and follow me on twitter for more posts on Angular and web technologies.