1

I'm creating an Angular responsive app where I have more than 10 breakpoints in which I have to group the elements in different containers. Having this in mind I believe that I can't get advantage of the css mediaQueris and I want to have the innerWidth property of the window within the components.

In order to get it I have created the following directive and I extend the components with it:

import { Directive, NgZone, OnDestroy } from "@angular/core";
import { Subject, fromEvent } from "rxjs";
import { debounceTime, distinctUntilChanged, map, takeUntil } from "rxjs/operators";

@Directive()
export abstract class WindowResizeDirective implements OnDestroy {
    winWidth: number = window.innerWidth;

    protected destroy$: Subject<boolean> = new Subject();

    constructor(private _zone: NgZone) {
        this._zone.runOutsideAngular(() => {
            fromEvent(window, 'resize').pipe(
                debounceTime(300),
                map((ev: any) => ev.target.innerWidth),
                distinctUntilChanged(),
                takeUntil(this.destroy$)
            ).subscribe(width => {
                this._zone.run(() => {
                    this.winWidth = width;
                })
            });
        });
    }

    ngOnDestroy(): void {
        this.destroy$.next(true);
        this.destroy$.complete();
      }
}

However many times I need to use this directive of the page component once and than on many components which are children of the page component, thus a lot of change detection cycles are triggered for one and the same reason -> resize of the window. Can you suggest a way of improving the performance?

  1. I've been thinking of using a service instead of directive which is provided on parent level and than each child can get the same instance of the service.
  2. I'm not sure if the code within the directive is optimal at all.

Any suggestions would be appreciated.

3
  • Any timer-based solution is worse than using native ResizeObserver Commented Mar 27, 2024 at 21:40
  • Isn't this observer intended to observe changes in elements dimensions, not the window innerWidth? Commented Mar 27, 2024 at 22:22
  • Yes, and you can observe body resize. Check this question Commented Mar 27, 2024 at 23:19

3 Answers 3

0

I guess a service will be better here, create a Resize Service to handle the window resize event across multiple components.

Use BehaviorSubject to hold the current window width

something like this :

import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { BehaviorSubject, fromEvent } from 'rxjs';
import { debounceTime, map, takeUntil } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class ResizeService implements OnDestroy {
  private windowSize = new BehaviorSubject<number>(window.innerWidth);
  private destroy$ = new Subject<void>();

  constructor(private zone: NgZone) {
    this.zone.runOutsideAngular(() => {
      fromEvent(window, 'resize').pipe(
        debounceTime(300),
        map((event: any) => event.target.innerWidth),
        takeUntil(this.destroy$),
      ).subscribe(width => {
        this.zone.run(() => {
          this.windowSize.next(width);
        });
      });
    });
  }

  getWindowSize(): BehaviorSubject<number> {
    return this.windowSize;
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

inject it to your component

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { ResizeService } from './resize.service';

@Component({
  selector: 'app-some-component',
  template: `<!-- Your template here -->`,
})
export class SomeComponent implements OnInit, OnDestroy {
  private resizeSubscription: Subscription;
  winWidth: number;

  constructor(private resizeService: ResizeService) {}

  ngOnInit() {
    this.resizeSubscription = this.resizeService.getWindowSize().subscribe(width => {
      this.winWidth = width;
    });
  }

  ngOnDestroy() {
    if (this.resizeSubscription) {
      this.resizeSubscription.unsubscribe();
    }
  }
}
Sign up to request clarification or add additional context in comments.

1 Comment

If I provide the service in root and I navigate from a page which uses the service to another page, which doesn't, the change detection cycle still gets triggered on windows resize, even though not a single component is interested in the change.
0

Since below code does not trigger change detection, we can just add an hostlistener to listen for resize event. I am also adding an additional decorator ngDebounce, which is a cool decorator to debounce methods directly. Please find below the working example.

ngDebounce article

ts

import { Component, HostListener } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { ngDebounce } from './ngDebounce';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Hello from {{ name }}!</h1>
    <a target="_blank" href="https://angular.dev/overview">
      Learn more about Angular
    </a><br/><br/><br/><br/>
    winWidth: {{winWidth}}
  `,
})
export class App {
  winWidth: number = window.innerWidth;
  name = 'Angular';

  @HostListener('window:resize', ['$event'])
  @ngDebounce(500)
  onResize(event: any) {
    console.log('resize');
    this.winWidth = event.target.innerWidth;
  }
}

bootstrapApplication(App);

ngdebounce.ts

export function ngDebounce(timeout: number) {
  // store timeout value for cancel the timeout
  let timeoutRef: any = null;

  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // store original function for future use
    const original = descriptor.value;

    // override original function body
    descriptor.value = function debounce(...args: any[]) {
      // clear previous timeout
      clearTimeout(timeoutRef);

      // sechudle timer
      timeoutRef = setTimeout(() => {
        // call original function
        original.apply(this, args);
      }, timeout);
    };

    // return descriptor with new value
    return descriptor;
  };
}

Stackblitz Demo


Just use a getter method to fetch the latest value of window.innerWidth whenever necessary

get windowInnerWidth() {
    return window?.innerWidth || 0;
}

1 Comment

This doesn't triggers change detection on resize, thus my logic in the template would be never executed.
0

I feel to improve the performance it's better use an unique "fromEvent.Resize" in the main.app that take account all the elements with a directive

You have a simple directive like

@Directive({ selector: '[checkWidth]' })
export abstract class CheckWidthDirective{
    winWidth: number = window.innerWidth;
}

If in your main.app you have a

@ContentChildren(CheckWidthDirective,{descendant:true}) 
                                    resizes!:QueryList<CheckWidthDirective>

ngAfterViewInit(){
   fromEvent('window.resize').pipe(
        debounceTime(300),
        map((event: any) => event.target.innerWidth),
        takeUntil(this.destroy$),
      ).subscribe(width => {
          this.resizes.winWidth=width
      });
}

Well, your directive can use a setter if you want, or signals or...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.