Skip to content

Commit 08afaa3

Browse files
committed
refactor: use observables for SharedResizeObserver
1 parent d799c43 commit 08afaa3

File tree

3 files changed

+145
-110
lines changed

3 files changed

+145
-110
lines changed

src/material/form-field/directives/floating-label.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {
1616
OnDestroy,
1717
Output,
1818
} from '@angular/core';
19-
import {SharedResizeObserver} from '../resize-observer';
19+
import {SharedResizeObserver} from '../shared-resize-observer';
2020
import {Platform} from '@angular/cdk/platform';
21+
import {Subscription} from 'rxjs';
2122

2223
/**
2324
* Internal directive that maintains a MDC floating label. This directive does not
@@ -62,7 +63,7 @@ export class MatFormFieldFloatingLabel implements OnDestroy {
6263
if (this._monitorResize) {
6364
this._startResizeObserver();
6465
} else {
65-
this._stopResizeObserver();
66+
this._resizeSubscription.unsubscribe();
6667
}
6768
}
6869
private _monitorResize = false;
@@ -75,12 +76,12 @@ export class MatFormFieldFloatingLabel implements OnDestroy {
7576

7677
private _resizeObserver = inject(SharedResizeObserver);
7778

78-
private _stopResizeObserver = () => {};
79+
private _resizeSubscription = new Subscription();
7980

8081
constructor(private _elementRef: ElementRef<HTMLElement>) {}
8182

8283
ngOnDestroy() {
83-
this._stopResizeObserver();
84+
this._resizeSubscription.unsubscribe();
8485
}
8586

8687
/** Gets the width of the label. Used for the outline notch. */
@@ -107,12 +108,10 @@ export class MatFormFieldFloatingLabel implements OnDestroy {
107108

108109
private _startResizeObserver() {
109110
if (this._platform.isBrowser) {
110-
this._stopResizeObserver();
111-
this._stopResizeObserver = this._ngZone.runOutsideAngular(() =>
112-
this._resizeObserver.observe(this._elementRef.nativeElement, () => this._handleResize(), {
113-
box: 'border-box',
114-
}),
115-
);
111+
this._resizeSubscription.unsubscribe();
112+
this._resizeSubscription = this._resizeObserver
113+
.observe(this._elementRef.nativeElement, {box: 'border-box'})
114+
.subscribe(() => this._handleResize());
116115
}
117116
}
118117
}

src/material/form-field/resize-observer.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {inject, Injectable, NgZone, OnDestroy} from '@angular/core';
9+
import {Observable, Subject} from 'rxjs';
10+
import {filter, shareReplay, takeUntil} from 'rxjs/operators';
11+
12+
/**
13+
* Handler that logs "ResizeObserver loop limit exceeded" errors.
14+
* These errors are not shown in the Chrome console, so we log them to ensure developers are aware.
15+
* @param e The error
16+
*/
17+
const loopLimitExceededErrorHandler = (e: unknown) => {
18+
if (e instanceof Error && e.message === 'ResizeObserver loop limit exceeded') {
19+
console.error(
20+
`${e.message}. This could indicate a performance issue with your app. See https://github.com/WICG/resize-observer/blob/master/explainer.md#error-handling`,
21+
);
22+
}
23+
};
24+
25+
/**
26+
* A shared ResizeObserver to be used for a particular box type (content-box, border-box, or
27+
* device-pixel-content-box)
28+
*/
29+
class SingleBoxSharedResizeObserver {
30+
/** Stream that emits when the shared observer is destroyed. */
31+
private _destroyed = new Subject<void>();
32+
/** Stream of all events from the ResizeObserver. */
33+
private _resizeSubject = new Subject<ResizeObserverEntry[]>();
34+
/** ResizeObserver used to observe element resize events. */
35+
private _resizeObserver = new ResizeObserver(entries => this._resizeSubject.next(entries));
36+
/** A map of elements to streams of their resize events. */
37+
private _elementObservables = new Map<Element, Observable<ResizeObserverEntry[]>>();
38+
39+
constructor(
40+
/** The box type to observe for resizes. */
41+
private _box: ResizeObserverBoxOptions,
42+
) {}
43+
44+
/**
45+
* Gets a stream of resize events for the given element.
46+
* @param target The element to observe.
47+
* @return The stream of resize events for the element.
48+
*/
49+
observe(target: Element): Observable<ResizeObserverEntry[]> {
50+
if (!this._elementObservables.has(target)) {
51+
this._elementObservables.set(
52+
target,
53+
new Observable<ResizeObserverEntry[]>(observer => {
54+
const subscription = this._resizeSubject
55+
.pipe(filter(entries => entries.some(entry => entry.target === target)))
56+
.subscribe(observer);
57+
this._resizeObserver.observe(target, {box: this._box});
58+
return () => {
59+
this._resizeObserver.unobserve(target);
60+
subscription.unsubscribe();
61+
this._elementObservables.delete(target);
62+
};
63+
}).pipe(
64+
// Share a replay of the last event so that subsequent calls to observe the same element
65+
// receive initial sizing info like the first one. Also enable ref counting so the
66+
// element will be automatically unobserved when there are no more subscriptions.
67+
shareReplay({bufferSize: 1, refCount: true}),
68+
takeUntil(this._destroyed),
69+
),
70+
);
71+
}
72+
return this._elementObservables.get(target)!;
73+
}
74+
75+
/** Destroys this instance. */
76+
destroy() {
77+
this._destroyed.next();
78+
this._destroyed.complete();
79+
this._resizeSubject.complete();
80+
this._elementObservables.clear();
81+
this._resizeObserver.disconnect();
82+
}
83+
}
84+
85+
/**
86+
* Allows observing resize events on multiple elements using a shared set of ResizeObserver.
87+
* Sharing a ResizeObserver instance is recommended for better performance (see
88+
* https://github.com/WICG/resize-observer/issues/59).
89+
*
90+
* Rather than share a single `ResizeObserver`, this class creates one `ResizeObserver` per type
91+
* of observed box ('content-box', 'border-box', and 'device-pixel-content-box'). This avoids
92+
* later calls to `observe` with a different box type from influencing the events dispatched to
93+
* earlier calls.
94+
*/
95+
@Injectable({
96+
providedIn: 'root',
97+
})
98+
export class SharedResizeObserver implements OnDestroy {
99+
/** Map of box type to shared resize observer. */
100+
private _observers = new Map<ResizeObserverBoxOptions, SingleBoxSharedResizeObserver>();
101+
102+
/** The Angular zone. */
103+
private _ngZone = inject(NgZone);
104+
105+
constructor() {
106+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
107+
this._ngZone.runOutsideAngular(() => {
108+
window.addEventListener('error', loopLimitExceededErrorHandler);
109+
});
110+
}
111+
}
112+
113+
ngOnDestroy() {
114+
for (const [, observer] of this._observers) {
115+
observer.destroy();
116+
}
117+
this._observers.clear();
118+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
119+
window.removeEventListener('error', loopLimitExceededErrorHandler);
120+
}
121+
}
122+
123+
/**
124+
* Gets a stream of resize events for the given target element and box type.
125+
* @param target The element to observe for resizes.
126+
* @param options Options to pass to the `ResizeObserver`
127+
* @return The stream of resize events for the element.
128+
*/
129+
observe(target: Element, options?: ResizeObserverOptions): Observable<ResizeObserverEntry[]> {
130+
const box = options?.box || 'content-box';
131+
if (!this._observers.has(box)) {
132+
this._observers.set(box, new SingleBoxSharedResizeObserver(box));
133+
}
134+
return this._observers.get(box)!.observe(target);
135+
}
136+
}

0 commit comments

Comments
 (0)