Skip to content

Commit acaed95

Browse files
crisbetojelbourn
authored andcommitted
fix(tabs): avoid hitting change detection if text content hasn't changed (#14251)
Currently we trigger a change detection run when the content of the tab header changes. Since the `MutationObserver` callback can fire if the text node got swapped out, but the actual text didn't change, we could get into an infinite change detection loop if a poorly constructed data binding is used. These changes add a check that will only do extra work if the content has changed. Fixes #14249.
1 parent 50b9da6 commit acaed95

File tree

1 file changed

+22
-11
lines changed

1 file changed

+22
-11
lines changed

src/lib/tabs/tab-header.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
114114
/** Used to manage focus between the tabs. */
115115
private _keyManager: FocusKeyManager<MatTabLabelWrapper>;
116116

117-
private _selectedIndex: number = 0;
117+
/** Cached text content of the header. */
118+
private _currentTextContent: string;
118119

119120
/** The index of the active tab. */
120121
@Input()
@@ -128,6 +129,7 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
128129
this._keyManager.updateActiveItemIndex(value);
129130
}
130131
}
132+
private _selectedIndex: number = 0;
131133

132134
/** Event emitted when the option is selected. */
133135
@Output() readonly selectFocusedIndex = new EventEmitter();
@@ -237,16 +239,25 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
237239
* Callback for when the MutationObserver detects that the content has changed.
238240
*/
239241
_onContentChanges() {
240-
const zoneCallback = () => {
241-
this.updatePagination();
242-
this._alignInkBarToSelectedTab();
243-
this._changeDetectorRef.markForCheck();
244-
};
245-
246-
// The content observer runs outside the `NgZone` by default, which
247-
// means that we need to bring the callback back in ourselves.
248-
// @breaking-change 8.0.0 Remove null check for `_ngZone` once it's a required parameter.
249-
this._ngZone ? this._ngZone.run(zoneCallback) : zoneCallback();
242+
const textContent = this._elementRef.nativeElement.textContent;
243+
244+
// We need to diff the text content of the header, because the MutationObserver callback
245+
// will fire even if the text content didn't change which is inefficient and is prone
246+
// to infinite loops if a poorly constructed expression is passed in (see #14249).
247+
if (textContent !== this._currentTextContent) {
248+
this._currentTextContent = textContent;
249+
250+
const zoneCallback = () => {
251+
this.updatePagination();
252+
this._alignInkBarToSelectedTab();
253+
this._changeDetectorRef.markForCheck();
254+
};
255+
256+
// The content observer runs outside the `NgZone` by default, which
257+
// means that we need to bring the callback back in ourselves.
258+
// @breaking-change 8.0.0 Remove null check for `_ngZone` once it's a required parameter.
259+
this._ngZone ? this._ngZone.run(zoneCallback) : zoneCallback();
260+
}
250261
}
251262

252263
/**

0 commit comments

Comments
 (0)