Reputation: 66
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?
Any suggestions would be appreciated.
Upvotes: 1
Views: 879
Reputation: 58039
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...
Upvotes: 0
Reputation: 58244
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.
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;
};
}
Just use a getter method to fetch the latest value of window.innerWidth
whenever necessary
get windowInnerWidth() {
return window?.innerWidth || 0;
}
Upvotes: 0
Reputation: 5558
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();
}
}
}
Upvotes: 0