@excom/dom-observer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.rush/temp/chunked-rush-logs/dom-observer.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/dom-observer.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/dom-observer.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +5 -0
  15. package/dom-observer.ts +186 -0
  16. package/index.ts +17 -0
  17. package/package.json +45 -0
  18. package/rush-logs/dom-observer.apply-exports.cache.log +1 -0
  19. package/rush-logs/dom-observer.apply-exports.log +1 -0
  20. package/rush-logs/dom-observer.build_docs.cache.log +1 -0
  21. package/rush-logs/dom-observer.build_docs.log +1 -0
  22. package/rush-logs/dom-observer.build_package-metas.cache.log +1 -0
  23. package/rush-logs/dom-observer.build_package-metas.log +1 -0
  24. package/support/custom-elements.json +134 -0
  25. package/support/demos/simple.html +18 -0
  26. package/support/dist-docs/dom-observer.md +117 -0
  27. package/support/docs/INTERNAL.md +1 -0
  28. package/support/docs/README.md +59 -0
  29. package/support/package-meta.json +90 -0
  30. package/support/tests/__snapshots__/simple.view.test.ts.snap +23 -0
  31. package/support/tests/dom-observer.test.ts +326 -0
  32. package/support/tests/simple.view.test.ts +40 -0
  33. package/tsconfig.json +5 -0
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 36.671489000000065
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 46.228836
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 98.21526899999981
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "../../packages/dom-observer": "../../packages/dom-observer:n8+/7TfT7BWuKysR6wAgrprpaS+oWYsVdAAvlNgBNcM=:"
3
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
@@ -0,0 +1,186 @@
1
+ import { LoopGuard, selectOne } from "@excom/kit-utils";
2
+ import { Neutron, TEvent } from "@excom/neutron";
3
+
4
+ export type DomObserverChangeDetail = {
5
+ target: Element;
6
+ mutations: MutationRecord[];
7
+ };
8
+
9
+ export type DomObserverChangeEvent = TEvent & {
10
+ type: "dom-observer-change";
11
+ detail: DomObserverChangeDetail;
12
+ };
13
+
14
+ /**
15
+ * Resolves a target element via a CSS selector and fires a
16
+ * `dom-observer-change` event whenever that element mutates. Renders
17
+ * nothing of its own — pair it with `<event-handler>` (or Quark) to
18
+ * react to the changes.
19
+ *
20
+ * If `target-ref` does not match anything when the element connects,
21
+ * a document-level `MutationObserver` waits for a matching element to
22
+ * appear and then switches over to observing it.
23
+ *
24
+ * When the target is an `HTMLTemplateElement`, the `<template>` itself
25
+ * is observed (attribute/child mutations on the element), AND so is the
26
+ * template's `.content` `DocumentFragment` — otherwise mutations to the
27
+ * authored template children would be invisible (they don't live as
28
+ * descendants of the `<template>` element in the DOM tree).
29
+ *
30
+ * @fires dom-observer-change - Fires whenever the resolved target mutates,
31
+ * and once immediately (with `mutations: []`) as soon as the target is
32
+ * resolved so listeners can seed from current state. `mutations` is
33
+ * the `MutationRecord[]` from the underlying `MutationObserver`
34
+ * callback (empty on that first fire).
35
+ * @type DomObserverChangeEvent
36
+ *
37
+ * @example Log every mutation on a specific element
38
+ * <event-handler listen-for="dom-observer-change" target-ref="window">
39
+ * <dom-observer target-ref="#watched"></dom-observer>
40
+ * </event-handler>
41
+ */
42
+ export const DomObserver = Neutron({
43
+ tag: "dom-observer",
44
+ events: {
45
+ change: { prefixWithTag: true },
46
+ },
47
+ props: {
48
+ /**
49
+ * @option
50
+ * CSS selector used to resolve the element to observe. Resolved
51
+ * against `document`. If no element matches at connect time, the
52
+ * element waits for one to appear.
53
+ */
54
+ targetRef: String,
55
+ /**
56
+ * @state
57
+ * The currently observed target element (if any).
58
+ */
59
+ targetElement: HTMLElement,
60
+ /**
61
+ * @state
62
+ * Document-level observer used to wait for a target matching
63
+ * `target-ref` to appear. Disconnected as soon as the target is
64
+ * found.
65
+ */
66
+ targetFindingObserver: MutationObserver,
67
+ /**
68
+ * @state
69
+ * Observer attached to the resolved `targetElement` (and to its
70
+ * `.content` fragment when the target is a `<template>`).
71
+ */
72
+ targetChangeObserver: MutationObserver,
73
+ },
74
+ })
75
+ .defineMethods({
76
+ findTargetElement: (element) => {
77
+ // @ts-ignore TODO defineMethods
78
+ const { targetRef, setTargetElement } = element;
79
+ if (!targetRef) return;
80
+ const targetElement = selectOne(targetRef, {
81
+ scope: element,
82
+ });
83
+ if (targetElement) {
84
+ return { targetElement };
85
+ }
86
+ /* Wait for a match. `MutationRecord.target` on a childList mutation
87
+ is the *parent*, not the inserted child, so scan the added nodes
88
+ (and their descendants). */
89
+ const targetFindingObserver = new MutationObserver((mutations) => {
90
+ for (const mutation of mutations) {
91
+ for (const node of mutation.addedNodes) {
92
+ if (!(node instanceof Element)) continue;
93
+ const match = node.matches(targetRef)
94
+ ? node
95
+ : node.querySelector(targetRef);
96
+ if (match) {
97
+ setTargetElement(match);
98
+ return;
99
+ }
100
+ }
101
+ }
102
+ });
103
+ targetFindingObserver.observe(document.body, {
104
+ childList: true,
105
+ subtree: true,
106
+ });
107
+ return { targetFindingObserver };
108
+ },
109
+ setTargetElement: ({ targetFindingObserver }, targetElement) => {
110
+ targetFindingObserver?.disconnect();
111
+ return {
112
+ targetElement,
113
+ targetFindingObserver: null,
114
+ };
115
+ },
116
+ notifyChange: ({ targetElement }, mutations: MutationRecord[] = []) => ({
117
+ emit: ["change", { detail: { target: targetElement, mutations } }],
118
+ }),
119
+ })
120
+ .onPropChanged(
121
+ "targetRef",
122
+ ({ targetRef, targetFindingObserver }, previous) => {
123
+ const shouldDisconnect = previous.targetRef && targetFindingObserver;
124
+ if (shouldDisconnect) {
125
+ targetFindingObserver.disconnect();
126
+ }
127
+ return [
128
+ shouldDisconnect && { targetFindingObserver: null },
129
+ targetRef && { findTargetElement: [] },
130
+ ];
131
+ }
132
+ )
133
+ .onPropChanged(
134
+ "targetElement",
135
+ ({ targetElement, targetChangeObserver, notifyChange }, previous) => {
136
+ if (previous.targetElement && targetChangeObserver) {
137
+ targetChangeObserver.disconnect();
138
+ if (!targetElement) return { targetChangeObserver: null };
139
+ }
140
+ if (targetElement) {
141
+ /* The change event continues the chain that made the mutation
142
+ (a Quark rule writing an attribute, an effect inserting
143
+ children): inherit the loop-guard depth on those addresses. */
144
+ const observer = new MutationObserver((mutations) =>
145
+ LoopGuard.run(
146
+ Math.max(
147
+ 0,
148
+ ...mutations.map((m) =>
149
+ LoopGuard.depthOf(m.target, m.attributeName ?? "content")
150
+ )
151
+ ),
152
+ () => notifyChange(mutations)
153
+ )
154
+ );
155
+ const init: MutationObserverInit = {
156
+ attributes: true,
157
+ childList: true,
158
+ characterData: true,
159
+ subtree: true,
160
+ };
161
+ observer.observe(targetElement, init);
162
+ /* A `<template>`'s authored content lives on `.content`, not as
163
+ DOM descendants. Observe both so either mutation is one stream. */
164
+ if (targetElement instanceof HTMLTemplateElement) {
165
+ observer.observe(targetElement.content, init);
166
+ }
167
+ return [{ targetChangeObserver: observer }, { notifyChange: [] }];
168
+ }
169
+ }
170
+ )
171
+ .onDisconnected(
172
+ ({ isMoving, targetChangeObserver, targetFindingObserver }) => {
173
+ if (isMoving) return;
174
+ targetChangeObserver?.disconnect();
175
+ targetFindingObserver?.disconnect();
176
+ return {
177
+ targetElement: null,
178
+ targetChangeObserver: null,
179
+ targetFindingObserver: null,
180
+ };
181
+ }
182
+ )
183
+ .onConnected(({ isMoving, wasMounted, findTargetElement }) => {
184
+ if (isMoving || !wasMounted) return;
185
+ findTargetElement();
186
+ });
package/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { DomObserver } from "./dom-observer";
2
+
3
+ DomObserver.define();
4
+
5
+ export { DomObserver };
6
+
7
+ type T_HTMLDomObserverElement = typeof DomObserver.CustomElement;
8
+ declare global {
9
+ interface HTMLDomObserverElement extends T_HTMLDomObserverElement {}
10
+ interface Window {
11
+ HTMLDomObserverElement: HTMLDomObserverElement;
12
+ }
13
+ interface HTMLElementTagNameMap {
14
+ "dom-observer": HTMLDomObserverElement;
15
+ }
16
+ }
17
+ export type { HTMLDomObserverElement };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@excom/dom-observer",
3
+ "version": "0.1.0",
4
+ "description": "<dom-observer> custom element that resolves a target via a CSS selector and fires events on its mutations.",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@excom/neutron": "^0.1.0",
12
+ "@excom/kit-utils": "^0.1.0"
13
+ },
14
+ "peerDependencies": {},
15
+ "devDependencies": {
16
+ "@excom/heft-rig": "^0.1.0"
17
+ },
18
+ "repository": {
19
+ "url": "excom-dev/nucleus",
20
+ "directory": "packages/dom-observer"
21
+ },
22
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/dom-observer/support/docs/README.md",
23
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
24
+ "keywords": [
25
+ "dom-observer",
26
+ "neutron",
27
+ "custom-elements",
28
+ "mutation-observer"
29
+ ],
30
+ "excom": {
31
+ "documented": true,
32
+ "packageType": "kit-element"
33
+ },
34
+ "scripts": {
35
+ "build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
36
+ "build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
37
+ "format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
38
+ "test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
39
+ "coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
40
+ "dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
41
+ "preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
42
+ "build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
43
+ "build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
44
+ }
45
+ }
@@ -0,0 +1 @@
1
+ Caching has been disabled for this project's "apply-exports" command.
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1,134 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "modules": [
4
+ {
5
+ "kind": "javascript-module",
6
+ "path": "dom-observer.ts",
7
+ "declarations": [
8
+ {
9
+ "kind": "class",
10
+ "name": "DomObserver",
11
+ "customElement": true,
12
+ "tagName": "dom-observer",
13
+ "description": "Resolves a target element via a CSS selector and fires a `dom-observer-change` event whenever that element mutates. Renders nothing of its own — pair it with `<event-handler>` (or Quark) to react to the changes. If `target-ref` does not match anything when the element connects, a document-level `MutationObserver` waits for a matching element to appear and then switches over to observing it. When the target is an `HTMLTemplateElement`, the `<template>` itself is observed (attribute/child mutations on the element), AND so is the template's `.content` `DocumentFragment` — otherwise mutations to the authored template children would be invisible (they don't live as descendants of the `<template>` element in the DOM tree).",
14
+ "attributes": [
15
+ {
16
+ "name": "target-ref",
17
+ "type": {
18
+ "text": "string"
19
+ },
20
+ "description": "CSS selector used to resolve the element to observe. Resolved against `document`. If no element matches at connect time, the element waits for one to appear.",
21
+ "fieldName": "targetRef"
22
+ },
23
+ {
24
+ "name": "target-element",
25
+ "type": {
26
+ "text": "HTMLElement"
27
+ },
28
+ "description": "The currently observed target element (if any).",
29
+ "fieldName": "targetElement"
30
+ },
31
+ {
32
+ "name": "target-finding-observer",
33
+ "type": {
34
+ "text": "MutationObserver"
35
+ },
36
+ "description": "Document-level observer used to wait for a target matching `target-ref` to appear. Disconnected as soon as the target is found.",
37
+ "fieldName": "targetFindingObserver"
38
+ },
39
+ {
40
+ "name": "target-change-observer",
41
+ "type": {
42
+ "text": "MutationObserver"
43
+ },
44
+ "description": "Observer attached to the resolved `targetElement` (and to its `.content` fragment when the target is a `<template>`).",
45
+ "fieldName": "targetChangeObserver"
46
+ }
47
+ ],
48
+ "members": [
49
+ {
50
+ "kind": "field",
51
+ "name": "targetRef",
52
+ "type": {
53
+ "text": "string"
54
+ },
55
+ "privacy": "public",
56
+ "readonly": false,
57
+ "description": "CSS selector used to resolve the element to observe. Resolved against `document`. If no element matches at connect time, the element waits for one to appear.",
58
+ "_neutron": {
59
+ "surface": "option"
60
+ }
61
+ },
62
+ {
63
+ "kind": "field",
64
+ "name": "targetElement",
65
+ "type": {
66
+ "text": "HTMLElement"
67
+ },
68
+ "privacy": "public",
69
+ "readonly": true,
70
+ "description": "The currently observed target element (if any).",
71
+ "_neutron": {
72
+ "surface": "state"
73
+ }
74
+ },
75
+ {
76
+ "kind": "field",
77
+ "name": "targetFindingObserver",
78
+ "type": {
79
+ "text": "MutationObserver"
80
+ },
81
+ "privacy": "public",
82
+ "readonly": true,
83
+ "description": "Document-level observer used to wait for a target matching `target-ref` to appear. Disconnected as soon as the target is found.",
84
+ "_neutron": {
85
+ "surface": "state"
86
+ }
87
+ },
88
+ {
89
+ "kind": "field",
90
+ "name": "targetChangeObserver",
91
+ "type": {
92
+ "text": "MutationObserver"
93
+ },
94
+ "privacy": "public",
95
+ "readonly": true,
96
+ "description": "Observer attached to the resolved `targetElement` (and to its `.content` fragment when the target is a `<template>`).",
97
+ "_neutron": {
98
+ "surface": "state"
99
+ }
100
+ }
101
+ ],
102
+ "events": [
103
+ {
104
+ "name": "dom-observer-change",
105
+ "description": "Fires whenever the resolved target mutates, and once immediately (with `mutations: []`) as soon as the target is resolved so listeners can seed from current state. `mutations` is the `MutationRecord[]` from the underlying `MutationObserver` callback (empty on that first fire).",
106
+ "type": {
107
+ "text": "DomObserverChangeEvent",
108
+ "expanded": "CustomEvent & { type: \"dom-observer-change\"; detail: { target: Element; mutations: MutationRecord[]; }; bubbles: true; cancelable: true; composed: true }"
109
+ }
110
+ }
111
+ ]
112
+ }
113
+ ],
114
+ "exports": [
115
+ {
116
+ "kind": "js",
117
+ "name": "DomObserver",
118
+ "declaration": {
119
+ "name": "DomObserver",
120
+ "module": "dom-observer.ts"
121
+ }
122
+ },
123
+ {
124
+ "kind": "custom-element-definition",
125
+ "name": "dom-observer",
126
+ "declaration": {
127
+ "name": "DomObserver",
128
+ "module": "dom-observer.ts"
129
+ }
130
+ }
131
+ ]
132
+ }
133
+ ]
134
+ }
@@ -0,0 +1,18 @@
1
+ <div>
2
+ <section>
3
+ <dom-observer target-ref=":scope + details"></dom-observer>
4
+ <details id="do-watched">
5
+ <summary>Toggle me — mutates the "open" attribute</summary>
6
+ <p>Toggling this
7
+ fired <code>dom-observer-change</code>.</p>
8
+ </details>
9
+ <output class="tag-pre" style="white-space: pre;"></output>
10
+ </section>
11
+ <quark-sheet>
12
+ @use "/demo-utils" as *;
13
+
14
+ section {
15
+ @on dom-observer-change (handle: appendOutputFromDetail);
16
+ }
17
+ </quark-sheet>
18
+ </div>
@@ -0,0 +1,117 @@
1
+ # dom-observer
2
+
3
+ Fire an event whenever a configured element mutates.
4
+
5
+
6
+ ```html
7
+ <div>
8
+ <section>
9
+ <dom-observer target-ref=":scope + details"></dom-observer>
10
+ <details id="do-watched">
11
+ <summary>Toggle me — mutates the "open" attribute</summary>
12
+ <p>Toggling this
13
+ fired <code>dom-observer-change</code>.</p>
14
+ </details>
15
+ <output class="tag-pre" style="white-space: pre;"></output>
16
+ </section>
17
+ <quark-sheet>
18
+ @use "/demo-utils" as *;
19
+
20
+ section {
21
+ @on dom-observer-change (handle: appendOutputFromDetail);
22
+ }
23
+ </quark-sheet>
24
+ </div>
25
+ ```
26
+
27
+
28
+ ## Features
29
+
30
+ - **Mutation events** Fires `dom-observer-change` on target changes
31
+ - **Selector-based** `target-ref` resolves any element, anywhere
32
+ - **Waits for its target** No matching element yet? It watches for one
33
+ - **Fires once immediately** An empty-`mutations` fire on resolve lets
34
+ listeners seed from current state
35
+ - **`<template>`-aware** Also observes a template's `.content` fragment
36
+
37
+ ## Installation
38
+
39
+
40
+ `@excom/dom-observer` v0.1.0
41
+
42
+ ```bash
43
+ pnpm add @excom/dom-observer
44
+ ```
45
+
46
+ ```bash
47
+ npm install @excom/dom-observer
48
+ ```
49
+
50
+ ```bash
51
+ yarn add @excom/dom-observer
52
+ ```
53
+
54
+ ### Import
55
+
56
+ ```ts
57
+ import "@excom/dom-observer";
58
+ ```
59
+
60
+
61
+
62
+ ## Usage
63
+
64
+ Point `target-ref` at any selector, then react to `dom-observer-change`
65
+ with `<event-handler>` (or Quark).
66
+
67
+ ```html
68
+ <dom-observer target-ref="#watched"></dom-observer>
69
+ <event-handler listen-for="dom-observer-change" target-ref="#log">
70
+ <!-- runs on every #watched mutation, and once on attach -->
71
+ </event-handler>
72
+ ```
73
+
74
+ ### API Reference
75
+
76
+
77
+ #### Attributes
78
+
79
+ | Name | Surface | Type | Default | Values | Description |
80
+ | --- | --- | --- | --- | --- | --- |
81
+ | `target-ref` | option | `string` | | | CSS selector used to resolve the element to observe. Resolved against `document`. If no element matches at connect time, the element waits for one to appear. |
82
+ | `target-element` | state | `HTMLElement` | | | The currently observed target element (if any). |
83
+ | `target-finding-observer` | state | `MutationObserver` | | | Document-level observer used to wait for a target matching `target-ref` to appear. Disconnected as soon as the target is found. |
84
+ | `target-change-observer` | state | `MutationObserver` | | | Observer attached to the resolved `targetElement` (and to its `.content` fragment when the target is a `<template>`). |
85
+
86
+ #### Fires
87
+
88
+ | Name | Type | Description |
89
+ | --- | --- | --- |
90
+ | `dom-observer-change` | `DomObserverChangeEvent` (`CustomEvent & { type: "dom-observer-change"; detail: { target: Element; mutations: MutationRecord[]; }; bubbles: true; cancelable: true; composed: true }`) | Fires whenever the resolved target mutates, and once immediately (with `mutations: []`) as soon as the target is resolved so listeners can seed from current state. `mutations` is the `MutationRecord[]` from the underlying `MutationObserver` callback (empty on that first fire). |
91
+
92
+
93
+
94
+ ### Examples
95
+
96
+ #### Waiting for the target to exist
97
+
98
+ If nothing matches `target-ref` at connect time, `<dom-observer>` watches
99
+ the document for a match and switches over automatically — no glue code:
100
+
101
+ ```html
102
+ <dom-observer target-ref="article#late"></dom-observer>
103
+ ```
104
+
105
+ #### Observing a `<template>`
106
+
107
+ A `<template>`'s authored content lives on its `.content`
108
+ `DocumentFragment`, not as DOM descendants of the `<template>` itself.
109
+ `<dom-observer>` observes both, so mutations to either surface through the
110
+ same event stream:
111
+
112
+ ```html
113
+ <template id="rows">
114
+ <li>seed</li>
115
+ </template>
116
+ <dom-observer target-ref="#rows"></dom-observer>
117
+ ```
@@ -0,0 +1 @@
1
+ - Consider moving `findTargetElement` and related logic to its own Base element or common util. Could be useful for other elements that use `x-ref`s to watch for the desired element if it's not in the document yet.
@@ -0,0 +1,59 @@
1
+ # dom-observer
2
+
3
+ Fire an event whenever a configured element mutates.
4
+
5
+ <include-content data-demo="simple"></include-content>
6
+
7
+ ## Features
8
+
9
+ - **Mutation events** Fires `dom-observer-change` on target changes
10
+ - **Selector-based** `target-ref` resolves any element, anywhere
11
+ - **Waits for its target** No matching element yet? It watches for one
12
+ - **Fires once immediately** An empty-`mutations` fire on resolve lets
13
+ listeners seed from current state
14
+ - **`<template>`-aware** Also observes a template's `.content` fragment
15
+
16
+ ## Installation
17
+
18
+ <include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
19
+
20
+ ## Usage
21
+
22
+ Point `target-ref` at any selector, then react to `dom-observer-change`
23
+ with `<event-handler>` (or Quark).
24
+
25
+ ```html
26
+ <dom-observer target-ref="#watched"></dom-observer>
27
+ <event-handler listen-for="dom-observer-change" target-ref="#log">
28
+ <!-- runs on every #watched mutation, and once on attach -->
29
+ </event-handler>
30
+ ```
31
+
32
+ ### API Reference
33
+
34
+ <include-content is-active template-ref="/views/api-reference/api-reference.html"></include-content>
35
+
36
+ ### Examples
37
+
38
+ #### Waiting for the target to exist
39
+
40
+ If nothing matches `target-ref` at connect time, `<dom-observer>` watches
41
+ the document for a match and switches over automatically — no glue code:
42
+
43
+ ```html
44
+ <dom-observer target-ref="article#late"></dom-observer>
45
+ ```
46
+
47
+ #### Observing a `<template>`
48
+
49
+ A `<template>`'s authored content lives on its `.content`
50
+ `DocumentFragment`, not as DOM descendants of the `<template>` itself.
51
+ `<dom-observer>` observes both, so mutations to either surface through the
52
+ same event stream:
53
+
54
+ ```html
55
+ <template id="rows">
56
+ <li>seed</li>
57
+ </template>
58
+ <dom-observer target-ref="#rows"></dom-observer>
59
+ ```
@@ -0,0 +1,90 @@
1
+ {
2
+ "shortName": "dom-observer",
3
+ "package": {
4
+ "name": "@excom/dom-observer",
5
+ "version": "0.1.0",
6
+ "description": "<dom-observer> custom element that resolves a target via a CSS selector and fires events on its mutations.",
7
+ "peerDependencies": {},
8
+ "excom": {
9
+ "documented": true,
10
+ "packageType": "kit-element"
11
+ }
12
+ },
13
+ "demos": {
14
+ "simple": "<div>\n <section>\n <dom-observer target-ref=\":scope + details\"></dom-observer>\n <details id=\"do-watched\">\n <summary>Toggle me — mutates the \"open\" attribute</summary>\n <p>Toggling this\n fired <code>dom-observer-change</code>.</p>\n </details>\n <output class=\"tag-pre\" style=\"white-space: pre;\"></output>\n </section>\n <quark-sheet>\n @use \"/demo-utils\" as *;\n\n section {\n @on dom-observer-change (handle: appendOutputFromDetail);\n }\n </quark-sheet>\n</div>"
15
+ },
16
+ "readme": "<h1 id=\"md-dom-observer\">dom-observer</h1>\n<p>Fire an event whenever a configured element mutates.</p>\n<p><include-content data-demo=\"simple\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Mutation events</strong> Fires <code>dom-observer-change</code> on target changes</li>\n<li><strong>Selector-based</strong> <code>target-ref</code> resolves any element, anywhere</li>\n<li><strong>Waits for its target</strong> No matching element yet? It watches for one</li>\n<li><strong>Fires once immediately</strong> An empty-<code>mutations</code> fire on resolve lets\nlisteners seed from current state</li>\n<li><strong><code>&lt;template&gt;</code>-aware</strong> Also observes a template&#39;s <code>.content</code> fragment</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Point <code>target-ref</code> at any selector, then react to <code>dom-observer-change</code>\nwith <code>&lt;event-handler&gt;</code> (or Quark).</p>\n<include-content data-language=\"html\"><template>&lt;dom-observer target-ref=\"#watched\"&gt;&lt;/dom-observer&gt;\n&lt;event-handler listen-for=\"dom-observer-change\" target-ref=\"#log\"&gt;\n &lt;!-- runs on every #watched mutation, and once on attach --&gt;\n&lt;/event-handler&gt;</template></include-content>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-waiting-for-the-target-to-exist\">Waiting for the target to exist</h4>\n<p>If nothing matches <code>target-ref</code> at connect time, <code>&lt;dom-observer&gt;</code> watches\nthe document for a match and switches over automatically — no glue code:</p>\n<include-content data-language=\"html\"><template>&lt;dom-observer target-ref=\"article#late\"&gt;&lt;/dom-observer&gt;</template></include-content>\n<h4 id=\"md-observing-a-lt-template-gt\">Observing a <code>&lt;template&gt;</code></h4>\n<p>A <code>&lt;template&gt;</code>&#39;s authored content lives on its <code>.content</code>\n<code>DocumentFragment</code>, not as DOM descendants of the <code>&lt;template&gt;</code> itself.\n<code>&lt;dom-observer&gt;</code> observes both, so mutations to either surface through the\nsame event stream:</p>\n<include-content data-language=\"html\"><template>&lt;template id=\"rows\"&gt;\n &lt;li&gt;seed&lt;/li&gt;\n&lt;/template&gt;\n&lt;dom-observer target-ref=\"#rows\"&gt;&lt;/dom-observer&gt;</template></include-content>\n",
17
+ "docs": {
18
+ "readme": "<h1 id=\"md-dom-observer\">dom-observer</h1>\n<p>Fire an event whenever a configured element mutates.</p>\n<p><include-content data-demo=\"simple\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Mutation events</strong> Fires <code>dom-observer-change</code> on target changes</li>\n<li><strong>Selector-based</strong> <code>target-ref</code> resolves any element, anywhere</li>\n<li><strong>Waits for its target</strong> No matching element yet? It watches for one</li>\n<li><strong>Fires once immediately</strong> An empty-<code>mutations</code> fire on resolve lets\nlisteners seed from current state</li>\n<li><strong><code>&lt;template&gt;</code>-aware</strong> Also observes a template&#39;s <code>.content</code> fragment</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Point <code>target-ref</code> at any selector, then react to <code>dom-observer-change</code>\nwith <code>&lt;event-handler&gt;</code> (or Quark).</p>\n<include-content data-language=\"html\"><template>&lt;dom-observer target-ref=\"#watched\"&gt;&lt;/dom-observer&gt;\n&lt;event-handler listen-for=\"dom-observer-change\" target-ref=\"#log\"&gt;\n &lt;!-- runs on every #watched mutation, and once on attach --&gt;\n&lt;/event-handler&gt;</template></include-content>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-waiting-for-the-target-to-exist\">Waiting for the target to exist</h4>\n<p>If nothing matches <code>target-ref</code> at connect time, <code>&lt;dom-observer&gt;</code> watches\nthe document for a match and switches over automatically — no glue code:</p>\n<include-content data-language=\"html\"><template>&lt;dom-observer target-ref=\"article#late\"&gt;&lt;/dom-observer&gt;</template></include-content>\n<h4 id=\"md-observing-a-lt-template-gt\">Observing a <code>&lt;template&gt;</code></h4>\n<p>A <code>&lt;template&gt;</code>&#39;s authored content lives on its <code>.content</code>\n<code>DocumentFragment</code>, not as DOM descendants of the <code>&lt;template&gt;</code> itself.\n<code>&lt;dom-observer&gt;</code> observes both, so mutations to either surface through the\nsame event stream:</p>\n<include-content data-language=\"html\"><template>&lt;template id=\"rows\"&gt;\n &lt;li&gt;seed&lt;/li&gt;\n&lt;/template&gt;\n&lt;dom-observer target-ref=\"#rows\"&gt;&lt;/dom-observer&gt;</template></include-content>\n"
19
+ },
20
+ "installation": {
21
+ "name": "@excom/dom-observer",
22
+ "shortName": "dom-observer",
23
+ "version": "0.1.0",
24
+ "description": "<dom-observer> custom element that resolves a target via a CSS selector and fires events on its mutations.",
25
+ "packageType": "kit-element",
26
+ "cdn": "<script src=\"https://unpkg.com/@excom/kit-utils/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/neutron/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/dom-observer@0.1.0/dist/index.umd.min.js\"></script>",
27
+ "install": {
28
+ "npm": "npm install @excom/dom-observer"
29
+ },
30
+ "imports": {
31
+ "js": "import \"@excom/dom-observer\";",
32
+ "html": "<!-- import path to `node_modules` will depend on your build setup -->\n<script type=\"module\" src=\"/node_modules/@excom/dom-observer\"></script>\n<link rel=\"stylesheet\" href=\"/node_modules/@excom/dom-observer\">"
33
+ },
34
+ "peerDependencies": []
35
+ },
36
+ "elementApis": [
37
+ {
38
+ "tag": "dom-observer",
39
+ "kind": "class",
40
+ "attributes": [
41
+ {
42
+ "name": "target-ref",
43
+ "type": "string",
44
+ "description": "CSS selector used to resolve the element to observe. Resolved against <code>document</code>. If no element matches at connect time, the element waits for one to appear.",
45
+ "fieldName": "targetRef",
46
+ "surface": "option"
47
+ },
48
+ {
49
+ "name": "target-change-observer",
50
+ "type": "MutationObserver",
51
+ "description": "Observer attached to the resolved <code>targetElement</code> (and to its <code>.content</code> fragment when the target is a <code>&lt;template&gt;</code>).",
52
+ "fieldName": "targetChangeObserver",
53
+ "surface": "state"
54
+ },
55
+ {
56
+ "name": "target-element",
57
+ "type": "HTMLElement",
58
+ "description": "The currently observed target element (if any).",
59
+ "fieldName": "targetElement",
60
+ "surface": "state"
61
+ },
62
+ {
63
+ "name": "target-finding-observer",
64
+ "type": "MutationObserver",
65
+ "description": "Document-level observer used to wait for a target matching <code>target-ref</code> to appear. Disconnected as soon as the target is found.",
66
+ "fieldName": "targetFindingObserver",
67
+ "surface": "state"
68
+ }
69
+ ],
70
+ "events": [
71
+ {
72
+ "name": "dom-observer-change",
73
+ "description": "Fires whenever the resolved target mutates, and once immediately (with <code>mutations: []</code>) as soon as the target is resolved so listeners can seed from current state. <code>mutations</code> is the <code>MutationRecord[]</code> from the underlying <code>MutationObserver</code> callback (empty on that first fire).",
74
+ "type": "DomObserverChangeEvent",
75
+ "typeExpanded": "CustomEvent & { type: \"dom-observer-change\"; detail: { target: Element; mutations: MutationRecord[]; }; bubbles: true; cancelable: true; composed: true }"
76
+ }
77
+ ],
78
+ "slots": [],
79
+ "cssProperties": [],
80
+ "cssClasses": [],
81
+ "cssAliases": [],
82
+ "listens": [],
83
+ "commands": [],
84
+ "defaultActions": [],
85
+ "expectedChildren": [],
86
+ "provisions": []
87
+ }
88
+ ],
89
+ "exportedFiles": {}
90
+ }
@@ -0,0 +1,23 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`simple view > logs attribute mutations on the details element > complexity 1`] = `
4
+ {
5
+ "attributeRuns": 0,
6
+ "closest": 0,
7
+ "getVar": 0,
8
+ "importNode": 0,
9
+ "listenerRuns": 0,
10
+ "matches": 0,
11
+ "parentElement": 0,
12
+ "quarkRuns": 0,
13
+ "queryScopeCost": 0,
14
+ "querySelectorAll": 0,
15
+ "removeAttribute": 0,
16
+ "ruleRuns": 0,
17
+ "schedulePaint": 0,
18
+ "setAttribute": 1,
19
+ "setVar": 0,
20
+ "textContent": 1,
21
+ "variableRuns": 0,
22
+ }
23
+ `;
@@ -0,0 +1,326 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ fixture,
7
+ it,
8
+ vi,
9
+ wait,
10
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
11
+ import "../../index";
12
+
13
+ describe("dom-observer", () => {
14
+ /*
15
+ * Listen at document before any fixture, so the sync
16
+ * `dom-observer-change` on connect is captured.
17
+ */
18
+ let events: CustomEvent[];
19
+ const listener = (e: Event) => events.push(e as CustomEvent);
20
+
21
+ beforeEach(() => {
22
+ events = [];
23
+ document.addEventListener("dom-observer-change", listener);
24
+ });
25
+
26
+ afterEach(() => {
27
+ document.removeEventListener("dom-observer-change", listener);
28
+ document.body.innerHTML = "";
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ it("emits dom-observer-change once the target is resolved", async () => {
33
+ const target = document.createElement("div");
34
+ target.id = "watched";
35
+ document.body.appendChild(target);
36
+
37
+ const el = fixture<HTMLDomObserverElement>(
38
+ `<dom-observer target-ref="#watched"></dom-observer>`,
39
+ );
40
+
41
+ await wait(0);
42
+ expect(events.length).toBe(1);
43
+ expect(events[0].detail.mutations).toEqual([]);
44
+ expect(el.targetElement).toBe(target);
45
+ });
46
+
47
+ it("emits dom-observer-change on attribute mutations of the target", async () => {
48
+ const target = document.createElement("div");
49
+ target.id = "watched";
50
+ document.body.appendChild(target);
51
+
52
+ fixture<HTMLDomObserverElement>(
53
+ `<dom-observer target-ref="#watched"></dom-observer>`,
54
+ );
55
+ await wait(0);
56
+ events.length = 0;
57
+
58
+ target.setAttribute("data-state", "active");
59
+ await wait(0);
60
+
61
+ expect(events.length).toBeGreaterThanOrEqual(1);
62
+ const last = events[events.length - 1];
63
+ expect(last.detail.mutations[0].type).toBe("attributes");
64
+ expect(last.detail.mutations[0].attributeName).toBe("data-state");
65
+ });
66
+
67
+ it("emits dom-observer-change on childList mutations of the target", async () => {
68
+ const target = document.createElement("ul");
69
+ target.id = "watched";
70
+ document.body.appendChild(target);
71
+
72
+ fixture<HTMLDomObserverElement>(
73
+ `<dom-observer target-ref="#watched"></dom-observer>`,
74
+ );
75
+ await wait(0);
76
+ events.length = 0;
77
+
78
+ target.appendChild(document.createElement("li"));
79
+ await wait(0);
80
+
81
+ const types = events.flatMap((e) =>
82
+ (e.detail.mutations as MutationRecord[]).map((m) => m.type),
83
+ );
84
+ expect(types).toContain("childList");
85
+ });
86
+
87
+ it("waits for the target to appear before observing", async () => {
88
+ const el = fixture<HTMLDomObserverElement>(
89
+ `<dom-observer target-ref="#late"></dom-observer>`,
90
+ );
91
+ await wait(0);
92
+ expect(events.length).toBe(0);
93
+ expect(el.targetElement).toBeFalsy();
94
+
95
+ const target = document.createElement("section");
96
+ target.id = "late";
97
+ document.body.appendChild(target);
98
+ await wait(0);
99
+
100
+ expect(el.targetElement).toBe(target);
101
+ expect(events.length).toBe(1);
102
+ expect(events[0].detail.mutations).toEqual([]);
103
+ });
104
+
105
+ it("observes a <template>'s .content fragment", async () => {
106
+ const tpl = document.createElement("template");
107
+ tpl.id = "rows";
108
+ tpl.innerHTML = "<li>seed</li>";
109
+ document.body.appendChild(tpl);
110
+
111
+ fixture<HTMLDomObserverElement>(
112
+ `<dom-observer target-ref="#rows"></dom-observer>`,
113
+ );
114
+ await wait(0);
115
+ events.length = 0;
116
+
117
+ // Mutating `template.content` must surface; `.content` is a
118
+ // DocumentFragment outside the `<template>` descendant tree.
119
+ const li = document.createElement("li");
120
+ li.textContent = "added";
121
+ tpl.content.appendChild(li);
122
+ await wait(0);
123
+
124
+ const types = events.flatMap((e) =>
125
+ (e.detail.mutations as MutationRecord[]).map((m) => m.type),
126
+ );
127
+ expect(types).toContain("childList");
128
+ });
129
+
130
+ it("emits dom-observer-change on characterData mutations of the target", async () => {
131
+ const target = document.createElement("p");
132
+ target.id = "watched";
133
+ target.textContent = "before";
134
+ document.body.appendChild(target);
135
+
136
+ fixture<HTMLDomObserverElement>(
137
+ `<dom-observer target-ref="#watched"></dom-observer>`,
138
+ );
139
+ await wait(0);
140
+ events.length = 0;
141
+
142
+ (target.firstChild as Text).data = "after";
143
+ await wait(0);
144
+
145
+ const types = events.flatMap((e) =>
146
+ (e.detail.mutations as MutationRecord[]).map((m) => m.type),
147
+ );
148
+ expect(types).toContain("characterData");
149
+ });
150
+
151
+ it("finds a late target nested inside an inserted subtree", async () => {
152
+ const el = fixture<HTMLDomObserverElement>(
153
+ `<dom-observer target-ref="#late-nested"></dom-observer>`,
154
+ );
155
+ await wait(0);
156
+ expect(el.targetElement).toBeFalsy();
157
+
158
+ const wrapper = document.createElement("div");
159
+ wrapper.innerHTML = `<section><p id="late-nested"></p></section>`;
160
+ document.body.appendChild(wrapper);
161
+ await wait(0);
162
+
163
+ expect(el.targetElement).toBe(wrapper.querySelector("#late-nested"));
164
+ expect(events.length).toBe(1);
165
+ });
166
+
167
+ it("re-targets while still waiting for the first target", async () => {
168
+ const target = document.createElement("div");
169
+ target.id = "watched";
170
+ document.body.appendChild(target);
171
+
172
+ const el = fixture<HTMLDomObserverElement>(
173
+ `<dom-observer target-ref="#never-appears"></dom-observer>`,
174
+ );
175
+ await wait(0);
176
+ expect(el.targetFindingObserver).toBeTruthy();
177
+ expect(events.length).toBe(0);
178
+
179
+ el.targetRef = "#watched";
180
+ await wait(0);
181
+ expect(el.targetElement).toBe(target);
182
+ expect(el.targetFindingObserver).toBeNull();
183
+ expect(events.length).toBe(1);
184
+
185
+ // the abandoned finder no longer switches targets
186
+ const late = document.createElement("div");
187
+ late.id = "never-appears";
188
+ document.body.appendChild(late);
189
+ await wait(0);
190
+ expect(el.targetElement).toBe(target);
191
+ });
192
+
193
+ it("re-observes when target-ref moves to another resolved element", async () => {
194
+ const first = document.createElement("div");
195
+ first.id = "first";
196
+ const second = document.createElement("div");
197
+ second.id = "second";
198
+ document.body.append(first, second);
199
+
200
+ const el = fixture<HTMLDomObserverElement>(
201
+ `<dom-observer target-ref="#first"></dom-observer>`,
202
+ );
203
+ await wait(0);
204
+ expect(el.targetElement).toBe(first);
205
+ events.length = 0;
206
+
207
+ el.targetRef = "#second";
208
+ await wait(0);
209
+ expect(el.targetElement).toBe(second);
210
+ // seeds listeners from the new target
211
+ expect(events.length).toBe(1);
212
+ expect(events[0].detail.target).toBe(second);
213
+ events.length = 0;
214
+
215
+ first.setAttribute("data-state", "ignored");
216
+ await wait(0);
217
+ expect(events.length).toBe(0);
218
+
219
+ second.setAttribute("data-state", "seen");
220
+ await wait(0);
221
+ expect(events.length).toBe(1);
222
+ expect(events[0].detail.target).toBe(second);
223
+ expect(events[0].detail.mutations[0].attributeName).toBe("data-state");
224
+ });
225
+
226
+ it("stops observing when the target is cleared", async () => {
227
+ const target = document.createElement("div");
228
+ target.id = "watched";
229
+ document.body.appendChild(target);
230
+
231
+ const el = fixture<HTMLDomObserverElement>(
232
+ `<dom-observer target-ref="#watched"></dom-observer>`,
233
+ );
234
+ await wait(0);
235
+ expect(el.targetChangeObserver).toBeTruthy();
236
+ events.length = 0;
237
+
238
+ el.targetElement = null;
239
+ await wait(0);
240
+ expect(el.targetChangeObserver).toBeNull();
241
+
242
+ target.setAttribute("data-state", "cleared");
243
+ await wait(0);
244
+ expect(events.length).toBe(0);
245
+ });
246
+
247
+ it("keeps observing across a synchronous move", async () => {
248
+ const target = document.createElement("div");
249
+ target.id = "watched";
250
+ document.body.appendChild(target);
251
+
252
+ const el = fixture<HTMLDomObserverElement>(
253
+ `<dom-observer target-ref="#watched"></dom-observer>`,
254
+ );
255
+ await wait(0);
256
+ events.length = 0;
257
+
258
+ const other = document.createElement("div");
259
+ document.body.appendChild(other);
260
+ other.appendChild(el);
261
+ await wait(0);
262
+ expect(el.targetElement).toBe(target);
263
+ expect(events.length).toBe(0);
264
+
265
+ target.setAttribute("data-state", "moved");
266
+ await wait(0);
267
+ expect(events.length).toBe(1);
268
+ });
269
+
270
+ it("re-resolves the target after a real disconnect and reconnect", async () => {
271
+ const target = document.createElement("div");
272
+ target.id = "watched";
273
+ document.body.appendChild(target);
274
+
275
+ const el = fixture<HTMLDomObserverElement>(
276
+ `<dom-observer target-ref="#watched"></dom-observer>`,
277
+ );
278
+ await wait(0);
279
+ el.remove();
280
+ await wait(0);
281
+ expect(el.targetElement).toBeNull();
282
+ events.length = 0;
283
+
284
+ document.body.appendChild(el);
285
+ await wait(0);
286
+ expect(el.targetElement).toBe(target);
287
+ expect(events.length).toBe(1);
288
+ expect(events[0].detail.mutations).toEqual([]);
289
+
290
+ target.setAttribute("data-state", "reconnected");
291
+ await wait(0);
292
+ expect(events.length).toBe(2);
293
+ });
294
+
295
+ it("reconnects without a target-ref as a no-op", async () => {
296
+ const el = fixture<HTMLDomObserverElement>(`<dom-observer></dom-observer>`);
297
+ await wait(0);
298
+ el.remove();
299
+ await wait(0);
300
+ expect(() => document.body.appendChild(el)).not.toThrow();
301
+ await wait(0);
302
+ expect(el.targetElement).toBeFalsy();
303
+ expect(el.targetFindingObserver).toBeFalsy();
304
+ expect(events.length).toBe(0);
305
+ });
306
+
307
+ it("disconnects observers when disconnected", async () => {
308
+ const target = document.createElement("div");
309
+ target.id = "watched";
310
+ document.body.appendChild(target);
311
+
312
+ const el = fixture<HTMLDomObserverElement>(
313
+ `<dom-observer target-ref="#watched"></dom-observer>`,
314
+ );
315
+ await wait(0);
316
+ events.length = 0;
317
+
318
+ el.remove();
319
+ await wait(0);
320
+
321
+ target.setAttribute("data-state", "after-disconnect");
322
+ await wait(0);
323
+ expect(events.length).toBe(0);
324
+ expect(el.targetElement).toBeNull();
325
+ });
326
+ });
@@ -0,0 +1,40 @@
1
+ import "@excom/quark-sheet";
2
+ import "../../index";
3
+ import {
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ it,
9
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
10
+ import {
11
+ expectComplexity,
12
+ flush,
13
+ installDemoModules,
14
+ measureComplexity,
15
+ mountView,
16
+ readDemo,
17
+ restoreDemoModules,
18
+ } from "@excom/quark/support/tests/view-helpers";
19
+
20
+ describe("simple view", () => {
21
+ beforeEach(() => installDemoModules());
22
+ afterEach(() => {
23
+ document.body.innerHTML = "";
24
+ restoreDemoModules();
25
+ });
26
+
27
+ it("logs attribute mutations on the details element", async () => {
28
+ const { root, quark } = await mountView(readDemo(import.meta.url, "simple"));
29
+ const details = root.querySelector("details")!;
30
+ const meter = measureComplexity(quark!);
31
+ details.setAttribute("open", "");
32
+ await flush();
33
+ const budget = meter.take();
34
+ meter.stop();
35
+ expect(root.querySelector("output")?.textContent).toMatch(
36
+ /HTMLDetailsElement|MutationRecord/,
37
+ );
38
+ expectComplexity(budget);
39
+ });
40
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "@excom/heft-rig/profiles/default/config/tsconfig.json",
3
+ "include": ["./*.ts"],
4
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
5
+ }