mount-observer
v0.1.31
Published
Observe and act on css matches.
Readme
Implementation Status
The following features have been implemented and tested:
Core Functionality
- ✅ matching: CSS selector-based element matching
- ✅ whenDefined: Wait for custom elements to be defined before mounting
- ✅ whereInstanceOf: Constructor-based element filtering (single or array)
- ✅ whereLocalNameMatches: Regular expression-based localName filtering
- ✅ shouldMount: Custom JavaScript check for complex mounting conditions
- ✅ Registry matching: Automatic filtering by customElementRegistry (Chrome 146+)
- ✅ withMediaMatching: Media query-based conditional mounting (string or MediaQueryList)
- ✅ whereObservedRootSizeMatches: Container query-based conditional mounting (observes root element size)
- ✅ whereElementIntersectsWith: Intersection observer-based conditional mounting (observes element visibility)
- ✅ whereConnectionHas: Network connection-based conditional mounting (observes connection speed/type)
- ✅ withScopePerimeter: Donut hole scoping (exclude elements inside matching ancestors)
Lifecycle & Events
- ✅ mount/dismount/disconnect events: Element lifecycle tracking
- ✅ mediamatch/mediaunmatch events: Media query state change notifications (with
getPlayByPlayoption) - ✅ load event: Import completion notification
Advanced Features
- ✅ Dynamic imports: Lazy loading of JavaScript modules
- ✅ assignOnMount: Property assignment when elements mount
- ✅ assignOnDismount: Property assignment when elements dismount
- ✅ stageOnMount: Reversible property assignment (auto-restores on dismount)
- ✅ do callbacks: Mount/dismount/disconnect/reconnect lifecycle hooks
- ✅ with property: Hierarchical observer composition with sub-observers
- ✅ Element mount extension: element.mount() method for scoped registry observation
- ✅ Shared MutationObserver: Efficient observer sharing across instances
- ✅ Code splitting: Conditional features loaded on-demand
- ✅ Memory management: WeakRef usage for DOM node references
Not Yet Implemented
- ❌ Reconnect event handling
The MountObserver API
Author: Bruce B. Anderson (with valuable feedback from @doeixd)
Issues / PRs / polyfill: mount-observer
Last Update: Feb 23, 2026
Benefits of this API
What follows is a far more ambitious alternative to the lazy custom element proposal. The goals of the MountObserver API are more encompassing and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one API. It basically maps common filtering conditions in the DOM to mounting a "campaign" of some sort, like importing a resource, and/or progressively enhancing an element, and/or "binding from a distance".
"Binding from a distance" refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc, to the HTML as it streams in. Libraries that take this approach include Corset and trans-render, selector-observer, pure, weld, bess. The concept has been promoted by a number of prominent voices in the community.
The underlying theme is that this API is meant to make it easy for developers to do the right thing by encouraging lazy loading and smaller footprints. It rolls up most of the other observer APIs into one, including, potentially, a selector observer, which may be a similar duplicate to the match-media counterpart proposal.
Finite Element Analysis
Most every web application can be recursively broken down into logical regions, building blocks which are assembled together to form the whole site.
At the most micro level, utilizing highly reusable, generic custom elements -- elements that can extend the HTML vocabulary, elements that could be incorporated into the browser, even -- form a great foundation to build on.
But as one zooms out from the micro to the macro, the nature of the components changes in significant ways.
At the micro level, components will have few, if any, dependencies, and those dependencies will tend to be quite stable, and likely all be used. The dependencies will skew more towards tightly coupled utility libraries.
"Macro" level components will tend to be heavy on business-domain specific data, heavy on gluing / orchestrating smaller components, light on difficult, esoteric JavaScript. They aren't confined to static JS files, and likely will include dynamic content as well. They will also be heavy on conditional sections of the application only loading if requested by the user.
ES module based web components may or may not be the best fit for these application macro "modules". A better fit might be a server-centric solution, like Rails, just to take an example.
A significant pain point has to do with downloading all the third-party web components and/or (progressive) enhancements that these macro components / compositions require, and loading them into memory only when needed.
Does this API make the impossible possible?
There is quite a bit of functionality this proposal would open up that is exceedingly difficult to polyfill reliably:
It is unclear how to use mutation observers to observe changes to custom state. The closest thing might be a solution like this, but that falls short for elements that aren't visible or during template instantiation, and requires carefully constructed "negating" queries if needing to know when the CSS selector is no longer matching.
For simple CSS matches, like "my-element" or "[name='hello']", it is enough to use a mutation observer and only observe the elements within the specified DOM region (more on that below). But as CSS has evolved, it is quite easy to think of numerous CSS selectors that would require us to expand our mutation observer to scan the entire Shadow DOM realm, or the entire DOM tree outside any Shadow DOM, for any and all mutations (including attribute changes), and re-evaluate every single element within the specified DOM region for new matches or old matches that no longer match. Things like child selectors, :has, and so on. All this is done miraculously by the browser in a performant way. Reproducing this in userland using JavaScript alone while matching the same performance seems impossible.
Knowing when an element previously being monitored passes totally "out-of-scope" so that no more hard references to the element remain. This would allow for cleanup of no longer needed weak references without requiring polling.
Some CSS selectors, such as the donut hole scope range, aren't supported by oEl.querySelectorAll(...) or oEl.matches(...).
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.
Most significant use cases
The amount of code necessary to accomplish these common tasks designed to improve the user experience is significant. Building it into the platform would potentially:
- Give developers a strong signal to do the right thing by:
- Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
- Supporting "binding from a distance" that can set property values of elements in bulk as the HTML streams in. For example, say a web page is streaming in HTML with thousands of input elements (say a long tax form). We want to have some indication in the head tag of the HTML (for example) to make all the input elements read-only as they stream through the page. With CSS, we could do similar things, for example set the background to red of all input elements. Why can't we do something similar with setting properties like readOnly, disabled, etc? With this API, giving developers the "keys" to CSS filtering so they can "mount a campaign" to apply common settings on them all feels like something that almost every web developer has mentally screamed to themselves "why can't I do that?", doesn't it?
- Supporting "progressive enhancement" more effectively.
- Potentially allow the platform to do more work in low-level (C/C++/Rust?) code without as much context switching into the JavaScript memory space, which may reduce CPU cycles as well. This is done by passing a substantial number of conditions into the API, which can all be evaluated at a lower level before the API needs to surface up to the developer "found one!".
- As discussed earlier, to do the job right, polyfills really need to reexamine all the elements within the observed node for matches anytime any element within the Shadow Root so much as sneezes (has an attribute modified, changes custom state, etc), due to modern selectors such as the :has selector. Surely the platform has found ways to do this more efficiently?
The extra flexibility this new primitive would provide could be quite useful to things other than lazy loading of custom elements, such as implementing custom enhancements as well as binding from a distance in userland.
Quick Examples of the Most Common Use Cases
Before getting into the weeds, let's demonstrate a few of the most prominent use cases:
Use Case 1: Custom Attribute Enhancement
<body>
<div log-to-console="clicked on a div">hello</div>
<script type=module>
document.mount({
matching: '[log-to-console]',
do: (el) => {
el.addEventListener('click', e => {
console.log(e.target.getAttribute('log-to-console'));
});
}
})
</script>
</body>Use Case 2: Lazy Global Custom Element Definition
To specify the equivalent of what the alternative proposal linked to above would do, we can do the following:
// MyElement.js
export default class MyElement extends HTMLElement {
connectedCallback() {
this.textContent = 'Hello!';
}
}
// main.js
import 'mount-observer/ElementMountExtension.js';
document.mount({
matching: 'my-element',
import: './MyElement.js',
do: 'builtIns.defineCustomElement'
});
// HTML - elements will be upgraded when discovered
// by the mount observer
<my-element></my-element>
This registers custom elements with the global customElements registry.
See this extending package that provides for a more declarative approach.
Scoped
To register the class in the same custom element registry as the element which calls the "mount" method (element in this case), use "builtIns.defineScopedCustomElement":
element.mount({
matching: 'my-element',
import: './MyElement.js',
do: 'builtIns.defineScopedCustomElement'
});Enhancing Elements with assign-gingerly
The builtIns.enhanceMountedElement handler automatically enhances mounted elements using the assign-gingerly enhancement system. This allows us to attach behavior and state to elements without subclassing.
// MyEnhancement.js
class ButtonEnhancement {
constructor(element, ctx, initVals) {
this.element = new WeakRef(element);
this.ctx = ctx;
this.clickCount = 0;
element.addEventListener('click', ({target}) => {
this.clickCount++;
target.setAttribute('data-clicks', this.clickCount);
});
}
}
export default {
spawn: ButtonEnhancement,
enhKey: 'buttonEnh'
};
// main.js
document.mount({
matching: '.enhance-me',
import: './MyEnhancement.js',
do: 'builtIns.enhanceMountedElement'
});
// HTML
<button class="enhance-me">Click me</button>
// Access the enhancement
const button = document.querySelector('.enhance-me');
console.log(button.enh.buttonEnh.clickCount); // 0
button.click();
console.log(button.enh.buttonEnh.clickCount); // 1The handler:
- Searches the imported module for an export with a
spawnproperty (the enhancement class), starting with default. - Calls
element.enh.get(registryItem, context)to spawn the enhancement - Stores the enhancement instance on
element.enh[enhKey]if anenhKeyis provided
Exposing Module Exports from Script Elements
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.
Problem 1: ES Module Export Access
The browser doesn't expose module exports from <script type="module"> elements. There's been a decade-old proposal to add this, but it remains unimplemented.
The Solution:
<!-- Use nomodule to prevent browser from loading it separately -->
<script nomodule src="./config.js" id="myConfig"></script>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
const observer = new MountObserver({
do: 'builtIns.scriptExport'
});
observer.observe(document);
// Access the module's exports via element.export
const config = document.getElementById('myConfig').export;
console.log(config.apiKey);
console.log(config.endpoints);
</script>Why nomodule?
- Prevents the browser from loading the module separately
- Avoids having the module loaded twice in memory
- The handler imports it once and exposes exports via
element.export
Problem 2: Declarative JSON Import
Importing JSON typically requires fetch() or dynamic import() with assertions. This handler provides a declarative alternative.
The Solution:
<!-- Load JSON data -->
<script src="./data.json" type="json" id="myData"></script>
<!-- Also supports full MIME types -->
<script src="./config.json" type="application/json" id="config"></script>
<script src="./linked-data.json" type="application/ld+json" id="linkedData"></script>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
const observer = new MountObserver({
do: 'builtIns.scriptExport'
});
observer.observe(document);
// Access the JSON data via element.export.default
const data = document.getElementById('myData').export.default;
console.log(data.items);
console.log(data.config);
</script>Why no nomodule for JSON?
- The browser ignores script elements with non-standard type attributes
- No risk of double-loading since the browser won't load it at all
- The handler imports it with the appropriate JSON assertion
Supported JSON types:
type="json"- Simple and cleantype="application/json"- Standard MIME typetype="application/ld+json"- JSON-LD linked data- Any type containing "json" triggers JSON import assertion
How it works:
- Matches
script[src]elements (via static properties) - Skips
type="module"scripts (browser-handled) - Processes scripts with
nomoduleattribute OR type containing "json" - Resolves the
srcrelative to the document - Imports with appropriate assertion (JSON if type contains "json")
- Stores the imported module on
element.export - Dispatches a
resolvedevent with the imported module
Reusing imported modules:
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:
<script src="./data.json" type="json" id="myData"></script>
<script type="module">
const dataScript = document.getElementById('myData');
// Listen for the resolved event
dataScript.addEventListener('resolved', (e) => {
console.log('Data loaded:', e.export);
// e.export contains the imported module
// For JSON: e.export.default contains the data
});
// Or access directly after processing
// dataScript.export will contain the imported module
</script>This is particularly useful when multiple components need to access the same data or configuration without triggering multiple imports.
Benefits:
- Access ES module exports from script elements (finally!)
- Declarative JSON loading without fetch
- Prevents double-loading of modules
- Clean, intuitive syntax
- Works with relative and absolute URLs
- No need to specify
matchingorwhereInstanceOf(handler provides defaults)
Use cases:
- Accessing configuration module exports in HTML
- Loading JSON data declaratively
- Progressive enhancement with module loading
- Declarative dependency management
- Loading JSON-LD structured data
Mount Observer Script Elements (MOSEs)
Inspired by the 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.
<!-- Inline JSON configuration -->
<script type="mountobserver">
{
"matching": "my-fancy-button",
"import": "./fancy-button.js",
"do": "builtIns.defineCustomElement"
}
</script>
<!-- External JSON configuration -->
<script type="mountobserver" src="./observer-config.json"></script>
<!-- Bootstrap the handler -->
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
// Handler provides matching and whereInstanceOf via static properties
const observer = new MountObserver({
do: 'builtIns.mountObserverScript'
});
observer.observe(document);
</script>How it works:
- The handler matches
script[type="mountobserver"]elements (via static properties) - If the script has a
srcattribute, imports JSON from that URL - Otherwise, parses the script's textContent as JSON
- Supports both single config objects and arrays of configs
- Stores the parsed config on
scriptElement.exportfor reuse - Dispatches a
resolvedevent with the parsed config - Calls
scriptElement.mount(config)for each configuration - The
mount()method creates a MountObserver for that configuration
Reusing parsed configurations:
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:
<script type="mountobserver" id="myConfig">
{
"matching": ".my-element",
"do": "builtIns.logToConsole"
}
</script>
<script type="module">
const configScript = document.getElementById('myConfig');
// Listen for the resolved event (fires only once on first parse)
configScript.addEventListener('resolved', (e) => {
console.log('Config loaded:', e.export);
// e.export contains the parsed configuration
});
// Or access directly after processing
// configScript.export will contain the parsed config
// If observed again, the handler will reuse configScript.export
// without re-parsing or firing the resolved event
</script>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.
Multiple configs in one script:
You can define multiple mount observer configurations in a single script element using a JSON array:
<script type="mountobserver">
[
{
"do": "builtIns.hoistTemplate"
},
{
"do": "builtIns.HTMLInclude"
},
{
"matching": "my-button",
"import": "./my-button.js",
"do": "builtIns.defineCustomElement"
}
]
</script>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.
Benefits:
- Zero JavaScript required for observer configuration
- Configurations are pure JSON (fully serializable)
- Easy to generate server-side or from build tools
- Supports both inline and external configurations
- Leverages the
element.mount()API for automatic scope management - No need to specify
matchingorwhereInstanceOffor the handler itself
Use cases:
- Server-side rendering with progressive enhancement
- Build-time generation of observer configurations
- CMS-driven component loading
- Declarative micro-frontend architecture
- Configuration management without JavaScript bundling
Example with multiple configurations:
<!-- Load custom elements -->
<script type="mountobserver">
{
"matching": "my-button",
"import": "./components/my-button.js",
"do": "builtIns.defineCustomElement"
}
</script>
<!-- Enhance existing elements -->
<script type="mountobserver">
{
"matching": ".interactive",
"import": "./enhancements/interactive.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
<!-- Single bootstrap script activates all configurations -->
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
new MountObserver({
do: 'builtIns.mountObserverScript'
}).observe(document);
</script>Hoisting Templates for Performance
The builtIns.hoistTemplate handler optimizes template usage by moving a template element's content from shadow roots to document.head.
Why hoist templates?
Template hoisting is particularly useful when you need to share conditional or repeated templates across multiple custom element instances.
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:
- Reduces memory usage (one template instead of many copies)
- Improves cloning performance (single source of truth)
- Maintains the same API through the
remoteContentgetter
Basic usage:
<my-web-component>
#shadow
<template id="my-template">
<div>My content</div>
</template>
</my-web-component>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
const observer = new MountObserver({
do: 'builtIns.hoistTemplate'
});
observer.observe(document);
</script>What happens:
- The handler finds templates with IDs in shadow roots
- Moves the template content to a new template in
<head> - Updates the original template with
src="#mount-observer-0"(unique ID) - Defines a
remoteContentgetter that returns the hoisted template's content
Accessing hoisted content:
const template = shadowRoot.querySelector('#my-template');
// After hoisting, use remoteContent to access the content
const content = template.remoteContent; // Returns DocumentFragment
const clone = content.cloneNode(true); // Clone the contentThe handler automatically hoists templates that:
- Have an
idattribute - Don't already have a
srcattribute - Are in a shadow root (or disconnected, being cloned)
- Have content (empty templates are skipped)
Declarative usage with MOSE:
<script type="mountobserver">
{
"do": "builtIns.hoistTemplate"
}
</script>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
new MountObserver({
do: 'builtIns.mountObserverScript'
}).observe(document);
</script>Implemented as HoistingTemplates requirement
Element Mount Configuration (EMC) Scripts
The builtIns.emcScript handler provides declarative element enhancement using <script type="emc"> elements. EMC scripts combine mount observation with the assign-gingerly enhancement system to apply behaviors, properties, and classes to elements as they mount.
Why use EMC scripts?
- Declaratively enhance elements without writing JavaScript
- Lazy load enhancement classes only when needed
- Automatically register and spawn enhancements
- Works with scoped custom element registries
- Supports attribute-based element matching
- Reuses enhancement definitions across multiple elements
Basic usage:
<!-- Define enhancement configuration -->
<script type="emc">
{
"matching": ".interactive",
"enhConfig": {
"spawn": "./my-enhancement.js",
"enhKey": "myEnhancement"
}
}
</script>
<!-- Elements matching the selector get enhanced -->
<div class="interactive">This will be enhanced</div>
<div class="interactive">This too</div>External JSON configuration:
<!-- Load configuration from external file -->
<script type="emc" src="./enh-config.json"></script>With attribute matching:
<script type="emc">
{
"matching": "button",
"enhConfig": {
"spawn": "./button-enhancement.js",
"enhKey": "fancyButton",
"withAttrs": {
"base": "variant"
}
}
}
</script>
<!-- Only buttons with variant="primary" get enhanced -->
<button variant="primary">Enhanced</button>
<button>Not enhanced</button>How it works:
- EMC script is parsed (inline JSON or external via
src) - Configuration is stored on
scriptElement.export resolvedevent is dispatched- Script ID is auto-generated as
${parentElement.localName}.${enhKey}if not specified - MountObserver watches for elements matching the configuration
- When element mounts:
- Checks if already enhanced (via
element.enh[enhKey]) - Registers enhancement class if not already registered
- Spawns enhancement instance via
element.enh.get(enhancementConfig)
- Checks if already enhanced (via
Enhancement class example:
// my-enhancement.js
export default class MyEnhancement {
constructor(element, ctx, initVals) {
this.element = element;
// Apply enhancement
this.element.classList.add('enhanced');
}
dispose() {
// Cleanup
this.element.classList.remove('enhanced');
}
}Requirements:
- Must import
ElementMountExtension.jsto enableelement.enhproperty - Must import
assign-gingerly/object-extension.jsfor enhancement registry - Enhancement classes should be constructors that accept
(element, ctx, initVals)
Implemented as EMCScript requirement
Syndicating Mount Observers with Synthesizer
The Synthesizer abstract base class enables automatic propagation of mount observer configurations across shadow DOM boundaries. It acts as a "syndicator-subscriber" pattern where a syndicator in the document root broadcasts script elements to subscribers in shadow roots.
Why use Synthesizer?
- Automatically share mount observer configurations across shadow roots
- Eliminates manual observer setup in each shadow root
- Ensures consistent behavior across component boundaries
- Works with both MOSE and EMC script elements
- Provides a declarative, inheritance-based approach
How it works:
- Syndicator (in document root): Watches for
script[type="mountobserver"]andscript[type="emc"]elements and broadcasts them to subscribers - Subscriber (in shadow roots): Receives and clones script elements from the syndicator
- Automatic activation: Both syndicator and subscriber activate 7 built-in handlers in their respective root nodes
Basic usage:
<!-- Define your Synthesizer custom element -->
<script type="module">
import { Synthesizer } from 'mount-observer/Synthesizer.js';
class AppSynthesizer extends Synthesizer {}
customElements.define('app-synthesizer', AppSynthesizer);
</script>
<!-- Syndicator in document root with mount observer scripts -->
<app-synthesizer>
<script type="mountobserver">
{
"matching": "button.primary",
"import": "./primary-button.js",
"do": "builtIns.defineCustomElement"
}
</script>
<script type="emc">
{
"matching": ".interactive",
"enhConfig": {
"spawn": "./interactive.js",
"enhKey": "interactive"
}
}
</script>
</app-synthesizer>
<!-- Component with shadow root -->
<my-component>
#shadow
<!-- Subscriber automatically receives scripts from syndicator -->
<app-synthesizer></app-synthesizer>
<!-- These elements will be enhanced by the syndicated observers -->
<button class="primary">Click me</button>
<div class="interactive">Interactive content</div>
</my-component>What happens:
- The syndicator (
<app-synthesizer>in document root) activates 7 built-in handlers:builtIns.mountObserverScript- Process MOSE scriptsbuiltIns.scriptExport- Expose module exports from script elementsbuiltIns.HTMLInclude- Enable intra-document HTML includesbuiltIns.hoistTemplate- Optimize template usagebuiltIns.generateIds- Auto-generate unique IDsbuiltIns.emcParserScript- Load parsers for EMC scriptsbuiltIns.emcScript- Process EMC scripts
These handlers form the core infrastructure for declarative progressive enhancement:
- mountObserverScript: Enables fully declarative mount observer configuration via
<script type="mountobserver">elements - scriptExport: Makes ES module exports accessible from script elements, enabling configuration sharing
- HTMLInclude: Allows template reuse and inheritance patterns with
<template src="#id"> - hoistTemplate: Optimizes memory usage by centralizing template content
- generateIds: Automates ID generation for forms and accessibility features
- emcParserScript: Enables lazy-loading of complex parsers for enhancement attributes
- emcScript: Processes enhancement configurations for progressive enhancement
Together, these handlers enable a complete HTML-first development workflow where behaviors, enhancements, and configurations can be declared in HTML and automatically propagated across shadow DOM boundaries.
The syndicator watches for script elements being added to its light children
When a script is added, it waits for the
resolvedevent (ensuring the script is parsed)The syndicator dispatches an
AddedScriptElementEventwith the script elementSubscribers in shadow roots:
- Find the syndicator in the document root (matching localName)
- Process existing scripts from the syndicator
- Subscribe to
addedscriptelementevents for new scripts - Clone each script element and copy its
exportproperty - Append cloned scripts to their own light children
- Activate the same 7 built-in handlers in their shadow root
Syndicator vs Subscriber:
The Synthesizer automatically determines its role based on its root node:
- Document root → Acts as syndicator (broadcasts scripts)
- Shadow root → Acts as subscriber (receives scripts)
Activation of built-in handlers:
Both syndicator and subscriber call element.mount() to activate handlers in their respective scopes:
// Activated in both syndicator and subscriber root nodes
await this.getRootNode().mount({
do: 'builtIns.mountObserverScript'
});
await this.getRootNode().mount({
do: 'builtIns.scriptExport'
});
await this.getRootNode().mount({
do: 'builtIns.HTMLInclude'
});
await this.getRootNode().mount({
do: 'builtIns.hoistTemplate'
});
await this.getRootNode().mount({
do: 'builtIns.generateIds'
});
await this.getRootNode().mount({
do: 'builtIns.emcParserScript'
});
await this.getRootNode().mount({
do: 'builtIns.emcScript'
});This ensures that:
- MOSE scripts are processed in each scope
- Script exports are available
- HTML includes work within each shadow root
- Templates are hoisted for performance
- IDs are auto-generated in forms and components
- Parsers are loaded for EMC scripts
- EMC scripts enhance elements in each scope
Script processing:
When a subscriber receives a script element:
- Checks if the script has an
exportproperty (parsed configuration) - If not, waits for the
resolvedevent (with 5-second timeout) - Clones the script element
- Copies the
exportproperty from source to clone (by reference) - Appends the cloned script to the subscriber's light children
- The activated handlers process the cloned script in the shadow root's scope
Benefits:
- Declarative: Define observers once in the document root
- Automatic: Scripts propagate to all shadow roots automatically
- Scoped: Each shadow root gets its own observer instances
- Efficient: Parsed configurations are shared (not re-parsed)
- Maintainable: Update observers in one place, changes propagate everywhere
Example - Multiple components:
<!-- Syndicator with shared observers -->
<app-synthesizer>
<script type="mountobserver">
{
"matching": "button",
"import": "./button-enhancement.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
</app-synthesizer>
<!-- Component 1 -->
<my-header>
#shadow
<app-synthesizer></app-synthesizer>
<button>Header Button</button> <!-- Enhanced -->
</my-header>
<!-- Component 2 -->
<my-footer>
#shadow
<app-synthesizer></app-synthesizer>
<button>Footer Button</button> <!-- Enhanced -->
</my-footer>Both components receive the button enhancement observer automatically.
Error handling:
- Logs errors if handler activation fails
- Logs errors if script processing fails
- Continues processing other scripts even if one fails
- Provides 5-second timeout for waiting on
resolvedevents
Requirements:
- Must extend the
Synthesizerabstract class - Must be defined as a custom element
- Syndicator must be in the document root
- Subscribers must be in shadow roots
- All handlers must be imported and registered before use
Comparison with mountGlobally():
Unlike mountGlobally(), which discovers shadow roots by observing custom elements, Synthesizer:
- Uses explicit syndicator-subscriber pattern
- Provides more control over which scripts are syndicated
- Works with any shadow root structure
- Doesn't rely on custom element discovery
- Allows for selective script propagation
[Implemented as Syndicating Mount Observers With Synthesizer requirement](requirements/Done/Syndicating Mount Observers With Synthesizer.md)
Intra-Document HTML Includes with HTMLInclude
The builtIns.HTMLInclude handler enables declarative HTML fragment reuse within a document using <template src="#id"> syntax. Think of it as "constants for HTML" - define content once with an ID, then reference it multiple times throughout your document.
Why use HTML includes?
- Reduces duplication of repeated HTML structures
- Enables template-based content generation
- Supports partial updates via matching insertions
- Works across shadow DOM boundaries
- Supports declarative shadow DOM attachment
- Caches lookups for performance
- Detects circular references automatically
- Can be used to inherit from MOSEs
Basic usage - Simple cloning:
<!-- Define reusable content -->
<div id="reusable">
<p>This content can be reused</p>
<button>Click me</button>
</div>
<!-- Reference it with a template -->
<template src="#reusable"></template>
<!-- Results in: -->
<div>
<p>This content can be reused</p>
<button>Click me</button>
</div>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
const observer = new MountObserver({
do: 'builtIns.HTMLInclude'
});
observer.observe(document);
</script>What happens:
- The handler finds templates with
srcattributes starting with# - Searches for an element with that ID (across shadow boundaries)
- Clones the content from the source element
- Replaces the template with the cloned content
- Removes the
idattribute from cloned elements to avoid duplicate IDs
Cloning priority:
remoteContentproperty (hoisted templates) - highest prioritycontentproperty (regular templates)- The element itself (any element with an ID)
Works with hoisted templates:
<my-web-component>
#shadow
<template id="my-template">
<div>Hoisted content</div>
</template>
</my-web-component>
<!-- After hoisting, this still works -->
<template src="#my-template"></template>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
// First hoist templates
new MountObserver({
do: 'builtIns.hoistTemplate'
}).observe(document);
// Then use them
new MountObserver({
do: 'builtIns.HTMLInclude'
}).observe(document);
</script>Shadow DOM Support
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 feature.
Basic shadow DOM usage:
<!-- Define reusable shadow content -->
<template id="shadow-content">
<style>
:host {
display: block;
padding: 10px;
}
.shadow-text {
color: blue;
}
</style>
<div class="shadow-text">
<slot name="greeting"></slot>
<slot></slot>
</div>
</template>
<!-- Attach to shadow root -->
<div class="host-element">
<template src="#shadow-content" shadowrootmodeonload="open"></template>
<span slot="greeting">Hello</span>
<span>World!</span>
</div>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
new MountObserver({
do: 'builtIns.HTMLInclude'
}).observe(document);
</script>What happens:
- The handler checks for the
shadowrootmodeonloadattribute (case-insensitive) - If present, it attaches the cloned content to the parent element's shadow root
- If the parent doesn't have a shadow root, one is created with the specified mode
- If a shadow root already exists, the content is appended to it
- The template is removed as usual
Shadow root modes:
open- Shadow root is accessible viaelement.shadowRootclosed- Shadow root is not accessible from outside
Slots work automatically:
The native browser slot mechanism handles content distribution. Light DOM elements with slot attributes are automatically projected into the corresponding <slot> elements in the shadow DOM.
Example - Complex nested structure:
<template id="chorus">
<template src="#beautiful">
<span slot="subjectIs">
<slot name="subjectIs1"></slot>
</span>
</template>
<div>No matter what they say</div>
<div>Words <slot name="verb1"></slot> bring <slot name="pronoun1"></slot> down</div>
</template>
<div class="chorus">
<template src="#chorus" shadowrootmodeonload="open"></template>
<span slot="verb1">can't</span>
<span slot="pronoun1">me</span>
<span slot="subjectIs1">I am</span>
</div>Nested templates in shadow DOM:
Templates inside shadow roots are not automatically processed by the parent observer. To process nested templates, you need to observe the shadow root separately:
const host = document.querySelector('.host-element');
if (host.shadowRoot) {
const shadowObserver = new MountObserver({
do: 'builtIns.HTMLInclude'
});
await shadowObserver.observe(host.shadowRoot);
}Error handling:
- Invalid mode values: Logs warning if mode is not
"open"or"closed" - Missing parent: Logs warning if template has no parent element
- Attachment failures: Logs error if shadow root cannot be attached
Matching Insertions - Partial Updates
When a template has children, they are used to match elements in the cloned content and selectively update them. This enables partial modifications and "nulling out" content without duplicating the entire structure.
How it works:
- Template children generate CSS selectors (tag, classes, attributes)
- Matching elements in the cloned content are found
- Matched elements have their children replaced and attributes updated
- The
-iattribute specifies which attributes to update
Example - Updating attributes:
<!-- Source content -->
<div itemscope id="love">
<data value="false" itemprop="todayIsFriday">It's Thursday</data>
</div>
<!-- Template with matching insertion -->
<template src="#love">
<data value="true" itemprop="todayIsFriday" -i="value"></data>
</template>
<!-- Results in: -->
<div itemscope>
<data value="true" itemprop="todayIsFriday">It's Thursday</data>
</div>
<!-- The value attribute is updated, but content stays "It's Thursday" -->The -i attribute:
The -i (insert) attribute is a space-separated list of attribute names to update on matched elements. Attributes listed in -i are:
- Excluded from the CSS selector (allows matching elements with different values)
- Updated on matched elements with values from the template child
<template src="#form">
<!-- Update both value and placeholder -->
<input type="text" name="username" value="new" placeholder="Updated" -i="value placeholder">
</template>Example - Replacing content:
<!-- Source -->
<div id="greeting">
<p class="message">Hello</p>
</div>
<!-- Template replaces content -->
<template src="#greeting">
<p class="message">Goodbye</p>
</template>
<!-- Results in: -->
<div>
<p class="message">Goodbye</p>
</div>Example - Multiple matching elements:
<!-- Source with multiple items -->
<div id="list">
<span class="item">Item 1</span>
<span class="item">Item 2</span>
<span class="item">Item 3</span>
</div>
<!-- Update all matching items -->
<template src="#list">
<span class="item">Updated</span>
</template>
<!-- Results in: -->
<div>
<span class="item">Updated</span>
<span class="item">Updated</span>
<span class="item">Updated</span>
</div>Example - Nulling out content:
<!-- Source -->
<div id="status">
<span data-active="false" class="indicator">Inactive</span>
</div>
<!-- Update attribute, remove content -->
<template src="#status">
<span data-active="true" class="indicator" -i="data-active"></span>
</template>
<!-- Results in: -->
<div>
<span data-active="true" class="indicator"></span>
</div>
<!-- Content is removed, attribute is updated -->Use Case: Inheriting Groups of Mount-Observers
Matching insertions become particularly powerful when combined with Mount Observer Script Elements (MOSEs) for inheriting and customizing groups of mount-observers across shadow DOM boundaries.
Scenario: You have a base component with a set of mount-observers defined in its shadow root, and you want to reuse those observers in other components while making targeted modifications.
<!-- Base component with mount-observers -->
<template id="base-observers">
<script type="mountobserver">
{
"matching": "button.primary",
"import": "./primary-button.js",
"do": "builtIns.defineCustomElement"
}
</script>
<script type="mountobserver">
{
"matching": ".interactive",
"import": "./interactive.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
<script type="mountobserver">
{
"matching": "form",
"import": "./form-validator.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
</template>
<!-- Derived component - inherit and customize -->
<my-derived-component>
#shadow
<!-- Include base observers -->
<template src="#base-observers">
<!-- Override the form validator with a different one -->
<script type="mountobserver">
{
"matching": "form",
"import": "./custom-form-validator.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
</template>
<!-- Component content -->
<form>...</form>
<button class="primary">Submit</button>
</my-derived-component>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
// Bootstrap HTMLInclude handler
new MountObserver({
do: 'builtIns.HTMLInclude'
}).observe(document);
// Bootstrap MOSE handler to activate the observers
new MountObserver({
do: 'builtIns.mountObserverScript'
}).observe(document);
</script>What happens:
- The
<template src="#base-observers">clones all three MOSE scripts - The matching insertion finds the form validator script (matching by
matchingattribute) - Replaces its content with the custom validator configuration
- All three scripts are inserted into the shadow root
- The MOSE handler activates all observers in the shadow root's registry scope
Benefits:
- Composition: Build complex observer configurations from reusable pieces
- Inheritance: Derive new components with modified observer behavior
- Scoped registries: Each shadow root gets its own set of observers
- Declarative: No JavaScript required for observer inheritance
- Maintainable: Update base observers in one place, changes propagate
Advanced pattern - Multiple inheritance:
<!-- Base UI observers -->
<template id="ui-observers">
<script type="mountobserver">{"matching": "button", ...}</script>
<script type="mountobserver">{"matching": "input", ...}</script>
</template>
<!-- Base data observers -->
<template id="data-observers">
<script type="mountobserver">{"matching": "[itemscope]", ...}</script>
</template>
<!-- Component combines both -->
<my-component>
#shadow
<template src="#ui-observers"></template>
<template src="#data-observers"></template>
<!-- Add component-specific observers -->
<script type="mountobserver">
{
"matching": ".special",
"import": "./special.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
</my-component>This pattern enables:
- Mixins: Combine multiple observer groups
- Layering: Stack observers from different concerns (UI, data, behavior)
- Customization: Override specific observers while keeping others
- Reusability: Share observer configurations across components
Declarative usage with MOSE:
<script type="mountobserver">
{
"do": "builtIns.HTMLInclude"
}
</script>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
new MountObserver({
do: 'builtIns.mountObserverScript'
}).observe(document);
</script>Error handling:
The handler provides helpful error messages:
- Missing elements:
data-include-error="Element with id='foo' not found" - Circular references:
data-include-error="Circular reference detected: #foo" - Clone failures:
data-include-error="Unable to clone content from #foo"
Performance:
- Uses WeakMap caching for repeated ID lookups
- Efficient for scenarios like periodic tables with many repeated elements
- Searches across shadow boundaries using
upShadowSearch(registry-aware) - Respects scoped custom element registry boundaries
- Cleans up cache entries when elements are garbage collected
MOSE Export Optimization:
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.
How it works:
- Detects when cloning a live element (not a template) from a different root node
- Finds all
script[type="mountobserver"]elements in both source and clone - Matches scripts by their
idattribute - Copies the
exportproperty from source to clone (by reference) - If source script hasn't been processed yet, waits for the
resolvedevent
Example:
<!-- Source element with MOSE in light DOM -->
<div id="observer-config">
<script type="mountobserver" id="my-config">
{
"matching": ".interactive",
"import": "./interactive.js",
"do": "builtIns.enhanceMountedElement"
}
</script>
<div class="interactive">Content</div>
</div>
<!-- Clone into shadow DOM -->
<my-component>
#shadow
<template src="#observer-config"></template>
</my-component>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
// Process MOSEs in light DOM
new MountObserver({
do: 'builtIns.mountObserverScript'
}).observe(document.body);
// Clone into shadow roots
new MountObserver({
do: 'builtIns.HTMLInclude'
}).observe(document);
</script>Benefits:
- Performance: JSON is parsed only once, not for each clone
- Memory efficiency: Cloned scripts share the same export object
- Consistency: All clones use identical configuration
- Automatic: No manual intervention required
Requirements:
- Source and clone must be in different root nodes (document vs shadow root)
- MOSE scripts must have
idattributes for matching - Source script must be processed by
builtIns.mountObserverScriptorbuiltIns.scriptExportbefore cloning
Implemented as MatchingInsertionsAndDeletionsWithIntraDocumentHTMLIncludes requirement
Automatic ID Generation with genIds
The builtIns.generateIds handler automatically generates unique IDs for elements within scoped containers using the id-generation package. This is particularly useful for forms, microdata structures, and any scenario where you need unique IDs for accessibility or linking purposes.
Why use automatic ID generation?
- Eliminates manual ID management and conflicts
- Supports scoped ID generation within fieldsets or itemscope containers
- Automatically updates ID references in attributes (aria-labelledby, for, etc.)
- Provides shorthand syntax for common patterns
- Handles deferred attribute activation
- Removes
disabledfrom fieldsets after processing
Basic usage:
<fieldset disabled>
<label>
LHS: <input data-id={{lhs}}>
</label>
<label for=rhs>
RHS: <input data-id={{rhs}}>
</label>
<template -id defer-🎚️ 🎚️='on if isEqual, based on #{{lhs}} and #{{rhs}}.'>
<div>LHS === RHS</div>
</template>
</fieldset>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
const observer = new MountObserver({
do: 'builtIns.generateIds'
});
observer.observe(document);
</script>What happens:
- The handler watches for elements with the
-idattribute (the trigger) - Finds the nearest scope container (fieldset, [itemscope], or root)
- Generates unique IDs for elements with
data-id={{name}},#,@, or|attributes - Replaces
#{{name}}references with generated IDs in attributes - Removes
-idanddefer-*attributes after processing - Removes
disabledfrom fieldset containers
Result:
<fieldset>
<label>
LHS: <input id=gid-0 data-id=lhs>
</label>
<label for=rhs>
RHS: <input id=gid-1 data-id=rhs>
</label>
<template 🎚️='on if isEqual, based on #gid-0 and #gid-1.'>
<div>LHS === RHS</div>
</template>
</fieldset>Shorthand attributes:
<fieldset disabled>
<!-- # uses element's tag name -->
<my-element #></my-element> <!-- becomes id=gid-0 data-id=my-element -->
<!-- @ uses element's name attribute -->
<input @ name="email" type="email"> <!-- becomes id=gid-1 data-id=email -->
<!-- | uses element's itemprop attribute -->
<span | itemprop="price">$99</span> <!-- becomes id=gid-2 data-id=price -->
<button -id>Generate IDs</button>
</fieldset>Side effects with data-id:
The data-id attribute supports special symbols that trigger side effects:
<form>
<fieldset disabled>
<label>
LHS: <input data-id="{{@. lhs}}">
</label>
<label for=rhs>
RHS: <span contenteditable data-id="{{|.% rhs}}">
</label>
<template -id defer-🎚️ 🎚️='on if isEqual, based on #{{lhs}} and #{{rhs}}.'>
<div>LHS === RHS</div>
</template>
</fieldset>
</form>Result:
<form>
<fieldset>
<label>
LHS: <input name=lhs class=lhs id=gid-0 data-id=lhs>
</label>
<label for=rhs>
RHS: <span contenteditable itemprop=rhs class=rhs part=rhs id=gid-1 data-id=rhs>
</label>
<template 🎚️='on if isEqual, based on #gid-0 and #gid-1.'>
<div>LHS === RHS</div>
</template>
</fieldset>
</form>Symbol meanings:
| Symbol | Attribute | Meaning | |--------|-------------------|------------------------------------------------------------------------------| | @ | name | Second letter of name, common in social media for selecting names | | | | itemprop | "Pipe" resembles itemprop, half of dollar sign, looks like an I | | $ | itemscope+itemprop| Combination of S for Scope and Pipe | | % | part | Starts with p, percent indicates proportion | | . | class | CSS selector |
Multiple symbols can be combined: data-id="{{@.% myName}}" adds name, class, and part attributes.
Deferred attributes:
Use defer-* prefix to prevent attributes from being applied until IDs are generated:
<fieldset disabled>
<!-- These attributes won't work until IDs are generated -->
<label defer-for="for: #{{email}}">Email:</label>
<input data-id={{email}} type="email">
<button -id>Activate Form</button>
</fieldset>Supported reference attributes:
The handler automatically replaces #{{name}} references in these attributes:
- ARIA:
aria-labelledby,aria-describedby,aria-controls,aria-owns,aria-flowto,aria-activedescendant - Form:
for,form,list - Microdata:
itemref - Any
data-*attribute - Any attribute with a
defer-*prefix
Declarative usage with MOSE:
<script type="mountobserver">
{
"do": "builtIns.generateIds"
}
</script>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
new MountObserver({
do: 'builtIns.mountObserverScript'
}).observe(document);
</script>Scope containers:
The handler looks for the nearest scope container using .closest():
<fieldset>elements- Elements with
[itemscope]attribute - Falls back to the root node if no scope is found
Global counter:
IDs are generated using a global counter (via Symbol.for) to ensure uniqueness across multiple module instances. Generated IDs follow the pattern gid-0, gid-1, gid-2, etc.
Scoped Parser Registry for EMC Scripts
The scoped parser registry system enables lazy-loading of complex parsers for enhancement attributes while maintaining framework isolation. Each synthesizer element (be-hive, htmx-container, alpine-scope, etc.) maintains its own parser registry to prevent conflicts between different framework libraries.
Why use scoped parser registries?
- Lazy load complex parsers only when needed (e.g., nested-regex-groups)
- Maintain framework isolation (HTMX, Alpine, be-hive can coexist)
- Declarative parser loading via HTML script tags
- Programmatic parser registration for dynamic scenarios
- Automatic parser waiting before enhancement initialization
- Built-in parsers remain globally available
Basic Usage - Declarative Parser Loading
Load parsers declaratively using <script type="emc-parser"> elements:
<be-hive>
<!-- Load parser first -->
<script type="emc-parser"
src="nested-regex-groups/parser.js"
parser-name="nestedRegexGroups"></script>
<!-- Then load enhancement that depends on it -->
<script type="emc"
src="be-switched/emc.json"
wait-for-parsers="nestedRegexGroups"></script>
</be-hive>
<script type="module">
import { MountObserver } from 'mount-observer/MountObserver.js';
// Bootstrap the handlers
new MountObserver({
do: 'builtIns.emcScript'
}).observe(document);
</script>What happens:
- The
emc-parserscript loads the parser module via dynamic import - Parser is registered in the be-hive element's scoped registry
parser-registeredevent is dispatched- The
emcscript waits for the parser to be registered - Once ready, the enhancement is processed with access to the parser
Multiple Parsers
Wait for multiple parsers using space-delimited names:
<be-hive>
<script type="emc-parser" src="parser1.js" parser-name="parser1"></script>
<script type="emc-parser" src="parser2.js" parser-name="parser2"></script>
<!-- Wait for both parsers -->
<script type="emc"
src="my-enhancement/emc.json"
wait-for-parsers="parser1 parser2"></script>
</be-hive>Custom Timeout
Configure parser loading timeout (default: 60 seconds):
<be-hive>
<script type="emc-parser" src="slow-parser.js" parser-name="slowParser"></script>
<!-- Wait up to 2 minutes -->
<script type="emc"
src="my-enhancement/emc.json"
wait-for-parsers="slowParser"
data-parser-timeout="120000"></script>
</be-hive>Programmatic Parser Registration
Register parsers programmatically via JavaScript:
<be-hive id="myHive">
<script type="emc"
src="my-enhancement/emc.json"
wait-for-parsers="customParser"></script>
</be-hive>
<script type="module">
import { registerParser } from 'assign-gingerly/parserRegistry.js';
// Load parser programmatically
const parser = await import('./custom-parser.js');
const beHive = document.getElementById('myHive');
registerParser(beHive, 'customParser', parser.default);
</script>Parser Interface
Parsers are simple functions that transform attribute string values:
// my-parser.js
export default function parse(value) {
// Transform the string value
if (value === null) return null;
// Your parsing logic here
return parsedValue;
}Requirements:
- Parser must be a function with signature
(v: string | null) => any - Must be exported as the default export
- Should handle
nullvalues appropriately - Should throw descriptive errors for invalid input
Scoped vs Global Parsers
Scoped parsers (registered in be-hive elements):
- Isolated to a specific synthesizer element and its descendants
- Different frameworks can use the same parser names without conflicts
- Registered via
emc-parserscripts orregisterParser()
Global parsers (built-in):
- Available everywhere without registration
- Includes:
timestamp,date,csv,int,float,boolean,json - Fallback when parser not found in scoped registry
Resolution order:
- Check scoped registry (if synthesizer element exists)
- Fall back to global registry
- Throw error if not found in either
Shadow DOM Syndication
Parser registries are scoped to synthesizer elements and apply to all shadow roots within that scope:
<!-- Syndicator in document root -->
<be-hive>
<script type="emc-parser" src="parser.js" parser-name="myParser"></script>
<script type="emc" src="enhancement/emc.json" wait-for-parsers="myParser"></script>
</be-hive>
<!-- Component with shadow root -->
<my-component>
#shadow
<!-- Subscriber receives scripts from syndicator -->
<be-hive></be-hive>
<!-- Elements here can use the parser -->
<div be-switched="...">Content</div>
</my-component>How it works:
- Parser script is syndicated to shadow root's be-hive
- Parser is registered in the shadow root's scoped registry
- EMC script is syndicated and waits for parser
- Enhancements in shadow root have access to the parser
Error Handling
Parser loading errors:
<!-- Parser module fails to load -->
<script type="emc-parser"
src="broken-parser.js"
parser-name="broken"
data-parser-error="Module not found"></script>Parser waiting timeout:
<!-- EMC script times out waiting for parser -->
<script type="emc"
src="m
