Skip to content

Commit a962bb7

Browse files
authored
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.
1 parent 7ee07db commit a962bb7

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
@@ -146,6 +146,7 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
146146

147147
ngOnDestroy() {
148148
this._focusMonitor.stopMonitoring(this._elementRef);
149+
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
149150
}
150151

151152
/** Focuses the button. */

src/material/chips/chip.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
298298

299299
ngOnDestroy() {
300300
this._focusMonitor.stopMonitoring(this._elementRef);
301+
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
301302
this._actionChanges?.unsubscribe();
302303
this.destroyed.emit({chip: this});
303304
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
@@ -402,13 +402,14 @@ export class MatRipple implements OnInit, OnDestroy, RippleTarget {
402402
export class MatRippleLoader implements OnDestroy {
403403
constructor();
404404
// (undocumented)
405-
attachRipple(host: Element, ripple: MatRipple): void;
405+
attachRipple(host: HTMLElement, ripple: MatRipple): void;
406406
configureRipple(host: HTMLElement, config: {
407407
className?: string;
408408
centered?: boolean;
409409
disabled?: boolean;
410410
}): void;
411-
createRipple(host: HTMLElement): MatRipple | undefined;
411+
// (undocumented)
412+
destroyRipple(host: HTMLElement): void;
412413
getRipple(host: HTMLElement): MatRipple | undefined;
413414
// (undocumented)
414415
ngOnDestroy(): void;

0 commit comments

Comments
 (0)