Skip to content

Commit 03b2396

Browse files
authored
refactor: Mutation observer controller (#1756)
* Added the feature to get the changed attribute name inside the observer callback. * Restructured some code in the controller implementation. * Improved types and documentation. * Fixed call-site usage with the new API.
1 parent ee24523 commit 03b2396

File tree

5 files changed

+72
-42
lines changed

5 files changed

+72
-42
lines changed

src/components/button-group/button-group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default class IgcButtonGroupComponent extends EventEmitterMixin<
6363

6464
const buttons = this.toggleButtons;
6565
const idx = buttons.indexOf(
66-
added.length ? last(added).node : last(attributes)
66+
added.length ? last(added).node : last(attributes).node
6767
);
6868

6969
for (const [i, button] of buttons.entries()) {

src/components/carousel/carousel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
164164
return;
165165
}
166166
const idx = this.slides.indexOf(
167-
added.length ? last(added).node : last(attributes)
167+
added.length ? last(added).node : last(attributes).node
168168
);
169169

170170
for (const [i, slide] of this.slides.entries()) {
Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
2-
32
import { isElement } from '../util.js';
43

54
/** @ignore */
6-
export interface MutationControllerConfig<T> {
5+
export interface MutationControllerConfig<T extends Node = Node> {
76
/** The callback function to run when a mutation occurs. */
87
callback: MutationControllerCallback<T>;
98
/** The underlying mutation observer configuration parameters. */
@@ -20,27 +19,42 @@ export interface MutationControllerConfig<T> {
2019
filter?: MutationControllerFilter<T>;
2120
}
2221

23-
type MutationControllerCallback<T> = (
22+
type MutationControllerCallback<T extends Node = Node> = (
2423
params: MutationControllerParams<T>
2524
) => unknown;
2625

2726
/**
2827
* Filter configuration to return elements that either match
2928
* an array of selector strings or a predicate function.
3029
*/
31-
type MutationControllerFilter<T> = string[] | ((node: T) => boolean);
32-
type MutationDOMChange<T> = { target: Element; node: T };
30+
type MutationControllerFilter<T extends Node = Node> =
31+
| string[]
32+
| ((node: T) => boolean);
33+
34+
type MutationDOMChange<T extends Node = Node> = {
35+
/** The parent of the added/removed element. */
36+
target: Element;
37+
/** The added/removed element. */
38+
node: T;
39+
};
40+
41+
type MutationAttributeChange<T extends Node = Node> = {
42+
/** The host element of the changed attribute. */
43+
node: T;
44+
/** The changed attribute name. */
45+
attributeName: string | null;
46+
};
3347

34-
type MutationChange<T> = {
48+
type MutationChange<T extends Node = Node> = {
3549
/** Elements that have attribute(s) changes. */
36-
attributes: T[];
50+
attributes: MutationAttributeChange<T>[];
3751
/** Elements that have been added. */
3852
added: MutationDOMChange<T>[];
3953
/** Elements that have been removed. */
4054
removed: MutationDOMChange<T>[];
4155
};
4256

43-
export type MutationControllerParams<T> = {
57+
export type MutationControllerParams<T extends Node = Node> = {
4458
/** The original mutation records from the underlying observer. */
4559
records: MutationRecord[];
4660
/** The aggregated changes. */
@@ -49,25 +63,30 @@ export type MutationControllerParams<T> = {
4963
observer: MutationController<T>;
5064
};
5165

52-
function mutationFilter<T>(nodes: T[], filter?: MutationControllerFilter<T>) {
53-
if (!filter) {
66+
function applyNodeFilter<T extends Node = Node>(
67+
nodes: T[],
68+
predicate?: MutationControllerFilter<T>
69+
): T[] {
70+
if (!predicate) {
5471
return nodes;
5572
}
5673

57-
return Array.isArray(filter)
58-
? nodes.filter((node) =>
59-
filter.some((selector) => isElement(node) && node.matches(selector))
74+
return Array.isArray(predicate)
75+
? nodes.filter(
76+
(node) =>
77+
isElement(node) &&
78+
predicate.some((selector) => node.matches(selector))
6079
)
61-
: nodes.filter((node) => filter(node));
80+
: nodes.filter(predicate);
6281
}
6382

64-
class MutationController<T> implements ReactiveController {
65-
private _host: ReactiveControllerHost & Element;
66-
private _observer: MutationObserver;
67-
private _target: Element;
68-
private _config: MutationObserverInit;
69-
private _callback: MutationControllerCallback<T>;
70-
private _filter?: MutationControllerFilter<T>;
83+
class MutationController<T extends Node = Node> implements ReactiveController {
84+
private readonly _host: ReactiveControllerHost & Element;
85+
private readonly _observer: MutationObserver;
86+
private readonly _target: Element;
87+
private readonly _config: MutationObserverInit;
88+
private readonly _callback: MutationControllerCallback<T>;
89+
private readonly _filter?: MutationControllerFilter<T>;
7190

7291
constructor(
7392
host: ReactiveControllerHost & Element,
@@ -77,7 +96,7 @@ class MutationController<T> implements ReactiveController {
7796
this._callback = options.callback;
7897
this._config = options.config;
7998
this._target = options.target ?? this._host;
80-
this._filter = options.filter ?? [];
99+
this._filter = options.filter;
81100

82101
this._observer = new MutationObserver((records) => {
83102
this.disconnect();
@@ -88,36 +107,47 @@ class MutationController<T> implements ReactiveController {
88107
host.addController(this);
89108
}
90109

91-
public hostConnected() {
110+
/** @internal */
111+
public hostConnected(): void {
92112
this.observe();
93113
}
94114

95-
public hostDisconnected() {
115+
/** @internal */
116+
public hostDisconnected(): void {
96117
this.disconnect();
97118
}
98119

99120
private _process(records: MutationRecord[]): MutationControllerParams<T> {
121+
const predicate = this._filter;
100122
const changes: MutationChange<T> = {
101123
attributes: [],
102124
added: [],
103125
removed: [],
104126
};
105-
const filter = this._filter;
106127

107128
for (const record of records) {
108-
if (record.type === 'attributes') {
129+
const { type, target, attributeName, addedNodes, removedNodes } = record;
130+
131+
if (type === 'attributes') {
109132
changes.attributes.push(
110-
...mutationFilter([record.target as T], filter)
133+
...applyNodeFilter([target as T], predicate).map((node) => ({
134+
node,
135+
attributeName,
136+
}))
111137
);
112-
} else if (record.type === 'childList') {
138+
} else if (type === 'childList') {
113139
changes.added.push(
114-
...mutationFilter(Array.from(record.addedNodes) as T[], filter).map(
115-
(node) => ({ target: record.target as Element, node })
116-
)
140+
...applyNodeFilter([...addedNodes] as T[], predicate).map((node) => ({
141+
target: target as Element,
142+
node,
143+
}))
117144
);
118145
changes.removed.push(
119-
...mutationFilter(Array.from(record.removedNodes) as T[], filter).map(
120-
(node) => ({ target: record.target as Element, node })
146+
...applyNodeFilter([...removedNodes] as T[], predicate).map(
147+
(node) => ({
148+
target: target as Element,
149+
node,
150+
})
121151
)
122152
);
123153
}
@@ -130,12 +160,12 @@ class MutationController<T> implements ReactiveController {
130160
* Begin receiving notifications of changes to the DOM based
131161
* on the configured {@link MutationControllerConfig.target|target} and observer {@link MutationControllerConfig.config|options}.
132162
*/
133-
public observe() {
163+
public observe(): void {
134164
this._observer.observe(this._target, this._config);
135165
}
136166

137167
/** Stop watching for mutations. */
138-
public disconnect() {
168+
public disconnect(): void {
139169
this._observer.disconnect();
140170
}
141171
}
@@ -149,9 +179,9 @@ class MutationController<T> implements ReactiveController {
149179
* The mutation observer is disconnected before invoking the passed in callback and re-attached
150180
* after that in order to not loop itself in endless stream of changes.
151181
*/
152-
export function createMutationController<T>(
182+
export function createMutationController<T extends Node = Node>(
153183
host: ReactiveControllerHost & Element,
154184
config: MutationControllerConfig<T>
155-
) {
185+
): MutationController<T> {
156186
return new MutationController(host, config);
157187
}

src/components/select/select-group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default class IgcSelectGroupComponent extends LitElement {
5656
private _observerCallback({
5757
changes: { attributes },
5858
}: MutationControllerParams<IgcSelectItemComponent>) {
59-
for (const item of attributes) {
59+
for (const { node: item } of attributes) {
6060
if (!this.disabled) {
6161
this.controlledItems = this.activeItems;
6262
}

src/components/tabs/tabs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ export default class IgcTabsComponent extends EventEmitterMixin<
237237
changes,
238238
}: MutationControllerParams<IgcTabComponent>): void {
239239
const selected = changes.attributes.find(
240-
(tab) => this._tabs.includes(tab) && tab.selected
241-
);
240+
({ node: tab }) => this._tabs.includes(tab) && tab.selected
241+
)?.node;
242242
this._setSelectedTab(selected, false);
243243
}
244244

0 commit comments

Comments
 (0)