Skip to content

Commit 16e4a87

Browse files
committed
fix(material/button): resolve memory leaks in ripples (#28254)
The `MatRippleLoader` that is used in the button doesn't track any ripples, but instead patches the ripples onto the DOM nodes which in theory should avoid leaks since the ripple will be collected together with the node. The problem is that each ripple registers itself with the `RippleEventManager` which needs to be notified on destroy so that it can dereference the DOM nodes and remove the event listeners. These changes avoid the leaks by: 1. Destroying the ripple when the trigger is destroyed. 2. Cleaning up all the ripples when the ripple loader is destroyed. 3. No longer patching directives onto the DOM nodes. Fixes #28240. (cherry picked from commit a962bb7)
1 parent 214306f commit 16e4a87

File tree

4 files changed

+38
-12
lines changed

4 files changed

+38
-12
lines changed

src/material/button/button-base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export class MatButtonBase
171171

172172
ngOnDestroy() {
173173
this._focusMonitor.stopMonitoring(this._elementRef);
174+
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
174175
}
175176

176177
/** Focuses the button. */

src/material/chips/chip.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ export class MatChip
328328

329329
ngOnDestroy() {
330330
this._focusMonitor.stopMonitoring(this._elementRef);
331+
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
331332
this._actionChanges?.unsubscribe();
332333
this.destroyed.emit({chip: this});
333334
this.destroyed.complete();

src/material/core/private/ripple-loader.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const matRippleDisabled = 'mat-ripple-loader-disabled';
4141
*
4242
* This service allows us to avoid eagerly creating & attaching MatRipples.
4343
* It works by creating & attaching a ripple only when a component is first interacted with.
44+
*
45+
* @docs-private
4446
*/
4547
@Injectable({providedIn: 'root'})
4648
export class MatRippleLoader implements OnDestroy {
@@ -49,6 +51,7 @@ export class MatRippleLoader implements OnDestroy {
4951
private _globalRippleOptions = inject(MAT_RIPPLE_GLOBAL_OPTIONS, {optional: true});
5052
private _platform = inject(Platform);
5153
private _ngZone = inject(NgZone);
54+
private _hosts = new Map<HTMLElement, MatRipple>();
5255

5356
constructor() {
5457
this._ngZone.runOutsideAngular(() => {
@@ -59,6 +62,12 @@ export class MatRippleLoader implements OnDestroy {
5962
}
6063

6164
ngOnDestroy() {
65+
const hosts = this._hosts.keys();
66+
67+
for (const host of hosts) {
68+
this.destroyRipple(host);
69+
}
70+
6271
for (const event of rippleInteractionEvents) {
6372
this._document?.removeEventListener(event, this._onInteraction, eventListenerOptions);
6473
}
@@ -98,15 +107,13 @@ export class MatRippleLoader implements OnDestroy {
98107

99108
/** Returns the ripple instance for the given host element. */
100109
getRipple(host: HTMLElement): MatRipple | undefined {
101-
if ((host as any).matRipple) {
102-
return (host as any).matRipple;
103-
}
104-
return this.createRipple(host);
110+
const ripple = this._hosts.get(host);
111+
return ripple || this._createRipple(host);
105112
}
106113

107114
/** Sets the disabled state on the ripple instance corresponding to the given host element. */
108115
setDisabled(host: HTMLElement, disabled: boolean): void {
109-
const ripple = (host as any).matRipple as MatRipple | undefined;
116+
const ripple = this._hosts.get(host);
110117

111118
// If the ripple has already been instantiated, just disable it.
112119
if (ripple) {
@@ -134,19 +141,24 @@ export class MatRippleLoader implements OnDestroy {
134141

135142
const element = eventTarget.closest(`[${matRippleUninitialized}]`);
136143
if (element) {
137-
this.createRipple(element as HTMLElement);
144+
this._createRipple(element as HTMLElement);
138145
}
139146
};
140147

141148
/** Creates a MatRipple and appends it to the given element. */
142-
createRipple(host: HTMLElement): MatRipple | undefined {
149+
private _createRipple(host: HTMLElement): MatRipple | undefined {
143150
if (!this._document) {
144151
return;
145152
}
146153

154+
const existingRipple = this._hosts.get(host);
155+
if (existingRipple) {
156+
return existingRipple;
157+
}
158+
147159
// Create the ripple element.
148160
host.querySelector('.mat-ripple')?.remove();
149-
const rippleEl = this._document!.createElement('span');
161+
const rippleEl = this._document.createElement('span');
150162
rippleEl.classList.add('mat-ripple', host.getAttribute(matRippleClassName)!);
151163
host.append(rippleEl);
152164

@@ -166,8 +178,19 @@ export class MatRippleLoader implements OnDestroy {
166178
return ripple;
167179
}
168180

169-
attachRipple(host: Element, ripple: MatRipple): void {
181+
attachRipple(host: HTMLElement, ripple: MatRipple): void {
170182
host.removeAttribute(matRippleUninitialized);
171-
(host as any).matRipple = ripple;
183+
this._hosts.set(host, ripple);
184+
}
185+
186+
destroyRipple(host: HTMLElement) {
187+
const ripple = this._hosts.get(host);
188+
189+
if (ripple) {
190+
// Since this directive is created manually, it needs to be destroyed manually too.
191+
// tslint:disable-next-line:no-lifecycle-invocation
192+
ripple.ngOnDestroy();
193+
this._hosts.delete(host);
194+
}
172195
}
173196
}

tools/public_api_guard/material/core.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,14 @@ export class MatRipple implements OnInit, OnDestroy, RippleTarget {
399399
export class MatRippleLoader implements OnDestroy {
400400
constructor();
401401
// (undocumented)
402-
attachRipple(host: Element, ripple: MatRipple): void;
402+
attachRipple(host: HTMLElement, ripple: MatRipple): void;
403403
configureRipple(host: HTMLElement, config: {
404404
className?: string;
405405
centered?: boolean;
406406
disabled?: boolean;
407407
}): void;
408-
createRipple(host: HTMLElement): MatRipple | undefined;
408+
// (undocumented)
409+
destroyRipple(host: HTMLElement): void;
409410
getRipple(host: HTMLElement): MatRipple | undefined;
410411
// (undocumented)
411412
ngOnDestroy(): void;

0 commit comments

Comments
 (0)