Paint Me HTML
In Topic 11, we mapped out where the dynamic "holes" in your HTML are. Now, we look at the engine that actually fills them with data. The render() function is the most frequently called piece of code in LegoDOM, it is the bridge between JavaScript state and the pixels on the screen.
Rendering render() Engine
The render(el) function doesn't refresh the whole block. Instead, it iterates through the "Instruction Objects" (bindings) created during the scanning phase and updates only what is necessary.
const render = (el) => {
const state = el._studs;
if (!state) return;
const data = getPrivateData(el);
if (data.rendering) return;
data.rendering = true;
try {
const shadow = el.shadowRoot;
if (!shadow) return;
if (!data.bindings) data.bindings = scanForBindings(shadow);
if (config.metrics?.onRenderStart) config.metrics.onRenderStart(el);
data.bindings.forEach(b => {
// 1. Conditionals (b-if)
if (b.type === 'b-if') {
const condition = !!safeEval(b.expr, { state, global: Lego.globals, self: b.node });
const isAttached = !!b.node.parentNode;
if (condition && !isAttached) b.anchor.parentNode.replaceChild(b.node, b.anchor);
else if (!condition && isAttached) b.node.parentNode.replaceChild(b.anchor, b.node);
}
// 2. Visibility (b-show)
if (b.type === 'b-show') b.node.style.display = safeEval(b.expr, { state, self: b.node }) ? '' : 'none';
// 3. Text (b-text, b-html)
if (b.type === 'b-text') b.node.textContent = escapeHTML(resolve(b.path, state));
if (b.type === 'b-html') b.node.innerHTML = safeEval(b.expr, { state, self: b.node }); // Trusted HTML
// 4. Sync (b-sync)
if (b.type === 'b-sync') syncModelValue(b.node, resolve(b.node.getAttribute('b-sync'), state));
// 5. Mustaches
if (b.type === 'text') {
const out = b.template.replace(/\[\[(.*?)\]\]/g, (_, k) => escapeHTML(safeEval(k.trim(), { state, self: b.node }) ?? ''));
if (b.node.textContent !== out) b.node.textContent = out;
}
if (b.type === 'attr') {
const out = b.template.replace(/\[\[(.*?)\]\]/g, (_, k) => escapeHTML(safeEval(k.trim(), { state, self: b.node }) ?? ''));
if (b.node.getAttribute(b.attrName) !== out) {
b.node.setAttribute(b.attrName, out);
if (b.attrName === 'class') b.node.className = out;
}
}
if (b.type === 'b-for') {
const list = safeEval(b.listName, { state, global: Lego.globals, self: el }) || [];
if (!forPools.has(b.node)) forPools.set(b.node, new Map());
const pool = forPools.get(b.node);
const currentKeys = new Set();
list.forEach((item, i) => {
const key = (item && typeof item === 'object') ? (item.__id || (item.__id = Math.random())) : `${i}-${item}`;
currentKeys.add(key);
let child = pool.get(key);
if (!child) {
const temp = document.createElement('div');
temp.innerHTML = b.template;
child = temp.firstElementChild;
pool.set(key, child);
bind(child, el, { name: b.itemName, listName: b.listName, index: i });
}
const localScope = Object.assign(Object.create(state), { [b.itemName]: item });
updateNodeBindings(child, localScope);
child.querySelectorAll('[b-sync]').forEach(input => {
const path = input.getAttribute('b-sync');
if (path.startsWith(b.itemName + '.')) {
const list = safeEval(b.listName, { state, global: Lego.globals, self: el });
syncModelValue(input, resolve(path.split('.').slice(1).join('.'), list[i]));
}
});
if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
});
for (const [key, node] of pool.entries()) {
if (!currentKeys.has(key)) { node.remove(); pool.delete(key); }
}
}
});
} finally {
} finally {
if (config.metrics?.onRenderEnd) config.metrics.onRenderEnd(el);
data.rendering = false;
}
}
};1. The Guard Rails
Before doing any work, render checks two things:
The State: It ensures
el._studsexists.The Recursion Lock: It sets
data.rendering = trueat the start andfalseat the end. This prevents a "Render Loop" where an update triggers a render, which accidentally triggers another update.
2. Surgical Execution
The function loops through data.bindings and performs specific actions based on the type:
b-show: It evaluates the expression. If false, it setsdisplay: none. This is a "CSS-based" conditional; the element stays in the DOM but becomes invisible. Might change toremove()in the futureb-text: It uses theresolve()helper to find the value in your state and sets thetextContent.text(Mustaches): It takes the original template (e.g.,Count:), replaces the mustache with the actual value, and updates the text node.attr: It updates attributes likesrc,href, orclass. It even has a special check: if the attribute isclass, it also updatesnode.classNameto ensure the browser applies the styles correctly.
3. The safeEval Bridge & Security
You’ll notice that for things like b-show or mustaches, LegoDOM calls safeEval(expr, { state, self: b.node }).
Why not just eval()?eval() executes code in the global scope, which is a massive security hole and performance killer.
safeEval uses new Function with a Proxy Sandbox:
- Block List: It immediately throws if it sees dangerous keywords like
eval,Function,import, or global objects likewindow,document,fetch(unless explicitly provided). - Scope Proxy: The execution context is a
Proxy(with(proxy) { ... }). If the code tries to accessdocument.cookieeffectively, the proxy intercepts it. - Configurable Syntax: As of v2.0, this also handles the dynamic regex for
[[ ]]vssupport viaLego.config.syntax.
4. Directives vs. Mustache Priority
render processes directives (like b-show and b-text) and mustaches in the same loop. However, because it works with direct DOM references saved in the bindings array, it never has to "re-parse" the HTML string. It simply touches the specific property (like .value or .textContent) of the existing DOM node.
Summary: render() is a "Loop of Truth." It walks through the map created by the scanner, evaluates the current state of your data, and applies those values to the specific DOM nodes that need them.