In the beginning - Lego.init()
The init() function is the heart of LegoDOM. It orchestrates the entire initialization process, setting up the observer, connecting the global state, and processing existing blocks.
Location in the Codebase
The init() method is defined in src/index.js as part of the exported Lego object:
// src/index.js
import { snap, unsnap } from './core/lifecycle.js';
import { bind, render } from './core/renderer.js';
import { registry, activeBlocks, getPrivateData } from './core/registry.js';
import { config } from './utils/helpers.js';
import { setGlobals, globals } from './core/globals.js';
import { loadStylesheets } from './core/stylesheets.js';
import { routes, _go, _matchRoute } from './features/router.js';
import { initCrossTabSync } from './features/persistence.js';
import { defineLegoFile } from './core/parser.js';
const Lego = {
init: async (root = document.body, options = {}) => {
// 1. Validate root
if (!root || typeof root.nodeType !== 'number') root = document.body;
// 2. Configure
setStyleConfig(options.styles || {});
config.loader = options.loader;
config.debug = options.debug === true;
// 3. Load stylesheets
await loadStylesheets();
// 4. Initialize routing if configured
if (routes.length > 0) {
const path = window.location.pathname;
const search = window.location.search;
const match = routes.find(r => r.regex.test(path));
if (match) {
const values = path.match(match.regex).slice(1);
const params = Object.fromEntries(match.paramNames.map((n, i) => [n, values[i]]));
const query = Object.fromEntries(new URLSearchParams(search));
globals.$route.url = path + search;
globals.$route.route = match.path;
globals.$route.params = params;
globals.$route.query = query;
globals.$route.method = 'GET';
globals.$route.body = null;
}
}
// 5. Register inline templates
document.querySelectorAll('template[b-id]').forEach(t => {
registry[t.getAttribute('b-id')] = t;
});
// 6. Setup auto-loader if configured
const checkAndLoad = (n) => {
if (n.nodeType !== Node.ELEMENT_NODE) return;
snap(n);
const tagName = n.tagName.toLowerCase();
if (tagName.includes('-') && !registry[tagName] && config.loader && !activeBlocks.has(n)) {
const result = config.loader(tagName);
if (result) {
const promise = (typeof result === 'string')
? fetch(result).then(r => r.text())
: result;
Promise.resolve(promise)
.then(legoFile => Lego.defineLegoFile(legoFile, tagName + '.lego'))
.catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
}
}
};
// 7. Setup MutationObserver
const observer = new MutationObserver(m => m.forEach(r => {
r.addedNodes.forEach(checkAndLoad);
r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
}));
observer.observe(root, { childList: true, subtree: true });
// 8. Process existing DOM
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
checkAndLoad(root);
while (walker.nextNode()) checkAndLoad(walker.currentNode);
// 9. Bind globals to root
root._studs = globals;
bind(root, root);
render(root);
// 10. Setup routing event listeners
if (routes.length > 0) {
window.addEventListener('popstate', (event) => {
const targets = event.state?.legoTargets || null;
_matchRoute(targets);
});
document.addEventListener('click', e => {
const path = e.composedPath();
const link = path.find(el => el.tagName === 'A' && (el.hasAttribute('b-target') || el.hasAttribute('b-link')));
if (link) {
e.preventDefault();
const href = link.getAttribute('href');
const targetAttr = link.getAttribute('b-target');
const targets = targetAttr ? targetAttr.split(/\s+/).filter(Boolean) : [];
const shouldPush = link.getAttribute('b-link') !== 'false';
globals.$go(href, ...targets).get(shouldPush);
}
});
_matchRoute();
}
// 11. Initialize cross-tab sync
initCrossTabSync();
},
// ... other methods
};
export { Lego };
export default Lego;The Initialization Flow
1. Template Registration
The first thing init() does is look for blueprints you've already defined in your HTML.
document.querySelectorAll('template[b-id]').forEach(t => {
registry[t.getAttribute('b-id')] = t;
});It scans the entire document for any <template> tag that has a b-id attribute. It then adds these to the registry object (from src/core/registry.js). This allows you to define blocks directly in your HTML file without writing a single line of JavaScript.
2. Stylesheet Loading
If you're using shared stylesheets, init() loads them:
await loadStylesheets();This is handled by src/core/stylesheets.js and uses the Constructable Stylesheets API for performance.
3. Setting up the Registry (customElements)
This is the most critical part of the initialization. LegoDOM registers your blocks as Native Web Components:
- What it does: It tells the browser that whenever it sees a tag like
<user-card>, it should treat it as a specialLegoBlock. - The Mechanism: It calls
customElements.define('user-card', HTMLElement). - The Reaction:
- When a block is added (via HTML, JS, or
b-if), the browser triggersconnectedCallback()which callssnap(). - When a block is removed, the browser triggers
disconnectedCallback()which callsunsnap().
- When a block is added (via HTML, JS, or
4. Manifest (Pre-Registration)
To support lazy-loading inside Shadow DOMs without an Observer, you can pre-register blocks using a manifest. This tells LegoDOM: "User-Card exists at this URL. If you see it, fetch it."
Lego.init({
manifest: [
{
base: '/api/components/',
suffix: true, // Appends .lego
legos: ['user-card', 'nav-bar']
},
{
map: {
'server-time': 'POST:/api/time'
}
}
]
});- Efficiency: The browser handles detection via
customElements. LegoDOM only fetches the code when the element is actually used. - Shadow DOM: Because the tag is known to the browser (registered as a lazy custom element), it works perfectly inside Shadow Roots.
5. Auto-Discovery (Fallback)
If you don't use a manifest, you can validly use config.loader with the Auto-Discovery observer.
const discover = (n) => {
// ...
};Note: The
MutationObserveris ONLY created if you provide aloaderin initialization options. If you only usemanifest(or standard registration), LegoDOM runs with zero observers for maximum performance.
5. The "First Snap"
After setting up the observer, it processes all existing elements:
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
checkAndLoad(root);
while (walker.nextNode()) checkAndLoad(walker.currentNode);Why? The observer only sees new things being added. It doesn't see what was already there when the page loaded.
By using a
TreeWalker, LegoDOM efficiently processes every custom block that was present in the initial HTML.
6. Global Data Binding
The root element gets bound to the global state:
root._studs = globals;
bind(root, root);
render(root);This treats the entire root as if it were a giant block. This allows you to use reactive data in your "Light DOM" (the regular HTML outside of blocks) by referencing values stored in Lego.globals (from src/core/globals.js).
7. Routing Initialization
If you have defined any routes using Lego.route(), the init function sets up:
- History Management: Listens for
popstateevents (back/forward buttons) - Link Interception: Captures clicks on
<a>tags withb-linkorb-targetattributes - Initial Route Match: Calls
_matchRoute()to render the current route
All routing logic is in src/features/router.js.
8. Cross-Tab Synchronization
Finally, init() enables localStorage synchronization across browser tabs:
initCrossTabSync();This is handled by src/features/persistence.js and allows $db properties to sync automatically.
Summary of init(): It finds templates, starts a "watchdog" for new elements, processes existing elements, enables global data, initializes routing, and sets up cross-tab sync. The entire initialization is modular, with each concern handled by its respective module in src/.