lego-dom 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/main.js +24 -3
- package/main.min.js +7 -0
- package/package.json +3 -1
- package/vite-plugin.js +0 -14
- package/.github/workflows/deploy-docs.yml +0 -56
- package/.legodom +0 -87
- package/docs/.vitepress/config.js +0 -162
- package/docs/api/config.md +0 -95
- package/docs/api/define.md +0 -58
- package/docs/api/directives.md +0 -50
- package/docs/api/globals.md +0 -29
- package/docs/api/index.md +0 -30
- package/docs/api/lifecycle.md +0 -40
- package/docs/api/route.md +0 -37
- package/docs/api/vite-plugin.md +0 -58
- package/docs/contributing/01-welcome.md +0 -38
- package/docs/contributing/02-registry.md +0 -133
- package/docs/contributing/03-batcher.md +0 -110
- package/docs/contributing/04-reactivity.md +0 -87
- package/docs/contributing/05-caching.md +0 -59
- package/docs/contributing/06-init.md +0 -136
- package/docs/contributing/07-observer.md +0 -72
- package/docs/contributing/08-snap.md +0 -140
- package/docs/contributing/09-diffing.md +0 -69
- package/docs/contributing/10-studs.md +0 -78
- package/docs/contributing/11-scanner.md +0 -117
- package/docs/contributing/12-render.md +0 -138
- package/docs/contributing/13-directives.md +0 -243
- package/docs/contributing/14-events.md +0 -57
- package/docs/contributing/15-router.md +0 -57
- package/docs/contributing/16-state.md +0 -47
- package/docs/contributing/17-legodom.md +0 -48
- package/docs/contributing/index.md +0 -24
- package/docs/examples/form.md +0 -42
- package/docs/examples/index.md +0 -104
- package/docs/examples/routing.md +0 -409
- package/docs/examples/sfc-showcase.md +0 -34
- package/docs/examples/todo-app.md +0 -383
- package/docs/guide/cdn-usage.md +0 -328
- package/docs/guide/components.md +0 -412
- package/docs/guide/directives.md +0 -539
- package/docs/guide/directory-structure.md +0 -248
- package/docs/guide/faq.md +0 -210
- package/docs/guide/getting-started.md +0 -262
- package/docs/guide/index.md +0 -88
- package/docs/guide/lifecycle.md +0 -525
- package/docs/guide/quick-start.md +0 -49
- package/docs/guide/reactivity.md +0 -415
- package/docs/guide/routing.md +0 -334
- package/docs/guide/server-side.md +0 -134
- package/docs/guide/sfc.md +0 -420
- package/docs/guide/templating.md +0 -388
- package/docs/index.md +0 -160
- package/docs/public/logo.svg +0 -17
- package/docs/router/basic-routing.md +0 -103
- package/docs/router/cold-entry.md +0 -91
- package/docs/router/history.md +0 -69
- package/docs/router/index.md +0 -73
- package/docs/router/resolver.md +0 -74
- package/docs/router/surgical-swaps.md +0 -134
- package/docs/tutorial/01-project-setup.md +0 -152
- package/docs/tutorial/02-your-first-component.md +0 -226
- package/docs/tutorial/03-adding-routes.md +0 -279
- package/docs/tutorial/04-multi-page-app.md +0 -329
- package/docs/tutorial/05-state-and-globals.md +0 -285
- package/docs/tutorial/index.md +0 -40
- package/examples/vite-app/README.md +0 -71
- package/examples/vite-app/index.html +0 -42
- package/examples/vite-app/package.json +0 -18
- package/examples/vite-app/src/app.css +0 -3
- package/examples/vite-app/src/app.js +0 -29
- package/examples/vite-app/src/components/app-navbar.lego +0 -34
- package/examples/vite-app/src/components/customers/customer-details.lego +0 -24
- package/examples/vite-app/src/components/customers/customer-orders.lego +0 -21
- package/examples/vite-app/src/components/customers/order-list.lego +0 -55
- package/examples/vite-app/src/components/greeting-card.lego +0 -41
- package/examples/vite-app/src/components/sample-component.lego +0 -75
- package/examples/vite-app/src/components/shells/customers-shell.lego +0 -21
- package/examples/vite-app/src/components/side-menu.lego +0 -46
- package/examples/vite-app/src/components/todo-list.lego +0 -239
- package/examples/vite-app/src/components/widgets/user-card.lego +0 -27
- package/examples/vite-app/vite.config.js +0 -22
- package/tests/error.test.js +0 -74
- package/tests/main.test.js +0 -103
- package/tests/memory.test.js +0 -68
- package/tests/monitoring.test.js +0 -74
- package/tests/naming.test.js +0 -74
- package/tests/parse-lego.test.js +0 -65
- package/tests/security.test.js +0 -67
- package/tests/server.test.js +0 -114
- package/tests/syntax.test.js +0 -67
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
# Paint Me HTML
|
|
2
|
-
|
|
3
|
-
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.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
## Rendering `render()` Engine
|
|
7
|
-
|
|
8
|
-
The `render(el)` function doesn't refresh the whole component. Instead, it iterates through the "Instruction Objects" (bindings) created during the scanning phase and updates only what is necessary.
|
|
9
|
-
|
|
10
|
-
```js
|
|
11
|
-
const render = (el) => {
|
|
12
|
-
const state = el._studs;
|
|
13
|
-
if (!state) return;
|
|
14
|
-
const data = getPrivateData(el);
|
|
15
|
-
if (data.rendering) return;
|
|
16
|
-
data.rendering = true;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const shadow = el.shadowRoot;
|
|
20
|
-
if (!shadow) return;
|
|
21
|
-
if (!data.bindings) data.bindings = scanForBindings(shadow);
|
|
22
|
-
|
|
23
|
-
if (config.metrics?.onRenderStart) config.metrics.onRenderStart(el);
|
|
24
|
-
|
|
25
|
-
data.bindings.forEach(b => {
|
|
26
|
-
// 1. Conditionals (b-if)
|
|
27
|
-
if (b.type === 'b-if') {
|
|
28
|
-
const condition = !!safeEval(b.expr, { state, global: Lego.globals, self: b.node });
|
|
29
|
-
const isAttached = !!b.node.parentNode;
|
|
30
|
-
if (condition && !isAttached) b.anchor.parentNode.replaceChild(b.node, b.anchor);
|
|
31
|
-
else if (!condition && isAttached) b.node.parentNode.replaceChild(b.anchor, b.node);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 2. Visibility (b-show)
|
|
35
|
-
if (b.type === 'b-show') b.node.style.display = safeEval(b.expr, { state, self: b.node }) ? '' : 'none';
|
|
36
|
-
|
|
37
|
-
// 3. Text (b-text, b-html)
|
|
38
|
-
if (b.type === 'b-text') b.node.textContent = escapeHTML(resolve(b.path, state));
|
|
39
|
-
if (b.type === 'b-html') b.node.innerHTML = safeEval(b.expr, { state, self: b.node }); // Trusted HTML
|
|
40
|
-
|
|
41
|
-
// 4. Sync (b-sync)
|
|
42
|
-
if (b.type === 'b-sync') syncModelValue(b.node, resolve(b.node.getAttribute('b-sync'), state));
|
|
43
|
-
|
|
44
|
-
// 5. Mustaches
|
|
45
|
-
if (b.type === 'text') {
|
|
46
|
-
const out = b.template.replace(/\[\[(.*?)\]\]/g, (_, k) => escapeHTML(safeEval(k.trim(), { state, self: b.node }) ?? ''));
|
|
47
|
-
if (b.node.textContent !== out) b.node.textContent = out;
|
|
48
|
-
}
|
|
49
|
-
if (b.type === 'attr') {
|
|
50
|
-
const out = b.template.replace(/\[\[(.*?)\]\]/g, (_, k) => escapeHTML(safeEval(k.trim(), { state, self: b.node }) ?? ''));
|
|
51
|
-
if (b.node.getAttribute(b.attrName) !== out) {
|
|
52
|
-
b.node.setAttribute(b.attrName, out);
|
|
53
|
-
if (b.attrName === 'class') b.node.className = out;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (b.type === 'b-for') {
|
|
57
|
-
const list = safeEval(b.listName, { state, global: Lego.globals, self: el }) || [];
|
|
58
|
-
if (!forPools.has(b.node)) forPools.set(b.node, new Map());
|
|
59
|
-
const pool = forPools.get(b.node);
|
|
60
|
-
const currentKeys = new Set();
|
|
61
|
-
list.forEach((item, i) => {
|
|
62
|
-
const key = (item && typeof item === 'object') ? (item.__id || (item.__id = Math.random())) : `${i}-${item}`;
|
|
63
|
-
currentKeys.add(key);
|
|
64
|
-
let child = pool.get(key);
|
|
65
|
-
if (!child) {
|
|
66
|
-
const temp = document.createElement('div');
|
|
67
|
-
temp.innerHTML = b.template;
|
|
68
|
-
child = temp.firstElementChild;
|
|
69
|
-
pool.set(key, child);
|
|
70
|
-
bind(child, el, { name: b.itemName, listName: b.listName, index: i });
|
|
71
|
-
}
|
|
72
|
-
const localScope = Object.assign(Object.create(state), { [b.itemName]: item });
|
|
73
|
-
updateNodeBindings(child, localScope);
|
|
74
|
-
|
|
75
|
-
child.querySelectorAll('[b-sync]').forEach(input => {
|
|
76
|
-
const path = input.getAttribute('b-sync');
|
|
77
|
-
if (path.startsWith(b.itemName + '.')) {
|
|
78
|
-
const list = safeEval(b.listName, { state, global: Lego.globals, self: el });
|
|
79
|
-
syncModelValue(input, resolve(path.split('.').slice(1).join('.'), list[i]));
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
|
|
83
|
-
});
|
|
84
|
-
for (const [key, node] of pool.entries()) {
|
|
85
|
-
if (!currentKeys.has(key)) { node.remove(); pool.delete(key); }
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
} finally {
|
|
90
|
-
} finally {
|
|
91
|
-
if (config.metrics?.onRenderEnd) config.metrics.onRenderEnd(el);
|
|
92
|
-
data.rendering = false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 1. The Guard Rails
|
|
99
|
-
|
|
100
|
-
Before doing any work, `render` checks two things:
|
|
101
|
-
|
|
102
|
-
- **The State**: It ensures `el._studs` exists.
|
|
103
|
-
|
|
104
|
-
- **The Recursion Lock**: It sets `data.rendering = true` at the start and `false` at the end. This prevents a "Render Loop" where an update triggers a render, which accidentally triggers another update.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
### 2. Surgical Execution
|
|
108
|
-
|
|
109
|
-
The function loops through `data.bindings` and performs specific actions based on the `type`:
|
|
110
|
-
|
|
111
|
-
- **`b-show`**: It evaluates the expression. If false, it sets `display: none`. This is a "CSS-based" conditional; the element stays in the DOM but becomes invisible. **Might change to `remove()` in the future**
|
|
112
|
-
|
|
113
|
-
- **`b-text`**: It uses the `resolve()` helper to find the value in your state and sets the `textContent`.
|
|
114
|
-
|
|
115
|
-
- **`text` (Mustaches)**: It takes the original template (e.g., `Count: {{count}}`), replaces the mustache with the actual value, and updates the text node.
|
|
116
|
-
|
|
117
|
-
- **`attr`**: It updates attributes like `src`, `href`, or `class`. It even has a special check: if the attribute is `class`, it also updates `node.className` to ensure the browser applies the styles correctly.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
### 3. The `safeEval` Bridge & Security
|
|
121
|
-
|
|
122
|
-
You’ll notice that for things like `b-show` or mustaches, the library calls `safeEval(expr, { state, self: b.node })`.
|
|
123
|
-
|
|
124
|
-
**Why not just `eval()`?**
|
|
125
|
-
`eval()` executes code in the global scope, which is a massive security hole and performance killer.
|
|
126
|
-
|
|
127
|
-
`safeEval` uses `new Function` with a **Proxy Sandbox**:
|
|
128
|
-
1. **Block List**: It immediately throws if it sees dangerous keywords like `eval`, `Function`, `import`, or global objects like `window`, `document`, `fetch` (unless explicitly provided).
|
|
129
|
-
2. **Scope Proxy**: The execution context is a `Proxy` (`with(proxy) { ... }`). If the code tries to access `document.cookie` effectively, the proxy intercepts it.
|
|
130
|
-
3. **Configurable Syntax**: As of v2.0, this also handles the dynamic regex for `[[ ]]` vs `{{ }}` support via `Lego.config.syntax`.
|
|
131
|
-
|
|
132
|
-
### 4. Directives vs. Mustache Priority
|
|
133
|
-
|
|
134
|
-
`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.
|
|
135
|
-
|
|
136
|
-
----------
|
|
137
|
-
|
|
138
|
-
**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.
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
# You know C++, I know Web Components++
|
|
2
|
-
|
|
3
|
-
Let's break down the two simplest yet most vital directives in the library: b-if and b-text. These are the "workhorses" that handle visibility and data display without you having to write manual DOM manipulation code.
|
|
4
|
-
|
|
5
|
-
## Conditional Directives `b-if` & `b-show`
|
|
6
|
-
|
|
7
|
-
Directives like `b-if`, `b-text`, and `b-for` are the "Instructions" that bridge the gap between your JavaScript state and the DOM. Without them, your state would just be numbers and strings sitting in memory with no way to manifest on the screen.
|
|
8
|
-
|
|
9
|
-
### 1. Conditional Visibility (`b-show`)
|
|
10
|
-
|
|
11
|
-
The `b-show` directive is used to show or hide elements based on a truthy or falsy value in your state.
|
|
12
|
-
|
|
13
|
-
- **How it works**: During the `render()` cycle, the library executes `safeEval(b.expr, { state, self: b.node })`.
|
|
14
|
-
|
|
15
|
-
- **The Implementation**:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```js
|
|
19
|
-
if (b.type === 'b-show') b.node.style.display = safeEval(b.expr, { state, self: b.node }) ? '' : 'none';
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
- LegoDOM does not physically remove the element from the DOM (which is expensive), this library uses `display: none`.
|
|
23
|
-
|
|
24
|
-
- This means **b-show** is incredibly fast because the browser doesn't have to recalculate the entire DOM tree.
|
|
25
|
-
|
|
26
|
-
- It however also means the element still exists in memory and its `mounted` hook remains active even when hidden.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
### 2. Alternating Visibility (`b-if`)
|
|
30
|
-
|
|
31
|
-
#### 1. The "Place in Line" Problem
|
|
32
|
-
|
|
33
|
-
When an element has `b-if="false"`, we want it to disappear. If we simply remove it (`node.remove()`), the browser "forgets" where that element was supposed to live.
|
|
34
|
-
|
|
35
|
-
- **The Risk:** If that element was originally between a `<h1>` and a `<footer>`, and the condition turns `true` later, how does the library know exactly where to put it back?
|
|
36
|
-
|
|
37
|
-
- **The Solution:** We leave a "bookmark" or an **Anchor** (a tiny, invisible `Comment` node) exactly where the element used to be.
|
|
38
|
-
|
|
39
|
-
```js
|
|
40
|
-
if (node.hasAttribute('b-if')) {
|
|
41
|
-
const expr = node.getAttribute('b-if');
|
|
42
|
-
// Create an anchor point to keep track of where the element belongs in the DOM
|
|
43
|
-
const anchor = document.createComment(`b-if: ${expr}`);
|
|
44
|
-
const data = getPrivateData(node);
|
|
45
|
-
data.anchor = anchor;
|
|
46
|
-
bindings.push({ type: 'b-if', node, anchor, expr });
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
### 2. Why use a Comment Node?
|
|
52
|
-
|
|
53
|
-
We use `document.createComment()` because:
|
|
54
|
-
|
|
55
|
-
- It is a valid DOM node that occupies a specific position in the `childNodes` list.
|
|
56
|
-
|
|
57
|
-
- It is completely invisible to the user and doesn't affect CSS layouts or accessibility.
|
|
58
|
-
|
|
59
|
-
- It acts as a permanent reference point for the `replaceChild` operation.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### 3. Why store it in `getPrivateData`?
|
|
63
|
-
|
|
64
|
-
LegoDOM was designed in a way that the `render()` function runs every time state changes.
|
|
65
|
-
|
|
66
|
-
- **Consistency:** By storing the anchor in the element's `privateData` (via `WeakMap`), we ensure that the same specific element is always paired with the same specific anchor.
|
|
67
|
-
|
|
68
|
-
- **The Swap Logic:**
|
|
69
|
-
|
|
70
|
-
- **Condition becomes `false`:** We find the element in the DOM and replace it with its stored anchor: `parent.replaceChild(anchor, node)`.
|
|
71
|
-
|
|
72
|
-
- **Condition becomes `true`:** We find the anchor in the DOM and replace it with the element: `parent.replaceChild(node, anchor)`.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
### 4. Memory Safety
|
|
76
|
-
|
|
77
|
-
By using `getPrivateData` (which is a `WeakMap`), we ensure that if the component is destroyed, the reference to the `anchor` is garbage collected. If we didn't store it here, we would have to scan the entire DOM or maintain complex external maps to find where to re-insert hidden elements, which would be a massive performance hit.
|
|
78
|
-
|
|
79
|
-
**In short:** The anchor is the "Reserved Seat" sign at a theater. `getPrivateData` is the list that remembers which seat belongs to which person while they are out in the lobby.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## Raw HTML Injection (`b-html`)
|
|
83
|
-
|
|
84
|
-
LegoDOM is secure by default: all text interpolation is escaped. If your data contains `<b>Bold</b>`, it detects it as text, not HTML.
|
|
85
|
-
|
|
86
|
-
`b-html` is the **only** hatch to render raw HTML.
|
|
87
|
-
|
|
88
|
-
```javascript
|
|
89
|
-
if (b.type === 'b-html') {
|
|
90
|
-
// SECURITY CRITICAL: This is the only place we set innerHTML directly.
|
|
91
|
-
b.node.innerHTML = safeEval(b.expr, { state, self: b.node });
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Why separate directive?**
|
|
96
|
-
By forcing you to use `b-html="..."`, we make it obvious during code reviews that "This part is dangerous." It prevents accidental XSS where a developer thought `{{ content }}` would render HTML.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
## Simple Text Interpolation (`b-text`)
|
|
101
|
-
|
|
102
|
-
While you can use <code v-pre>{{mustaches}}</code>, `b-text` is the "cleaner" way to bind the entire content of an element to a single variable.
|
|
103
|
-
|
|
104
|
-
- **The Logic**: It uses the `resolve()` helper to walk through your state object based on a string path.
|
|
105
|
-
|
|
106
|
-
- Example: `<span b-text="user.profile.name"></span>`.
|
|
107
|
-
|
|
108
|
-
- **The Implementation**:
|
|
109
|
-
|
|
110
|
-
```js
|
|
111
|
-
if (b.type === 'b-text') b.node.textContent = escapeHTML(resolve(b.path, state));
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
- **Security**: Note the use of `escapeHTML()`. This is a critical security feature that prevents **XSS (Cross-Site Scripting) attacks** by turning characters like `<` into `<`, ensuring that user-provided data cannot execute malicious scripts in your app.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
### 3. Efficiency in the Render Loop
|
|
118
|
-
|
|
119
|
-
Because both of these were "mapped" during the `scanForBindings` phase (Topic 11), the `render` engine holds a direct reference to the DOM `node`.
|
|
120
|
-
|
|
121
|
-
- It doesn't have to look for the element by ID or class.
|
|
122
|
-
|
|
123
|
-
- It just goes: "Variable X changed -> Go to memory address for Node Y -> Update `.style.display` or `.textContent`".
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
## Iterative Directive: `b-for` & The Pool
|
|
127
|
-
|
|
128
|
-
The library looks for the pattern `item in list` (e.g., `user in users`).
|
|
129
|
-
|
|
130
|
-
- **Capture**: During the scanning phase, it saves a deep clone of the `b-for` element itself as the "template node" (using `node.cloneNode(true)`) and then empties the element so it can be filled dynamically. This approach handles both element children and text-only content correctly.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
### The Concept of "The Pool" (`forPools`)
|
|
134
|
-
|
|
135
|
-
To prevent "DOM Thrashing" (constantly creating and deleting elements), the library uses a `WeakMap` called `forPools`.
|
|
136
|
-
|
|
137
|
-
- **The Cache**: Each `b-for` node has its own `Map` inside the pool.
|
|
138
|
-
|
|
139
|
-
- **The Key**: It identifies each item in your array. If the item is an object, it assigns a hidden `__id`. If it's a primitive (like a string), it combines the index and the value.
|
|
140
|
-
|
|
141
|
-
- **Why?**: If you re-order a list of 100 items, the library doesn't create 100 new elements. it finds the existing elements in the "pool" by their key and simply moves them to the new position.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
### 3. The Local Scope Injection
|
|
145
|
-
|
|
146
|
-
This is a brilliant piece of JavaScript engineering. When rendering a list item, the library needs the item (e.g., `user`) to be available, but it also needs the parent component's data to be available.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
```js
|
|
150
|
-
const localScope = Object.assign(Object.create(state), { [b.itemName]: item });
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
- **Prototype Inheritance**: It creates a new object where the **prototype** is the component's state (`_studs`).
|
|
155
|
-
|
|
156
|
-
- **The Result**: Inside the loop, if you reference <code v-pre>{{user.name}}</code>, it finds it on the `localScope`. If you reference <code v-pre>{{globalTitle}}</code> (defined in the parent), it doesn't find it on `localScope`, so it automatically looks up the prototype chain to find it in the parent `state`.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
### 4. Updating and Pruning
|
|
160
|
-
|
|
161
|
-
- **Update**: For every item in the current array, it calls `updateNodeBindings(child, localScope)` to fill in the mustaches for that specific row.
|
|
162
|
-
|
|
163
|
-
- **Surgical Sync**: It specifically looks for `b-sync` inputs inside the loop to ensure two-way binding works correctly for individual list items.
|
|
164
|
-
|
|
165
|
-
- **Prune**: After the loop finishes, any element left in the "pool" that wasn't used in the current render (meaning it was deleted from your data array) is physically removed from the DOM.
|
|
166
|
-
|
|
167
|
-
## Directive: `b-sync` (Two-Way Binding)
|
|
168
|
-
|
|
169
|
-
The `b-sync` directive is designed to keep an `<input>`, `<textarea>`, or `<select>` element in perfect synchronization with a specific variable in your state.
|
|
170
|
-
|
|
171
|
-
```js
|
|
172
|
-
if (child.hasAttribute('b-sync')) {
|
|
173
|
-
const prop = child.getAttribute('b-sync');
|
|
174
|
-
const updateState = () => {
|
|
175
|
-
let target, last;
|
|
176
|
-
if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
|
|
177
|
-
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
|
|
178
|
-
const item = list[loopCtx.index];
|
|
179
|
-
if (!item) return;
|
|
180
|
-
const subPath = prop.split('.').slice(1);
|
|
181
|
-
last = subPath.pop();
|
|
182
|
-
target = subPath.reduce((o, k) => o[k], item);
|
|
183
|
-
} else {
|
|
184
|
-
const keys = prop.split('.');
|
|
185
|
-
last = keys.pop();
|
|
186
|
-
target = keys.reduce((o, k) => o[k], state);
|
|
187
|
-
}
|
|
188
|
-
const newVal = child.type === 'checkbox' ? child.checked : child.value;
|
|
189
|
-
if (target && target[last] !== newVal) target[last] = newVal;
|
|
190
|
-
};
|
|
191
|
-
child.addEventListener('input', updateState);
|
|
192
|
-
child.addEventListener('change', updateState);
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### 1. The Setup: Listening for Changes
|
|
197
|
-
|
|
198
|
-
When the `scanForBindings` function encounters a `b-sync="somePath"` attribute, it doesn't just record it for rendering; it attaches an **event listener** to the element.
|
|
199
|
-
|
|
200
|
-
- **The Event**: It listens for the `input` event (which fires every time a character is typed).
|
|
201
|
-
|
|
202
|
-
- **The Logic**: When the event fires, the library captures the `event.target.value`.
|
|
203
|
-
|
|
204
|
-
- **The Update**: It uses the internal `set()` helper to reach into your component's `_studs` and update the value at the path you specified (e.g., `user.name`).
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
### 2. The Implementation: Multi-Type Support
|
|
208
|
-
|
|
209
|
-
The library is smart enough to handle different types of inputs automatically:
|
|
210
|
-
|
|
211
|
-
- **Checkboxes**: It looks at `.checked` instead of `.value`.
|
|
212
|
-
|
|
213
|
-
- **Numbers**: If the input `type` is "number" or "range", it automatically converts the string from the DOM into a real JavaScript `Number` before saving it to your state.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
### 3. Preventing the "Echo" Effect
|
|
217
|
-
|
|
218
|
-
A common problem in two-way binding is the "Infinite Update Loop":
|
|
219
|
-
|
|
220
|
-
1. You type "A".
|
|
221
|
-
|
|
222
|
-
2. `b-sync` updates the state to "A".
|
|
223
|
-
|
|
224
|
-
3. The state change triggers a `render()`.
|
|
225
|
-
|
|
226
|
-
4. `render()` updates the input value to "A".
|
|
227
|
-
|
|
228
|
-
5. The cursor jumps to the end of the input or triggers another event.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
**How Lego solves it**: During the `render()` phase for a `b-sync` binding, the library checks if the element is currently the `document.activeElement` (the thing you are typing in). If it is, and the value hasn't changed from what's already there, it skips the update to avoid disturbing your typing flow.
|
|
232
|
-
|
|
233
|
-
### 4. The `b-sync` inside `b-for` loops
|
|
234
|
-
|
|
235
|
-
As mentioned in Topic 14, `b-sync` works inside loops. If you have a list of inputs generated by a `b-for`, each input is synced to its specific item in the array. The library uses the `localScope` (with its prototype chain) to ensure that typing in the 3rd input only updates the 3rd item in your data list.
|
|
236
|
-
|
|
237
|
-
----------
|
|
238
|
-
|
|
239
|
-
**Summary**: `b-if` manages visibility via CSS, and `b-text` manages safe text updates via property resolution.
|
|
240
|
-
|
|
241
|
-
`b-for` is a "Reconciliation Engine". It uses a memory pool to recycle DOM nodes and clever prototype inheritance to give each row access to both its own data and the parent's data.
|
|
242
|
-
|
|
243
|
-
`b-sync` automates the "Boilerplate" of web development. You no longer have to write `onchange` handlers for every input; you just name the variable, and the library handles the rest.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Lights, Camera, Action
|
|
2
|
-
|
|
3
|
-
This is how LegoDOM makes your components interactive by connecting DOM events (clicks, keypresses, submits) directly to the methods you wrote in
|
|
4
|
-
your **SFC** (Single File Component) autodiscovered **.lego** file, `<template />` tag or Lego.define call.
|
|
5
|
-
|
|
6
|
-
## Event Handling (The `@event` Syntax)
|
|
7
|
-
|
|
8
|
-
The library uses a shorthand syntax for event listeners: `@event-name="methodName"`. For example, `@click="increment"` or `@submit="saveUser"`.
|
|
9
|
-
|
|
10
|
-
### 1. The `bind(container, el)` Phase
|
|
11
|
-
|
|
12
|
-
During the `snap()` process, after the Shadow DOM is attached, the library calls `bind(container, el)`.
|
|
13
|
-
|
|
14
|
-
- **Scanning for Attributes**: It looks through all elements in the Shadow DOM for any attribute starting with `@`.
|
|
15
|
-
|
|
16
|
-
- **The Match**: If it finds `@click="toggle"`, it identifies `click` as the event and `toggle` as the function name to look for in `_studs`.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### 2. Context Preservation (`.bind`)
|
|
20
|
-
|
|
21
|
-
This is a critical "under the hood" step. When you click a button, the browser usually sets the keyword `this` to the button itself. However, in Lego, you want `this` to be your **reactive state**.
|
|
22
|
-
|
|
23
|
-
- **The Implementation**:
|
|
24
|
-
|
|
25
|
-
```js
|
|
26
|
-
const handler = state[methodName].bind(state);
|
|
27
|
-
node.addEventListener(eventName, handler);
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
- By using `.bind(state)`, the library ensures that when your `toggle()` function runs, `this.show = true` actually updates the proxy state, not the HTML button.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
### 3. Argument Support
|
|
34
|
-
|
|
35
|
-
The library is flexible with how you call these methods:
|
|
36
|
-
|
|
37
|
-
- **Standard Call**: `@click="doSomething"` passes the native `Event` object as the first argument.
|
|
38
|
-
|
|
39
|
-
- **Parameterized Call**: `@click="deleteItem(5)"`. The library uses a regex to check if there are parentheses. If it finds them, it parses the arguments (like `5`) and passes them to your function.
|
|
40
|
-
|
|
41
|
-
- **The "Native" `event`**: If you write `@click="move(event, 10)"`, the library injects the native browser event object into that specific slot. (Note: use `event`, not `$event`).
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
### 4. Event Modifiers
|
|
45
|
-
|
|
46
|
-
Lego includes built-in modifiers to handle common web patterns without writing extra JS:
|
|
47
|
-
|
|
48
|
-
- **`.prevent`**: Automatically calls `event.preventDefault()`. Great for `@submit.prevent="save"`.
|
|
49
|
-
|
|
50
|
-
- **`.stop`**: Calls `event.stopPropagation()` to stop the event from bubbling up to parent elements.
|
|
51
|
-
|
|
52
|
-
- **`.enter`**: Only triggers the method if the "Enter" key was pressed (perfect for search bars).
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
----------
|
|
56
|
-
|
|
57
|
-
**Summary**: The `@` syntax automates `addEventListener`, ensures the correct `this` context for your methods, and provides powerful modifiers to keep your component logic clean.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Where do you want to go?
|
|
2
|
-
|
|
3
|
-
The true power of the Lego router isn't just changing the URL; it's the **targeted DOM injection** that allows you to swap _any_ part of the page with _any_ component, without writing a single line of `fetch` or `innerHTML` logic.
|
|
4
|
-
|
|
5
|
-
## The "Surgical" Philosophy
|
|
6
|
-
|
|
7
|
-
Most SPAs use a **Replacer Strategy**:
|
|
8
|
-
`URL Change -> Match Route -> Destroy App -> Rebuild App with new Page.`
|
|
9
|
-
|
|
10
|
-
LegoDOM uses a **Surgical Strategy**:
|
|
11
|
-
`URL Change -> Match Route -> Find Targets (#sidebar, #main) -> Modify ONLY those nodes.`
|
|
12
|
-
|
|
13
|
-
### The Implementation
|
|
14
|
-
|
|
15
|
-
The core function is `_go` (exposed as `$go`). It doesn't just look for a `<router-outlet>`. It accepts a list of targets.
|
|
16
|
-
|
|
17
|
-
```javascript
|
|
18
|
-
/* main.js (simplified) */
|
|
19
|
-
const _go = (path, ...targets) => {
|
|
20
|
-
// 1. Update History API
|
|
21
|
-
history.pushState({ legoTargets: targets }, "", path);
|
|
22
|
-
|
|
23
|
-
// 2. Find the component for this route
|
|
24
|
-
const route = routes.find(r => r.path === path);
|
|
25
|
-
const template = registry[route.tagName];
|
|
26
|
-
|
|
27
|
-
// 3. Surgical Swap
|
|
28
|
-
targets.forEach(selector => {
|
|
29
|
-
const el = document.querySelector(selector);
|
|
30
|
-
// CRITICAL: We don't touch the parent, we only replace children.
|
|
31
|
-
// This preserves the element's own state (scroll, attributes).
|
|
32
|
-
el.replaceChildren(template.cloneNode(true));
|
|
33
|
-
|
|
34
|
-
// 4. Trigger Snap (Reactivity & Lifecycle) on new content
|
|
35
|
-
snap(el);
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Why this matters
|
|
41
|
-
|
|
42
|
-
This architecture enables **Persistent Shells**. You can have a sidebar that plays music or holds chat state, while the main content navigates freely. Traditional routers usually require complex "Layout Components" to achieve this. LegoDOM does it by simply *not touching the sidebar*.
|
|
43
|
-
|
|
44
|
-
## Intelligent Defaults
|
|
45
|
-
|
|
46
|
-
While surgical routing is powerful, sometimes you just want standard navigation.
|
|
47
|
-
LegoDOM checks for a default `<lego-router>` element if no targets are specified.
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
/* main.js */
|
|
51
|
-
const resolveTargets = (query) => {
|
|
52
|
-
if (!query) return [document.querySelector('lego-router')];
|
|
53
|
-
// ...
|
|
54
|
-
};
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
This hybrid approach gives you the best of both worlds: Rapid prototyping (defaults) and App-like fidelity (surgical targets).
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Lego and the Brain
|
|
2
|
-
In Lego, the code is designed to allow a component in the footer to talk to a component in the header without them ever being "parents" or "children" of each other.
|
|
3
|
-
|
|
4
|
-
## Global State (`Lego.globals`) & The Observer Pattern
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
### 1. The "Why": Decoupling the Hierarchy
|
|
9
|
-
|
|
10
|
-
In most frameworks, data flows down like a waterfall. If a deeply nested component needs a piece of data, every parent above it must "pass it down." The code in `main.js` avoids this by creating a centralized, reactive hub.
|
|
11
|
-
|
|
12
|
-
### 2. The Implementation: "The Body is the Root"
|
|
13
|
-
|
|
14
|
-
When you define `Lego.globals`, the library doesn't just store your object. It wraps it in the same `reactive()` proxy, but binds it to `document.body`.
|
|
15
|
-
|
|
16
|
-
- **The Code**: `Globals = reactive(userState, document.body)`.
|
|
17
|
-
|
|
18
|
-
- **The Effect**: This means the entire `<body>` is technically the "component" for global state.
|
|
19
|
-
|
|
20
|
-
### 3. The `$global` Dependency Check (Smart Broadcast)
|
|
21
|
-
|
|
22
|
-
The library uses a specific optimization to avoid re-rendering the whole world.
|
|
23
|
-
|
|
24
|
-
- **Depenedency Tracking**: During `scanForBindings`, if the parser sees a variable that looks global (e.g. `[[ global.user ]]`), it marks that specific component with a `hasGlobalDependency` flag.
|
|
25
|
-
|
|
26
|
-
- **The Broadcast Loop**: When you change a global (e.g., `Lego.globals.theme = 'dark'`), the Proxy's `set` trap fires on `document.body`. The `render` function sees this is a global update and iterates through **activeComponents**, checking which ones have the flag. Only those components re-render.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
### 4. Why `Object.defineProperty` is avoided for Globals
|
|
30
|
-
|
|
31
|
-
The library sticks to **Proxy** for globals because it allows for **dynamic property addition**.
|
|
32
|
-
|
|
33
|
-
- In older libraries, you had to declare all your global variables upfront.
|
|
34
|
-
|
|
35
|
-
- Because Lego uses a Proxy, you can do `Lego.globals.newVar = 'surprise'` at runtime, and the library will immediately catch that "set" operation and notify all components, even though `newVar` didn't exist when the app started.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
### 5. The `$go` and Globals Synergy
|
|
39
|
-
|
|
40
|
-
This is where the code becomes a "system." When you use `$go` to swap a component (Topic 17), the new component is born, it scans its HTML, sees a `$user` variable, and "subscribes" to the global state.
|
|
41
|
-
|
|
42
|
-
- This allows for **Persistent Identity**: The user's name stays in the header because the header is subscribed to `Lego.globals.user`, even as the rest of the page is being torn down and rebuilt by the router.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
----------
|
|
46
|
-
|
|
47
|
-
**Summary**: The code uses a "Centralized Proxy" to bypass the DOM hierarchy, ensuring that data is shared via subscription rather than inheritance.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Mama "We Made It!"
|
|
2
|
-
Everything we’ve discussed i.e. the Scanner, the Registry, the Router, and the Snap etc. "Everything" stays dormant until `Lego.init()` is called. This function is not just a starter; it’s an orchestrator that synchronizes the JavaScript environment with the existing HTML on the page.
|
|
3
|
-
|
|
4
|
-
## The Lego Initializer
|
|
5
|
-
|
|
6
|
-
The code for `init()` is deceptively small because its primary job is to flip the switches on the systems we've already built.
|
|
7
|
-
|
|
8
|
-
### 1. Bootstrapping the Watchdog
|
|
9
|
-
|
|
10
|
-
The core of the initialization is setting up the `MutationObserver` we discussed in Topic 7.
|
|
11
|
-
|
|
12
|
-
- **The Target**: It targets `document.body`.
|
|
13
|
-
|
|
14
|
-
- **The Configuration**: It uses `{ childList: true, subtree: true }`.
|
|
15
|
-
|
|
16
|
-
- **The Why**: By starting the observer _before_ the first render, the library ensures that any elements it creates during the initial startup are also caught and "snapped" into life.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### 3. The "Initial Snap" (The Recursive Wake-up)
|
|
20
|
-
|
|
21
|
-
Once the observer is live, the code calls `snap(document.body)`.
|
|
22
|
-
|
|
23
|
-
- **The Why**: The `MutationObserver` only sees _new_ changes. It cannot see the HTML that was already there when the page loaded.
|
|
24
|
-
|
|
25
|
-
- **The Logic**: By manually calling `snap` on the body, the library recursively walks through your entire server-rendered HTML. It finds every custom tag (e.g., `<user-card>`) and "upgrades" them into components.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
### 4. Activating the Global Listener
|
|
29
|
-
|
|
30
|
-
This is where the **Router Hijack** (Topic 17) is installed.
|
|
31
|
-
|
|
32
|
-
- The code adds a single click listener to the `window`.
|
|
33
|
-
|
|
34
|
-
- **The Why**: Instead of attaching listeners to every individual `<a>` tag (which would be slow and break when new links are added), it uses **Event Delegation**. It waits for clicks to bubble up to the window, checks if they are `b-link` clicks, and then decides whether to trigger the router.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
### 5. The First Route Check
|
|
38
|
-
|
|
39
|
-
Finally, `init()` calls `router()` manually for the first time.
|
|
40
|
-
|
|
41
|
-
- **The Why**: If a user navigates directly to `mysite.com/dashboard`, the browser loads the page, but the JavaScript needs to know which component to put into the `router-view` immediately. This manual call ensures the UI matches the URL on the very first frame.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
----------
|
|
45
|
-
|
|
46
|
-
**The Architecture "Why"**: Lego is designed to be **Zero-Config**. By putting all these steps into `init()`, the developer only has to care about one thing: "When is my DOM ready?" The code handles the complex timing of making sure the Watchdog is looking while the Router is switching and the Snap is initializing.
|
|
47
|
-
|
|
48
|
-
**Summary**: `Lego.init()` is the bridge. It turns a static document into a reactive application by starting the observer, snapping the existing HTML, and hijacking the navigation.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Architectural Deep Dive
|
|
2
|
-
|
|
3
|
-
Welcome to the internal documentation of LegoDOM.
|
|
4
|
-
|
|
5
|
-
This isn't a "how to use" guide. This is a **"how it works"** guide. I believe that understanding the soul of LegoDOM should/could make you a better contributor.
|
|
6
|
-
|
|
7
|
-
## The Philosophy
|
|
8
|
-
**"The Platform is the Runtime."**
|
|
9
|
-
We avoid compilers, transpilers, and VDOMs. We use:
|
|
10
|
-
- **Proxies** for state.
|
|
11
|
-
- **TreeWalkers** for scanning.
|
|
12
|
-
- **Regex** for parsing.
|
|
13
|
-
- **MutationObservers** for efficiency.
|
|
14
|
-
|
|
15
|
-
## The Journey
|
|
16
|
-
Follow the path of a component from HTML string to Pixel:
|
|
17
|
-
|
|
18
|
-
1. [**Init**](./06-init) - How the library wakes up.
|
|
19
|
-
2. [**Scanner**](./11-scanner) - How we find holes in your HTML (Regex vs AST).
|
|
20
|
-
3. [**Studs**](./10-studs) - The Reactivity Engine (Proxies).
|
|
21
|
-
4. [**Render**](./12-render) - The "Loop of Truth" & Security.
|
|
22
|
-
5. [**Router**](./15-router) - The "Surgical" Update philosophy.
|
|
23
|
-
|
|
24
|
-
Dive in.
|
package/docs/examples/form.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Form Example
|
|
2
|
-
|
|
3
|
-
Handling forms in Lego.
|
|
4
|
-
|
|
5
|
-
## Live Demo
|
|
6
|
-
|
|
7
|
-
```html
|
|
8
|
-
<template b-id="login-form">
|
|
9
|
-
<form @submit.prevent="login()">
|
|
10
|
-
<div>
|
|
11
|
-
<label>Email:</label>
|
|
12
|
-
<input type="email" b-sync="email">
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<div>
|
|
16
|
-
<label>Password:</label>
|
|
17
|
-
<input type="password" b-sync="password">
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<p b-show="error" style="color: red">[[ error ]]</p>
|
|
21
|
-
|
|
22
|
-
<button type="submit">Login</button>
|
|
23
|
-
</form>
|
|
24
|
-
</template>
|
|
25
|
-
|
|
26
|
-
<script>
|
|
27
|
-
Lego.define('login-form', {
|
|
28
|
-
email: '',
|
|
29
|
-
password: '',
|
|
30
|
-
error: '',
|
|
31
|
-
|
|
32
|
-
login() {
|
|
33
|
-
if (!this.email || !this.password) {
|
|
34
|
-
this.error = 'Please fill in all fields';
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
alert(`Logging in as ${this.email}`);
|
|
38
|
-
this.error = '';
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
</script>
|
|
42
|
-
```
|