mount-observer 0.1.13 → 0.1.15

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 (43) hide show
  1. package/Events.js +9 -0
  2. package/Events.ts +12 -0
  3. package/MountObserver.js +21 -0
  4. package/MountObserver.ts +27 -0
  5. package/README.md +406 -69
  6. package/{DefineCustomElementHandler.js → handlers/DefineCustomElement.js} +103 -99
  7. package/handlers/DefineCustomElement.ts +123 -0
  8. package/handlers/EnhanceMountedElement.js +99 -0
  9. package/handlers/EnhanceMountedElement.ts +116 -0
  10. package/handlers/Events.js +110 -0
  11. package/handlers/EvtRt.js +59 -0
  12. package/handlers/GenIds.js +37 -0
  13. package/handlers/GenIds.ts +45 -0
  14. package/handlers/HTMLInclude.js +393 -0
  15. package/handlers/HTMLInclude.ts +453 -0
  16. package/handlers/HoistTemplate.js +77 -0
  17. package/handlers/HoistTemplate.ts +89 -0
  18. package/handlers/MountObserver.js +941 -0
  19. package/handlers/MountObserverScript.js +78 -0
  20. package/handlers/MountObserverScript.ts +89 -0
  21. package/handlers/ScriptExport.js +83 -0
  22. package/handlers/ScriptExport.ts +97 -0
  23. package/handlers/SharedMutationObserver.js +78 -0
  24. package/handlers/arr.js +16 -0
  25. package/handlers/connectionMonitor.js +122 -0
  26. package/handlers/elementIntersection.js +73 -0
  27. package/handlers/emitEvents.js +187 -0
  28. package/handlers/getRegistryRoot.js +52 -0
  29. package/handlers/loadImports.js +129 -0
  30. package/handlers/mediaQuery.js +90 -0
  31. package/handlers/rootSizeObserver.js +131 -0
  32. package/handlers/upShadowSearch.js +70 -0
  33. package/handlers/withScopePerimeter.js +22 -0
  34. package/index.js +2 -2
  35. package/index.ts +2 -2
  36. package/package.json +13 -3
  37. package/types/assign-gingerly/types.d.ts +244 -0
  38. package/types/be-a-beacon/types.d.ts +3 -0
  39. package/types/global.d.ts +29 -0
  40. package/types/id-generation/types.d.ts +26 -0
  41. package/types/mount-observer/types.d.ts +330 -0
  42. package/upShadowSearch.js +6 -3
  43. package/upShadowSearch.ts +6 -3
package/README.md CHANGED
@@ -10,6 +10,7 @@ The following features have been implemented and tested:
10
10
 
11
11
  ### Core Functionality
12
12
  - ✅ **matching**: CSS selector-based element matching
13
+ - ✅ **whenDefined**: Wait for custom elements to be defined before mounting
13
14
  - ✅ **whereInstanceOf**: Constructor-based element filtering (single or array)
14
15
  - ✅ **whereLocalNameMatches**: Regular expression-based localName filtering
15
16
  - ✅ **shouldMount**: Custom JavaScript check for complex mounting conditions
@@ -85,7 +86,7 @@ There is quite a bit of functionality this proposal would open up that is exceed
85
86
 
86
87
  4. Some CSS selectors, such as the [donut hole scope range](https://css-tricks.com/solved-by-css-donuts-scopes/#aa-donut-scoping-with-scope), aren't supported by oEl.querySelectorAll(...) or oEl.matches(...).
87
88
 
88
- 5. Scoped custom element registries form natural "islands" of DOM that have many commonalities with css "donut hole scoping", and which mutation observers aren't really designed around. The mount-observer is designed to work with scoped custom element registries as first-class citizens.
89
+ 5. Scoped custom element registries form natural "islands" of DOM that have many commonalities with css "donut hole scoping", and which mutation observers aren't really designed around. The mount-observer is designed to work with scoped custom element registries as first-class citizens. Learn more about [scoped custom element registries](https://developer.chrome.com/blog/scoped-registries).
89
90
 
90
91
 
91
92
  ### Most significant use cases
@@ -103,7 +104,7 @@ The extra flexibility this new primitive would provide could be quite useful to
103
104
 
104
105
  ## Quick Examples of the Most Common Use Cases
105
106
 
106
- Before getting into the weeds, let's demonstrate the two most prominent use cases:
107
+ Before getting into the weeds, let's demonstrate a few of the most prominent use cases:
107
108
 
108
109
  ### Use Case 1: Custom Attribute Enhancement
109
110
 
@@ -217,58 +218,136 @@ The handler:
217
218
  3. Stores the enhancement instance on `element.enh[enhKey]` if an `enhKey` is provided
218
219
 
219
220
 
220
- ## Loading ES Modules from Script Elements
221
+ ## Exposing Module Exports from Script Elements
221
222
 
222
- The `builtIns.scriptNoModule` handler enables declarative module loading using `<script nomodule>` elements. This provides a way to import ES modules and JSON data directly from HTML without writing JavaScript.
223
+ The `builtIns.scriptExport` handler solves a long-standing limitation: accessing ES module exports from script elements. It also provides a clean way to import JSON and other data formats declaratively.
224
+
225
+ ### Problem 1: ES Module Export Access
226
+
227
+ The browser doesn't expose module exports from `<script type="module">` elements. There's been a [decade-old proposal](https://github.com/whatwg/html/issues/1013) to add this, but it remains unimplemented.
228
+
229
+ **The Solution:**
223
230
 
224
231
  ```html
225
- <!-- Load a JavaScript module -->
232
+ <!-- Use nomodule to prevent browser from loading it separately -->
226
233
  <script nomodule src="./config.js" id="myConfig"></script>
227
234
 
228
- <!-- Load JSON data with import assertion -->
229
- <script nomodule src="./data.json" with-type="json" id="myData"></script>
230
-
231
235
  <script type="module">
232
236
  import { MountObserver } from 'mount-observer/MountObserver.js';
233
237
 
234
- // Handler provides matching and whereInstanceOf via static properties
235
238
  const observer = new MountObserver({
236
- do: 'builtIns.scriptNoModule'
239
+ do: 'builtIns.scriptExport'
237
240
  });
238
241
  observer.observe(document);
239
242
 
240
- // Access the imported modules
243
+ // Access the module's exports via element.export
241
244
  const config = document.getElementById('myConfig').export;
242
- const data = document.getElementById('myData').export.default;
245
+ console.log(config.apiKey);
246
+ console.log(config.endpoints);
247
+ </script>
248
+ ```
249
+
250
+ **Why `nomodule`?**
251
+ - Prevents the browser from loading the module separately
252
+ - Avoids having the module loaded twice in memory
253
+ - The handler imports it once and exposes exports via `element.export`
254
+
255
+ ### Problem 2: Declarative JSON Import
256
+
257
+ Importing JSON typically requires `fetch()` or dynamic `import()` with assertions. This handler provides a declarative alternative.
258
+
259
+ **The Solution:**
260
+
261
+ ```html
262
+ <!-- Load JSON data -->
263
+ <script src="./data.json" type="json" id="myData"></script>
264
+
265
+ <!-- Also supports full MIME types -->
266
+ <script src="./config.json" type="application/json" id="config"></script>
267
+ <script src="./linked-data.json" type="application/ld+json" id="linkedData"></script>
268
+
269
+ <script type="module">
270
+ import { MountObserver } from 'mount-observer/MountObserver.js';
243
271
 
244
- console.log(config.mountConfig); // Access exported values
245
- console.log(data); // Access JSON data
272
+ const observer = new MountObserver({
273
+ do: 'builtIns.scriptExport'
274
+ });
275
+ observer.observe(document);
276
+
277
+ // Access the JSON data via element.export.default
278
+ const data = document.getElementById('myData').export.default;
279
+ console.log(data.items);
280
+ console.log(data.config);
246
281
  </script>
247
282
  ```
248
283
 
284
+ <details>
285
+ <summary>Talking points</summary>
286
+
287
+ **Why no `nomodule` for JSON?**
288
+ - The browser ignores script elements with non-standard type attributes
289
+ - No risk of double-loading since the browser won't load it at all
290
+ - The handler imports it with the appropriate JSON assertion
291
+
292
+ **Supported JSON types:**
293
+ - `type="json"` - Simple and clean
294
+ - `type="application/json"` - Standard MIME type
295
+ - `type="application/ld+json"` - JSON-LD linked data
296
+ - Any type containing "json" triggers JSON import assertion
297
+
249
298
  **How it works:**
250
- 1. The handler matches `script[nomodule][src]` elements (via static properties)
251
- 2. Reads the `src` attribute and resolves it relative to the document
252
- 3. Checks for optional `with-type` attribute for import assertions (e.g., `"json"`)
253
- 4. Dynamically imports the module: `import(src, { with: { type: withType } })`
254
- 5. Stores the imported module on `element.export`
299
+ 1. Matches `script[src]` elements (via static properties)
300
+ 2. Skips `type="module"` scripts (browser-handled)
301
+ 3. Processes scripts with `nomodule` attribute OR type containing "json"
302
+ 4. Resolves the `src` relative to the document
303
+ 5. Imports with appropriate assertion (JSON if type contains "json")
304
+ 6. Stores the imported module on `element.export`
305
+ 7. Dispatches a `resolved` event with the imported module
306
+
307
+ **Reusing imported modules:**
308
+
309
+ The handler stores the imported module on the script element's `export` property and dispatches a `resolved` event. This allows other code to access the module without re-importing:
310
+
311
+ ```html
312
+ <script src="./data.json" type="json" id="myData"></script>
313
+
314
+ <script type="module">
315
+ const dataScript = document.getElementById('myData');
316
+
317
+ // Listen for the resolved event
318
+ dataScript.addEventListener('resolved', (e) => {
319
+ console.log('Data loaded:', e.export);
320
+ // e.export contains the imported module
321
+ // For JSON: e.export.default contains the data
322
+ });
323
+
324
+ // Or access directly after processing
325
+ // dataScript.export will contain the imported module
326
+ </script>
327
+ ```
328
+
329
+ This is particularly useful when multiple components need to access the same data or configuration without triggering multiple imports.
255
330
 
256
331
  **Benefits:**
257
- - Declarative module loading directly in HTML
258
- - Supports JSON imports with `with-type="json"` attribute
259
- - No need to specify `matching` or `whereInstanceOf` (handler provides defaults)
260
- - Modules are accessible via `scriptElement.export`
332
+ - Access ES module exports from script elements (finally!)
333
+ - Declarative JSON loading without fetch
334
+ - Prevents double-loading of modules
335
+ - Clean, intuitive syntax
261
336
  - Works with relative and absolute URLs
337
+ - No need to specify `matching` or `whereInstanceOf` (handler provides defaults)
262
338
 
263
339
  **Use cases:**
264
- - Loading configuration files declaratively
265
- - Importing JSON data without fetch
340
+ - Accessing configuration module exports in HTML
341
+ - Loading JSON data declaratively
266
342
  - Progressive enhancement with module loading
267
- - Declarative dependency management in HTML
343
+ - Declarative dependency management
344
+ - Loading JSON-LD structured data
345
+
346
+ </details>
268
347
 
269
348
  ## Mount Observer Script Elements (MOSEs)
270
349
 
271
- The `builtIns.mountObserverScript` handler enables fully declarative mount observer configuration using `<script type="mountobserver">` elements. This provides the ultimate in HTML-first progressive enhancement.
350
+ Inspired by the [speculation rules api](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) `builtIns.mountObserverScript` handler enables fully declarative mount observer configuration using `<script type="mountobserver">` elements. This provides the ultimate in HTML-first progressive enhancement.
272
351
 
273
352
  ```html
274
353
  <!-- Inline JSON configuration -->
@@ -295,12 +374,73 @@ The `builtIns.mountObserverScript` handler enables fully declarative mount obser
295
374
  </script>
296
375
  ```
297
376
 
377
+ <details>
378
+ <summary>Talking points</summary>
379
+
298
380
  **How it works:**
299
381
  1. The handler matches `script[type="mountobserver"]` elements (via static properties)
300
382
  2. If the script has a `src` attribute, imports JSON from that URL
301
383
  3. Otherwise, parses the script's textContent as JSON
302
- 4. Calls `scriptElement.mount(config)` with the parsed configuration
303
- 5. The `mount()` method creates a MountObserver for that configuration
384
+ 4. Supports both single config objects and arrays of configs
385
+ 5. Stores the parsed config on `scriptElement.export` for reuse
386
+ 6. Dispatches a `resolved` event with the parsed config
387
+ 7. Calls `scriptElement.mount(config)` for each configuration
388
+ 8. The `mount()` method creates a MountObserver for that configuration
389
+
390
+ **Reusing parsed configurations:**
391
+
392
+ The handler optimizes performance by storing the parsed configuration on the script element's `export` property and dispatching a `resolved` event. When the same script element is observed again (e.g., after being cloned or moved), the handler reuses the existing `export` instead of re-parsing:
393
+
394
+ ```html
395
+ <script type="mountobserver" id="myConfig">
396
+ {
397
+ "matching": ".my-element",
398
+ "do": "builtIns.logToConsole"
399
+ }
400
+ </script>
401
+
402
+ <script type="module">
403
+ const configScript = document.getElementById('myConfig');
404
+
405
+ // Listen for the resolved event (fires only once on first parse)
406
+ configScript.addEventListener('resolved', (e) => {
407
+ console.log('Config loaded:', e.export);
408
+ // e.export contains the parsed configuration
409
+ });
410
+
411
+ // Or access directly after processing
412
+ // configScript.export will contain the parsed config
413
+
414
+ // If observed again, the handler will reuse configScript.export
415
+ // without re-parsing or firing the resolved event
416
+ </script>
417
+ ```
418
+
419
+ This is particularly useful when inheriting mount observer configurations across shadow DOM boundaries, as the parsed config can be reused without re-parsing JSON. The `resolved` event fires only once (on first parse), but the handler will still process the configuration on subsequent observations.
420
+
421
+ **Multiple configs in one script:**
422
+
423
+ You can define multiple mount observer configurations in a single script element using a JSON array:
424
+
425
+ ```html
426
+ <script type="mountobserver">
427
+ [
428
+ {
429
+ "do": "builtIns.hoistTemplate"
430
+ },
431
+ {
432
+ "do": "builtIns.HTMLInclude"
433
+ },
434
+ {
435
+ "matching": "my-button",
436
+ "import": "./my-button.js",
437
+ "do": "builtIns.defineCustomElement"
438
+ }
439
+ ]
440
+ </script>
441
+ ```
442
+
443
+ This is equivalent to having three separate `<script type="mountobserver">` elements, but more concise. Each config in the array is processed independently and creates its own MountObserver.
304
444
 
305
445
  **Benefits:**
306
446
  - Zero JavaScript required for observer configuration
@@ -347,12 +487,16 @@ The `builtIns.mountObserverScript` handler enables fully declarative mount obser
347
487
  </script>
348
488
  ```
349
489
 
490
+ </details>
491
+
350
492
  ## Hoisting Templates for Performance
351
493
 
352
- The `builtIns.hoistTemplate` handler optimizes template usage by moving a template element's content from shadow roots to `document.head`. This is particularly useful when templates with IDs are repeated across multiple custom elements.
494
+ The `builtIns.hoistTemplate` handler optimizes template usage by moving a template element's content from shadow roots to `document.head`.
353
495
 
354
496
  **Why hoist templates?**
355
497
 
498
+ Template hoisting is particularly useful when you need to share conditional or repeated templates across multiple custom element instances.
499
+
356
500
  When HTML-first custom elements repeat throughout a page, each instance typically contains its own copy of template content. Moving these templates to a centralized location:
357
501
  - Reduces memory usage (one template instead of many copies)
358
502
  - Improves cloning performance (single source of truth)
@@ -467,6 +611,9 @@ The `builtIns.HTMLInclude` handler enables declarative HTML fragment reuse withi
467
611
  </script>
468
612
  ```
469
613
 
614
+ <details>
615
+ <summary>More discussion
616
+
470
617
  **What happens:**
471
618
  1. The handler finds templates with `src` attributes starting with `#`
472
619
  2. Searches for an element with that ID (across shadow boundaries)
@@ -507,6 +654,8 @@ The `builtIns.HTMLInclude` handler enables declarative HTML fragment reuse withi
507
654
  </script>
508
655
  ```
509
656
 
657
+ </details>
658
+
510
659
  ### Shadow DOM Support
511
660
 
512
661
  The HTMLInclude handler supports declarative shadow DOM attachment using the `shadowrootmodeonload` attribute. This allows you to attach cloned content directly to a parent element's shadow root, similar to the platform's [declarative shadow DOM](https://web.dev/articles/declarative-shadow-dom) feature.
@@ -857,9 +1006,68 @@ The handler provides helpful error messages:
857
1006
 
858
1007
  - Uses WeakMap caching for repeated ID lookups
859
1008
  - Efficient for scenarios like periodic tables with many repeated elements
860
- - Searches across shadow boundaries using `upShadowSearch`
1009
+ - Searches across shadow boundaries using `upShadowSearch` (registry-aware)
1010
+ - Respects scoped custom element registry boundaries
861
1011
  - Cleans up cache entries when elements are garbage collected
862
1012
 
1013
+ **MOSE Export Optimization:**
1014
+
1015
+ When cloning live DOM elements (not templates) that contain Mount Observer Script Elements (MOSEs) across shadow DOM boundaries, the HTMLInclude handler automatically copies the parsed `export` property from source scripts to cloned scripts. This optimization avoids re-parsing JSON when the same MOSE configuration is reused in multiple shadow roots.
1016
+
1017
+ **How it works:**
1018
+ 1. Detects when cloning a live element (not a template) from a different root node
1019
+ 2. Finds all `script[type="mountobserver"]` elements in both source and clone
1020
+ 3. Matches scripts by their `id` attribute
1021
+ 4. Copies the `export` property from source to clone (by reference)
1022
+ 5. If source script hasn't been processed yet, waits for the `resolved` event
1023
+
1024
+ **Example:**
1025
+
1026
+ ```html
1027
+ <!-- Source element with MOSE in light DOM -->
1028
+ <div id="observer-config">
1029
+ <script type="mountobserver" id="my-config">
1030
+ {
1031
+ "matching": ".interactive",
1032
+ "import": "./interactive.js",
1033
+ "do": "builtIns.enhanceMountedElement"
1034
+ }
1035
+ </script>
1036
+ <div class="interactive">Content</div>
1037
+ </div>
1038
+
1039
+ <!-- Clone into shadow DOM -->
1040
+ <my-component>
1041
+ #shadow
1042
+ <template src="#observer-config"></template>
1043
+ </my-component>
1044
+
1045
+ <script type="module">
1046
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1047
+
1048
+ // Process MOSEs in light DOM
1049
+ new MountObserver({
1050
+ do: 'builtIns.mountObserverScript'
1051
+ }).observe(document.body);
1052
+
1053
+ // Clone into shadow roots
1054
+ new MountObserver({
1055
+ do: 'builtIns.HTMLInclude'
1056
+ }).observe(document);
1057
+ </script>
1058
+ ```
1059
+
1060
+ **Benefits:**
1061
+ - **Performance**: JSON is parsed only once, not for each clone
1062
+ - **Memory efficiency**: Cloned scripts share the same export object
1063
+ - **Consistency**: All clones use identical configuration
1064
+ - **Automatic**: No manual intervention required
1065
+
1066
+ **Requirements:**
1067
+ - Source and clone must be in different root nodes (document vs shadow root)
1068
+ - MOSE scripts must have `id` attributes for matching
1069
+ - Source script must be processed by `builtIns.mountObserverScript` or `builtIns.scriptExport` before cloning
1070
+
863
1071
  [Implemented as MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes requirement](requirements/Done/MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes.md)
864
1072
 
865
1073
  ## Automatic ID Generation with genIds
@@ -873,18 +1081,23 @@ The `builtIns.generateIds` handler automatically generates unique IDs for elemen
873
1081
  - Automatically updates ID references in attributes (aria-labelledby, for, etc.)
874
1082
  - Provides shorthand syntax for common patterns
875
1083
  - Handles deferred attribute activation
1084
+ - Removes `disabled` from fieldsets after processing
876
1085
 
877
1086
  **Basic usage:**
878
1087
 
879
1088
  ```html
880
1089
  <fieldset disabled>
881
- <label defer-for="for: #{{username}}">Username:</label>
882
- <input data-id="username" type="text">
1090
+ <label>
1091
+ LHS: <input data-id={{lhs}}>
1092
+ </label>
883
1093
 
884
- <label defer-for="for: #{{password}}">Password:</label>
885
- <input data-id="password" type="password">
1094
+ <label for=rhs>
1095
+ RHS: <input data-id={{rhs}}>
1096
+ </label>
886
1097
 
887
- <button -id>Generate IDs</button>
1098
+ <template -id defer-🎚️ 🎚️='on if isEqual, based on #{{lhs}} and #{{rhs}}.'>
1099
+ <div>LHS === RHS</div>
1100
+ </template>
888
1101
  </fieldset>
889
1102
 
890
1103
  <script type="module">
@@ -901,23 +1114,41 @@ The `builtIns.generateIds` handler automatically generates unique IDs for elemen
901
1114
 
902
1115
  1. The handler watches for elements with the `-id` attribute (the trigger)
903
1116
  2. Finds the nearest scope container (fieldset, [itemscope], or root)
904
- 3. Generates unique IDs for elements with `data-id`, `#`, `@`, or `|` attributes
1117
+ 3. Generates unique IDs for elements with `data-id={{name}}`, `#`, `@`, or `|` attributes
905
1118
  4. Replaces `#{{name}}` references with generated IDs in attributes
906
1119
  5. Removes `-id` and `defer-*` attributes after processing
907
1120
  6. Removes `disabled` from fieldset containers
908
1121
 
909
- **Shorthand attributes:**
1122
+ **Result:**
910
1123
 
911
1124
  ```html
912
1125
  <fieldset>
1126
+ <label>
1127
+ LHS: <input id=gid-0 data-id=lhs>
1128
+ </label>
1129
+
1130
+ <label for=rhs>
1131
+ RHS: <input id=gid-1 data-id=rhs>
1132
+ </label>
1133
+
1134
+ <template 🎚️='on if isEqual, based on #gid-0 and #gid-1.'>
1135
+ <div>LHS === RHS</div>
1136
+ </template>
1137
+ </fieldset>
1138
+ ```
1139
+
1140
+ **Shorthand attributes:**
1141
+
1142
+ ```html
1143
+ <fieldset disabled>
913
1144
  <!-- # uses element's tag name -->
914
- <input # type="text"> <!-- becomes data-id="input" -->
1145
+ <my-element #></my-element> <!-- becomes id=gid-0 data-id=my-element -->
915
1146
 
916
1147
  <!-- @ uses element's name attribute -->
917
- <input @ name="email" type="email"> <!-- becomes data-id="email" -->
1148
+ <input @ name="email" type="email"> <!-- becomes id=gid-1 data-id=email -->
918
1149
 
919
1150
  <!-- | uses element's itemprop attribute -->
920
- <span | itemprop="price">$99</span> <!-- becomes data-id="price" -->
1151
+ <span | itemprop="price">$99</span> <!-- becomes id=gid-2 data-id=price -->
921
1152
 
922
1153
  <button -id>Generate IDs</button>
923
1154
  </fieldset>
@@ -928,31 +1159,55 @@ The `builtIns.generateIds` handler automatically generates unique IDs for elemen
928
1159
  The `data-id` attribute supports special symbols that trigger side effects:
929
1160
 
930
1161
  ```html
931
- <fieldset>
932
- <!-- @ sets name attribute -->
933
- <input data-id="@ username" type="text">
934
- <!-- Result: id="gid-0" name="username" data-id="username" -->
935
-
936
- <!-- | sets itemprop attribute -->
937
- <span data-id="| price">$99</span>
938
- <!-- Result: id="gid-1" itemprop="price" data-id="price" -->
939
-
940
- <!-- $ sets itemscope and itemprop -->
941
- <div data-id="$ product">...</div>
942
- <!-- Result: id="gid-2" itemscope itemprop="product" data-id="product" -->
943
-
944
- <!-- . adds to class attribute -->
945
- <div data-id=". highlight">Content</div>
946
- <!-- Result: id="gid-3" class="highlight" data-id="highlight" -->
947
-
948
- <!-- % adds to part attribute -->
949
- <div data-id="% header">Header</div>
950
- <!-- Result: id="gid-4" part="header" data-id="header" -->
951
-
952
- <button -id>Generate IDs</button>
953
- </fieldset>
1162
+ <form>
1163
+ <fieldset disabled>
1164
+ <label>
1165
+ LHS: <input data-id="{{@. lhs}}">
1166
+ </label>
1167
+
1168
+ <label for=rhs>
1169
+ RHS: <span contenteditable data-id="{{|.% rhs}}">
1170
+ </label>
1171
+
1172
+ <template -id defer-🎚️ 🎚️='on if isEqual, based on #{{lhs}} and #{{rhs}}.'>
1173
+ <div>LHS === RHS</div>
1174
+ </template>
1175
+ </fieldset>
1176
+ </form>
1177
+ ```
1178
+
1179
+ **Result:**
1180
+
1181
+ ```html
1182
+ <form>
1183
+ <fieldset>
1184
+ <label>
1185
+ LHS: <input name=lhs class=lhs id=gid-0 data-id=lhs>
1186
+ </label>
1187
+
1188
+ <label for=rhs>
1189
+ RHS: <span contenteditable itemprop=rhs class=rhs part=rhs id=gid-1 data-id=rhs>
1190
+ </label>
1191
+
1192
+ <template 🎚️='on if isEqual, based on #gid-0 and #gid-1.'>
1193
+ <div>LHS === RHS</div>
1194
+ </template>
1195
+ </fieldset>
1196
+ </form>
954
1197
  ```
955
1198
 
1199
+ **Symbol meanings:**
1200
+
1201
+ | Symbol | Attribute | Meaning |
1202
+ |--------|-------------------|------------------------------------------------------------------------------|
1203
+ | @ | name | Second letter of name, common in social media for selecting names |
1204
+ | \| | itemprop | "Pipe" resembles itemprop, half of dollar sign, looks like an I |
1205
+ | $ | itemscope+itemprop| Combination of S for Scope and Pipe |
1206
+ | % | part | Starts with p, percent indicates proportion |
1207
+ | . | class | CSS selector |
1208
+
1209
+ Multiple symbols can be combined: `data-id="{{@.% myName}}"` adds name, class, and part attributes.
1210
+
956
1211
  **Deferred attributes:**
957
1212
 
958
1213
  Use `defer-*` prefix to prevent attributes from being applied until IDs are generated:
@@ -961,8 +1216,7 @@ Use `defer-*` prefix to prevent attributes from being applied until IDs are gene
961
1216
  <fieldset disabled>
962
1217
  <!-- These attributes won't work until IDs are generated -->
963
1218
  <label defer-for="for: #{{email}}">Email:</label>
964
- <input data-id="email" type="email" defer-aria-describedby="aria-describedby: #{{emailHelp}}">
965
- <span data-id="emailHelp">Enter your email address</span>
1219
+ <input data-id={{email}} type="email">
966
1220
 
967
1221
  <button -id>Activate Form</button>
968
1222
  </fieldset>
@@ -1352,6 +1606,87 @@ const observer = new MountObserver({
1352
1606
 
1353
1607
  [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
1354
1608
 
1609
+ ## Waiting for Custom Element Definitions
1610
+
1611
+ The `whenDefined` property allows you to wait for custom elements to be defined before mounting elements. This ensures that elements are only processed after their custom element definitions are available, preventing issues with undefined custom elements.
1612
+
1613
+ ```javascript
1614
+ const observer = new MountObserver({
1615
+ matching: 'my-element',
1616
+ whenDefined: 'my-element', // Wait for my-element to be defined
1617
+ do: (element) => {
1618
+ console.log('my-element is now defined and mounted');
1619
+ }
1620
+ });
1621
+ observer.observe(document);
1622
+
1623
+ // Later, when the custom element is defined
1624
+ customElements.define('my-element', class extends HTMLElement {
1625
+ // ...
1626
+ });
1627
+ ```
1628
+
1629
+ **Waiting for multiple custom elements:**
1630
+
1631
+ You can specify an array of tag names to wait for all of them to be defined:
1632
+
1633
+ ```javascript
1634
+ const observer = new MountObserver({
1635
+ matching: 'my-element, another-element',
1636
+ whenDefined: ['my-element', 'another-element'], // Wait for both
1637
+ do: (element) => {
1638
+ console.log('Both elements are defined, mounting:', element.localName);
1639
+ }
1640
+ });
1641
+ ```
1642
+
1643
+ **How it works:**
1644
+
1645
+ 1. The check happens first, before any other `where*` conditions
1646
+ 2. Uses `customElements.whenDefined()` for each specified tag name
1647
+ 3. Uses the `customElementRegistry` of the observed root node
1648
+ 4. Only checks once per observer instance (doesn't re-check on subsequent mounts)
1649
+ 5. The `observe()` method waits for all definitions before processing elements
1650
+
1651
+ **Common use cases:**
1652
+
1653
+ ```javascript
1654
+ // Lazy load and wait for custom element definition
1655
+ const observer = new MountObserver({
1656
+ matching: 'my-button',
1657
+ whenDefined: 'my-button',
1658
+ import: './my-button.js',
1659
+ do: 'builtIns.defineCustomElement'
1660
+ });
1661
+
1662
+ // Wait for dependencies before enhancing
1663
+ const observer = new MountObserver({
1664
+ matching: '.needs-custom-elements',
1665
+ whenDefined: ['base-element', 'helper-element'],
1666
+ do: (element) => {
1667
+ // Safe to interact with custom elements now
1668
+ const base = element.querySelector('base-element');
1669
+ const helper = element.querySelector('helper-element');
1670
+ }
1671
+ });
1672
+ ```
1673
+
1674
+ **AND condition logic:**
1675
+
1676
+ Like all `where*` properties, `whenDefined` forms an AND condition with other filters. However, it's checked first as a prerequisite before evaluating other conditions:
1677
+
1678
+ ```javascript
1679
+ const observer = new MountObserver({
1680
+ matching: 'my-element',
1681
+ whenDefined: 'my-element', // Checked FIRST
1682
+ whereInstanceOf: HTMLElement, // Then checked
1683
+ whereLocalNameMatches: /^my-/, // Then checked
1684
+ do: (element) => { /* ... */ }
1685
+ });
1686
+ ```
1687
+
1688
+ [whenDefined implemented as [SupportForWhenDefined](requirements/SupportForWhenDefined.md)]
1689
+
1355
1690
  ## LocalName Pattern Matching
1356
1691
 
1357
1692
  The `whereLocalNameMatches` property allows filtering elements by their `localName` using regular expressions. This is useful when you need to match elements based on naming patterns rather than CSS selectors.
@@ -1492,6 +1827,8 @@ However, where this support for "whereInstanceOf" would be *most* helpful is whe
1492
1827
 
1493
1828
  MountObserver automatically respects scoped custom element registry boundaries. When observing a root node, only elements that share the same `customElementRegistry` as the root node will be mounted by default. This is an implicit AND condition that applies to all observations.
1494
1829
 
1830
+ Learn more about [scoped custom element registries in Chrome's blog post](https://developer.chrome.com/blog/scoped-registries).
1831
+
1495
1832
  **How it works:**
1496
1833
 
1497
1834
  ```javascript
@@ -1548,7 +1885,7 @@ This ensures that when we observe a shadow root with a scoped registry, we won't
1548
1885
 
1549
1886
  ## Element Mount Extension
1550
1887
 
1551
- For even more convenience, we can use the `element.mount()` method to observe elements within their scoped custom element registry context. This is particularly useful with scoped custom element registries (Chrome 146+, latest WebKit/Safari).
1888
+ For even more convenience, we can use the `element.mount()` method to observe elements within their scoped custom element registry context. This is particularly useful with [scoped custom element registries](https://developer.chrome.com/blog/scoped-registries) (Chrome 146+, latest WebKit/Safari).
1552
1889
 
1553
1890
  ```JavaScript
1554
1891
  import 'mount-observer/ElementMountExtension.js';
@@ -1605,13 +1942,13 @@ class MyComponent extends HTMLElement {
1605
1942
  }
1606
1943
  ```
1607
1944
 
1608
- Browser support: Works in all browsers, but scoped registry features require Chrome 146+ or latest WebKit/Safari.
1945
+ Browser support: Works in all browsers, but [scoped registry features](https://developer.chrome.com/blog/scoped-registries) require Chrome 146+ or latest WebKit/Safari.
1609
1946
 
1610
1947
  [Implemented as CustomElementRegistryMounting requirement](requirements/Done/CustomElementRegistryMounting.md).
1611
1948
 
1612
1949
  ### Global Propagation with `mountGlobally()`
1613
1950
 
1614
- The `mountGlobally()` method extends `mount()` to automatically propagate mount observers across custom element registry boundaries and shadow DOM scopes. This is useful for bootstrapping core handlers that should work everywhere, regardless of scoped registries. It should be used sparingly, as a last resort, probably limited to things that should arguably be built into the platform.
1951
+ The `mountGlobally()` method extends `mount()` to automatically propagate mount observers across [custom element registry](https://developer.chrome.com/blog/scoped-registries) boundaries and shadow DOM scopes. This is useful for bootstrapping core handlers that should work everywhere, regardless of scoped registries. It should be used sparingly, as a last resort, probably limited to things that should arguably be built into the platform.
1615
1952
 
1616
1953
  ```JavaScript
1617
1954
  import 'mount-observer/ElementMountExtension.js';