lego-dom 0.0.9 → 1.0.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 +44 -0
- package/README.md +1 -0
- package/{go.html → cdn.html} +33 -26
- package/docs/.vitepress/config.js +39 -1
- package/docs/api/directives.md +3 -3
- package/docs/api/index.md +1 -1
- package/docs/contributing/01-welcome.md +36 -0
- package/docs/contributing/02-registry.md +99 -0
- package/docs/contributing/03-batcher.md +110 -0
- package/docs/contributing/04-reactivity.md +87 -0
- package/docs/contributing/05-caching.md +59 -0
- package/docs/contributing/06-init.md +125 -0
- package/docs/contributing/07-observer.md +69 -0
- package/docs/contributing/08-snap.md +126 -0
- package/docs/contributing/09-diffing.md +69 -0
- package/docs/contributing/10-studs.md +76 -0
- package/docs/contributing/11-scanner.md +104 -0
- package/docs/contributing/12-render.md +116 -0
- package/docs/contributing/13-directives.md +225 -0
- package/docs/contributing/14-events.md +57 -0
- package/docs/contributing/15-router.md +9 -0
- package/docs/contributing/16-state.md +48 -0
- package/docs/contributing/17-legodom.md +55 -0
- package/docs/contributing/index.md +5 -0
- package/docs/examples/form.md +1 -1
- package/docs/examples/index.md +1 -1
- package/docs/examples/routing.md +4 -4
- package/docs/examples/todo-app.md +1 -1
- package/docs/guide/cdn-usage.md +8 -0
- package/docs/guide/components.md +33 -15
- package/docs/guide/directives.md +22 -22
- package/docs/guide/getting-started.md +35 -10
- package/docs/guide/index.md +3 -3
- package/docs/guide/quick-start.md +4 -1
- package/docs/guide/reactivity.md +22 -1
- package/docs/guide/routing.md +189 -289
- package/docs/guide/sfc.md +1 -1
- package/docs/guide/templating.md +2 -2
- package/docs/index.md +41 -7
- package/docs/router/basic-routing.md +103 -0
- package/docs/router/cold-entry.md +91 -0
- package/docs/router/history.md +69 -0
- package/docs/router/index.md +73 -0
- package/docs/router/resolver.md +74 -0
- package/docs/router/surgical-swaps.md +134 -0
- package/examples/vite-app/index.html +4 -12
- package/examples/vite-app/package.json +4 -2
- package/examples/vite-app/src/app.css +3 -0
- package/examples/vite-app/src/app.js +29 -0
- package/examples/vite-app/src/components/app-navbar.lego +34 -0
- package/examples/vite-app/src/components/customers/customer-details.lego +24 -0
- package/examples/vite-app/src/components/customers/customer-orders.lego +21 -0
- package/examples/vite-app/src/components/customers/order-list.lego +55 -0
- package/examples/vite-app/src/components/greeting-card.lego +1 -1
- package/examples/vite-app/src/components/sample-component.lego +15 -15
- package/examples/vite-app/src/components/shells/customers-shell.lego +21 -0
- package/examples/vite-app/src/components/todo-list.lego +12 -15
- package/examples/vite-app/src/components/widgets/user-card.lego +27 -0
- package/examples/vite-app/vite.config.js +5 -1
- package/main.js +247 -56
- package/package.json +1 -1
- package/parse-lego.js +17 -8
- package/{main.test.js → tests/main.test.js} +34 -17
- package/tests/parse-lego.test.js +65 -0
- package/vite-plugin.js +60 -22
- package/docs/.vitepress/dist/404.html +0 -22
- package/docs/.vitepress/dist/api/define.html +0 -35
- package/docs/.vitepress/dist/api/directives.html +0 -32
- package/docs/.vitepress/dist/api/globals.html +0 -27
- package/docs/.vitepress/dist/api/index.html +0 -25
- package/docs/.vitepress/dist/api/lifecycle.html +0 -38
- package/docs/.vitepress/dist/api/route.html +0 -34
- package/docs/.vitepress/dist/api/vite-plugin.html +0 -37
- package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.js +0 -11
- package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.js +0 -8
- package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.js +0 -3
- package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.js +0 -1
- package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.js +0 -14
- package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.js +0 -10
- package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.js +0 -13
- package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.lean.js +0 -1
- package/docs/.vitepress/dist/assets/app.BfblNDJy.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.Crdp7-Zp.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.C18E44rY.js +0 -9
- package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +0 -19
- package/docs/.vitepress/dist/assets/chunks/theme.VX3itTW6.js +0 -2
- package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.js +0 -34
- package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.js +0 -28
- package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.js +0 -338
- package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.js +0 -13
- package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.js +0 -297
- package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.js +0 -182
- package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.js +0 -174
- package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.js +0 -1
- package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.js +0 -140
- package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.js +0 -107
- package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.js +0 -2
- package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.js +0 -304
- package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.js +0 -33
- package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.js +0 -135
- package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.js +0 -193
- package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.js +0 -187
- package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.js +0 -119
- package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.lean.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.M4_o26kF.js +0 -23
- package/docs/.vitepress/dist/assets/index.md.M4_o26kF.lean.js +0 -1
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/.vitepress/dist/assets/style.eycE2Jhw.css +0 -1
- package/docs/.vitepress/dist/examples/form.html +0 -58
- package/docs/.vitepress/dist/examples/index.html +0 -52
- package/docs/.vitepress/dist/examples/routing.html +0 -362
- package/docs/.vitepress/dist/examples/sfc-showcase.html +0 -37
- package/docs/.vitepress/dist/examples/todo-app.html +0 -321
- package/docs/.vitepress/dist/guide/cdn-usage.html +0 -206
- package/docs/.vitepress/dist/guide/components.html +0 -198
- package/docs/.vitepress/dist/guide/contributing.html +0 -25
- package/docs/.vitepress/dist/guide/directives.html +0 -164
- package/docs/.vitepress/dist/guide/getting-started.html +0 -131
- package/docs/.vitepress/dist/guide/index.html +0 -26
- package/docs/.vitepress/dist/guide/lifecycle.html +0 -328
- package/docs/.vitepress/dist/guide/quick-start.html +0 -57
- package/docs/.vitepress/dist/guide/reactivity.html +0 -159
- package/docs/.vitepress/dist/guide/routing.html +0 -217
- package/docs/.vitepress/dist/guide/sfc.html +0 -211
- package/docs/.vitepress/dist/guide/templating.html +0 -143
- package/docs/.vitepress/dist/hashmap.json +0 -1
- package/docs/.vitepress/dist/index.html +0 -47
- package/docs/.vitepress/dist/logo.svg +0 -38
- package/docs/.vitepress/dist/vp-icons.css +0 -1
- package/examples/vite-app/src/main.js +0 -11
- package/examples.js +0 -99
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# In the beginning - `Lego.init()`
|
|
2
|
+
|
|
3
|
+
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 components.
|
|
4
|
+
|
|
5
|
+
## The LegoDOM Entry Point
|
|
6
|
+
|
|
7
|
+
When your HTML finishes loading
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
if (typeof window !== 'undefined') {
|
|
11
|
+
document.addEventListener('DOMContentLoaded', Lego.init);
|
|
12
|
+
window.Lego = Lego;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LegoDOM executes `Lego.init()`. This is the orchestration phase where the library sets up its "eyes" (the observer) and connects the global state to the page.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
return {
|
|
21
|
+
init: () => {
|
|
22
|
+
document.querySelectorAll('template[b-id]').forEach(t => {
|
|
23
|
+
registry[t.getAttribute('b-id')] = t;
|
|
24
|
+
});
|
|
25
|
+
const observer = new MutationObserver(m => m.forEach(r => {
|
|
26
|
+
r.addedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && snap(n));
|
|
27
|
+
r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
|
|
28
|
+
}));
|
|
29
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
30
|
+
|
|
31
|
+
snap(document.body);
|
|
32
|
+
bind(document.body, { _studs: Lego.globals, _data: { bound: false } });
|
|
33
|
+
|
|
34
|
+
if (routes.length > 0) {
|
|
35
|
+
// Smart History: Restore surgical targets on Back button
|
|
36
|
+
window.addEventListener('popstate', (event) => {
|
|
37
|
+
const targets = event.state?.legoTargets || null;
|
|
38
|
+
_matchRoute(targets);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
document.addEventListener('click', e => {
|
|
42
|
+
const link = e.target.closest('a[b-link]');
|
|
43
|
+
if (link) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
const href = link.getAttribute('href');
|
|
46
|
+
const targetAttr = link.getAttribute('b-target');
|
|
47
|
+
const targets = targetAttr ? targetAttr.split(' ') : [];
|
|
48
|
+
|
|
49
|
+
// Execute navigation via $go logic
|
|
50
|
+
Lego.globals.$go(href, ...targets);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
_matchRoute();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
// ... other functions ...
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 1. Template Registration
|
|
62
|
+
|
|
63
|
+
The first thing `init()` does is look for blueprints you've already defined in your HTML.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
document.querySelectorAll('template[b-id]').forEach(t => {
|
|
67
|
+
registry[t.getAttribute('b-id')] = t;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It scans the entire document for any `<template>` tag that has a `b-id` attribute. It then adds these to the `registry` object we discussed in [Topic 2](/contributing/02-registry). This allows you to define components directly in your HTML file without writing a single line of JavaScript.
|
|
73
|
+
|
|
74
|
+
### 2. Setting up the "Eyes" (`MutationObserver`)
|
|
75
|
+
|
|
76
|
+
This is the most critical part of the initialization. The library creates a `MutationObserver`.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const observer = new MutationObserver(m => m.forEach(r => {
|
|
80
|
+
r.addedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && snap(n));
|
|
81
|
+
r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
|
|
82
|
+
}));
|
|
83
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **What it does**: It watches the `document.body` for any changes to the HTML structure.
|
|
87
|
+
|
|
88
|
+
- **The Config**: It observes `{ childList: true, subtree: true }`. This means it sees if an element is added to the body, or if an element is added deep inside another element.
|
|
89
|
+
|
|
90
|
+
- **The Reaction**:
|
|
91
|
+
|
|
92
|
+
- If a new node is **added**, it calls `snap(n)` (which we will cover soon) to turn that raw HTML into a living component.
|
|
93
|
+
|
|
94
|
+
- If a node is **removed**, it calls `unsnap(n)` to clean up memory and fire lifecycle hooks.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### 3. The "First Snap"
|
|
98
|
+
|
|
99
|
+
After setting up the observer, it calls `snap(document.body)`.
|
|
100
|
+
|
|
101
|
+
- **Why?** The observer only sees _new_ things being added. It doesn't see what was already there when the page loaded.
|
|
102
|
+
|
|
103
|
+
- By calling `snap` on the body, the library manually processes every custom component that was present in the initial HTML.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
### 4. Global Data Binding
|
|
107
|
+
|
|
108
|
+
Finally, it binds the `Lego.globals` state to the `document.body`.
|
|
109
|
+
|
|
110
|
+
JavaScript
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
bind(document.body, { _studs: Lego.globals, _data: { bound: false } });
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This is a clever move: it treats the entire website body as if it were a giant component. This allows you to use reactive data in your "Light DOM" (the regular HTML outside of components) by referencing values stored in `Lego.globals`.
|
|
118
|
+
|
|
119
|
+
### 5. Routing Initialization
|
|
120
|
+
|
|
121
|
+
If you have defined any routes using `Lego.route()`, the `init` function sets up global click listeners and history management so that links with the `b-link` attribute don't refresh the page but instead perform a "surgical" swap of content.
|
|
122
|
+
|
|
123
|
+
----------
|
|
124
|
+
|
|
125
|
+
**Summary of `init()`:** It finds templates, starts a "watchdog" for new elements, processes existing elements, and enables global data.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Big Brother is Watching
|
|
2
|
+
|
|
3
|
+
In Topic 6, we saw that Lego.init() sets up a "watchdog." Now, let's look at exactly how that watchdog functions. This is what makes the library feel "automatic"—you can inject HTML into the page using vanilla JavaScript, and Lego will instantly recognize it and bring it to life.
|
|
4
|
+
|
|
5
|
+
## Mutation Observer
|
|
6
|
+
|
|
7
|
+
The `MutationObserver` is a built-in browser API that provides the ability to watch for changes being made to the DOM tree. In this library, it acts as the "Event Loop" for component lifecycles.
|
|
8
|
+
|
|
9
|
+
### 1. The Configuration
|
|
10
|
+
|
|
11
|
+
The library observes the `document.body` with specific settings:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **`childList: true`**: Tells the observer to watch for elements being added or removed.
|
|
19
|
+
|
|
20
|
+
- **`subtree: true`**: This is vital. Without this, the library would only see things added directly to the `<body>`. With it, the library sees changes happening deep inside nested divs or other components.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### 2. Processing `addedNodes`
|
|
24
|
+
|
|
25
|
+
Whenever a new piece of HTML is injected (via `innerHTML`, `appendChild`, etc.), the observer receives a list of `addedNodes`.
|
|
26
|
+
|
|
27
|
+
- **Filter**: The library checks `n.nodeType === Node.ELEMENT_NODE` to ensure it is an **Element** (like a `<div>` or `<my-comp>`) and not just a fragment of text.
|
|
28
|
+
|
|
29
|
+
- **The Action**: It calls `snap(n)`. This triggers the entire initialization process: attaching the Shadow DOM, creating reactive state, and rendering the template.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### 3. Processing `removedNodes`
|
|
33
|
+
|
|
34
|
+
When an element is deleted from the page, the observer catches it in `removedNodes`.
|
|
35
|
+
|
|
36
|
+
- **Cleanup**: It calls `unsnap(n)`.
|
|
37
|
+
|
|
38
|
+
- **Lifecycle Hook**: `unsnap` checks the component's state for an `unmounted` function. This allows you to perform cleanup, like stopping timers or closing WebSocket connections, preventing memory leaks.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Why this is superior to manual initialization
|
|
42
|
+
In most frameworks, you have to tell the library when you’ve changed the page (e.g., calling `root.render()`). This library turns that upside down by using the browser's native **MutationObserver** to watch the DOM and react automatically.
|
|
43
|
+
Unlike some libraries where adding a button via `innerHTML` requires a manual "re-scan" or "re-bind" call, this setup makes LegoDOM **reactive to the DOM itself**: the moment a tag like `<user-card>` appears in the document, it is automatically detected and upgraded into a functional, living component.
|
|
44
|
+
|
|
45
|
+
### 1. The Strategy: "Observe Once, Act Everywhere"
|
|
46
|
+
|
|
47
|
+
The library sets up a single observer on `document.body`.
|
|
48
|
+
|
|
49
|
+
- **`childList: true`**: This tells the observer to watch for the addition or removal of direct children.
|
|
50
|
+
|
|
51
|
+
- **`subtree: true`**: This is the "secret sauce." It extends the observation to the entire DOM tree. If you have a deeply nested `div` and you inject a Lego component into it, the observer will see it.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
### 2. The Logic Loop: Added Nodes
|
|
55
|
+
|
|
56
|
+
When the observer detects changes, it provides a list of `MutationRecord` objects. The library loops through these records specifically looking for `addedNodes`.
|
|
57
|
+
|
|
58
|
+
- **Type Filtering**: It checks `n.nodeType === Node.ELEMENT_NODE`. This ensures the library ignores text changes or comments and only focuses on **Elements** (tags).
|
|
59
|
+
|
|
60
|
+
- **The Upgrade (Snapping)**: For every new element found, it calls `snap(n)`. This is why you can do `document.body.innerHTML += '<my-counter></my-counter>'` in the console, and the counter will immediately start working.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### 3. The Logic Loop: Removed Nodes
|
|
64
|
+
|
|
65
|
+
This is equally important for "garbage collection." When an element is deleted, it appears in `removedNodes`.
|
|
66
|
+
|
|
67
|
+
- **The `unsnap` Trigger**: The library calls `unsnap(n)`.
|
|
68
|
+
|
|
69
|
+
- **Lifecycle Cleanup**: Inside `unsnap`, the library looks for a developer-defined `unmounted` function. This is where you'd kill `setInterval` timers or close database connections. Without this, your app would suffer from "Memory Leaks"—processes that keep running even though the component is gone.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Let there be, and it was!
|
|
2
|
+
|
|
3
|
+
If LegoDOM were a robot it would probably say - snap() is the most complex function I have because it acts as the "Middleman" between the static DOM, the reactive state, and the Shadow DOM. It is the "constructor" that the ~~stingy~~ browser never gave anyone - LegoDOM.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Snap, snap, snap!
|
|
7
|
+
|
|
8
|
+
The `snap(el)` function is responsible for "upgrading" a standard HTML element. It is recursive, meaning if you snap a `<div>`, it will automatically look inside that `<div>` and snap every child as well.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const snap = (el) => {
|
|
12
|
+
if (!el || el.nodeType !== Node.ELEMENT_NODE) return;
|
|
13
|
+
const data = getPrivateData(el);
|
|
14
|
+
const name = el.tagName.toLowerCase();
|
|
15
|
+
const templateNode = registry[name];
|
|
16
|
+
|
|
17
|
+
if (templateNode && !data.snapped) {
|
|
18
|
+
data.snapped = true;
|
|
19
|
+
const tpl = templateNode.content.cloneNode(true);
|
|
20
|
+
const shadow = el.attachShadow({ mode: 'open' });
|
|
21
|
+
|
|
22
|
+
// TIER 1: Logic from Lego.define (SFC)
|
|
23
|
+
const scriptLogic = sfcLogic.get(name) || {};
|
|
24
|
+
|
|
25
|
+
// TIER 2: Logic from the <template b-data="..."> attribute
|
|
26
|
+
const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
|
|
27
|
+
|
|
28
|
+
// TIER 3: Logic from the <my-comp b-data="..."> tag
|
|
29
|
+
const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
|
|
30
|
+
|
|
31
|
+
// Priority: Script < Template < Instance
|
|
32
|
+
el._studs = reactive({
|
|
33
|
+
...scriptLogic,
|
|
34
|
+
...templateLogic,
|
|
35
|
+
...instanceLogic
|
|
36
|
+
}, el);
|
|
37
|
+
|
|
38
|
+
shadow.appendChild(tpl);
|
|
39
|
+
|
|
40
|
+
const style = shadow.querySelector('style');
|
|
41
|
+
if (style) {
|
|
42
|
+
style.textContent = style.textContent.replace(/\bself\b/g, ':host');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bind(shadow, el);
|
|
46
|
+
render(el);
|
|
47
|
+
|
|
48
|
+
if (typeof el._studs.mounted === 'function') {
|
|
49
|
+
try { el._studs.mounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in mounted <${name}>:`, e); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let provider = el.parentElement;
|
|
54
|
+
while (provider && !provider._studs) provider = provider.parentElement;
|
|
55
|
+
if (provider && provider._studs) bind(el, provider);
|
|
56
|
+
|
|
57
|
+
[...el.children].forEach(snap);
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 1. The Blueprint Lookup
|
|
62
|
+
|
|
63
|
+
When `snap(el)` runs, the first thing it does is determine if the element is a Lego component.
|
|
64
|
+
|
|
65
|
+
- It converts the tag name to lowercase (e.g., `<MY-COMP>` becomes `my-comp`).
|
|
66
|
+
|
|
67
|
+
- It checks the **`registry`** (which we filled in [Topic 2](./02-registry.md)) to see if a `<template>` exists for that name.
|
|
68
|
+
|
|
69
|
+
- It uses `getPrivateData(el).snapped` to ensure it never "snaps" the same element twice, preventing infinite loops.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### 2. Attaching the Shadow DOM
|
|
73
|
+
|
|
74
|
+
If a template is found, Lego creates a **Shadow DOM** for the element:
|
|
75
|
+
|
|
76
|
+
JavaScript
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
const shadow = el.attachShadow({ mode: 'open' });
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- **Encapsulation**: By using `attachShadow`, the component’s internal styles and HTML are shielded from the rest of the page.
|
|
84
|
+
|
|
85
|
+
- **Template Injection**: It clones the content of the template and appends it to this new Shadow Root.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### 3. CSS "self" Transformation
|
|
89
|
+
|
|
90
|
+
Lego includes a small but clever utility for styling:
|
|
91
|
+
|
|
92
|
+
JavaScript
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
style.textContent = style.textContent.replace(/\bself\b/g, ':host');
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- This allows you to write `self { color: red; }` in your template CSS.
|
|
100
|
+
|
|
101
|
+
- During the snap process, Lego converts the word `self` to the official Web Component selector `:host`, which targets the component itself.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### 4. Data Merging & Reactivity
|
|
105
|
+
|
|
106
|
+
This is where the component's state is born. The library merges data from three different sources (we will dive deeper into this "Tier System" in Topic 9) and wraps the result in the **`reactive()`** proxy.
|
|
107
|
+
|
|
108
|
+
- The resulting proxy is stored in `el._studs`. This is the "brain" of your component.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
### 5. The First Render and Lifecycle
|
|
112
|
+
|
|
113
|
+
Once the data is ready and the Shadow DOM is attached:
|
|
114
|
+
|
|
115
|
+
1. **`bind(shadow, el)`**: Connects event listeners (like `@click`) inside the Shadow DOM.
|
|
116
|
+
|
|
117
|
+
2. **`render(el)`**: Performs the initial pass to fill in <code v-pre>{{ variables }}</code> and handle `b-show/b-for` logic.
|
|
118
|
+
|
|
119
|
+
3. **`mounted()`**: If you defined a `mounted` function in your logic, Lego calls it now. This is your signal that the component is officially "alive" and visible on the page.
|
|
120
|
+
|
|
121
|
+
---------
|
|
122
|
+
|
|
123
|
+
**Crucial Logic Note:** At the very end of the function, `snap` calls itself on every child of the element: `[...el.children].forEach(snap)`. This ensures that if you have components nested inside components, they all "wake up" in a top-down order.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
**Summary**: `snap()` takes a raw tag, gives it a Shadow DOM "soul," injects its HTML blueprint, sets up its reactive "brain" (`_studs`), and triggers its first breath (`mounted`).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# He said, She said, They Said, Who Said?
|
|
2
|
+
|
|
3
|
+
This topic is about the "Power Struggle" inside the code. When a component is born, it needs to know what its data is. The library looks at three different places to find that data, and it has a strict hierarchy of who wins in a conflict.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Diffing, Merging, and Priorities (The Tier System)
|
|
7
|
+
|
|
8
|
+
Inside the `snap(el)` function, you will see a variable called `logic`. This isn't just a simple object; it is the result of a three-way merge. The code uses the spread operator `{ ... }` to layer these "Tiers."
|
|
9
|
+
|
|
10
|
+
### The Three Tiers of Authority
|
|
11
|
+
|
|
12
|
+
1. **Tier 1: The Global Definition (Lowest Priority)**
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
const baseLogic = sfcLogic.get(name) || {};
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This is the JavaScript object you provided when you called `Lego.define()`. It contains your default values and methods. It’s the "fallback" data. SFCs and .lego files fall under this tier.
|
|
20
|
+
|
|
21
|
+
2. **Tier 2: The Template Attributes (Middle Priority)**
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const templateLogic = parseJSObject(t.getAttribute('b-data')) || {};
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Lego looks at the `<template>` tag itself in your HTML. If you added a `b-data` attribute there, it overrides the global definition. This is useful for creating "variants" of a component template without writing new JavaScript.
|
|
30
|
+
|
|
31
|
+
3. **Tier 3: The Instance Attributes (Highest Priority)**
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const instanceLogic = parseJSObject(el.getAttribute('b-data')) || {};
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is the data attached to the specific tag on your page (e.g., `<user-profile b-data="{id: 42}">`). This is the "Final Word." If the instance says the ID is 42, it doesn't matter what the template or the global definition said.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### The Merge Order
|
|
42
|
+
|
|
43
|
+
The code combines them like this:
|
|
44
|
+
|
|
45
|
+
JavaScript
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
const mergedLogic = { ...baseLogic, ...templateLogic, ...instanceLogic };
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
In JavaScript, when you spread objects, the properties on the right overwrite properties on the left. So, Instance > Template > Definition.
|
|
53
|
+
|
|
54
|
+
### The `parseJSObject` Utility
|
|
55
|
+
|
|
56
|
+
To make this work, the library includes a helper called `parseJSObject`.
|
|
57
|
+
|
|
58
|
+
- **Why not `JSON.parse`?** JSON is strict (requires double quotes, no functions).
|
|
59
|
+
|
|
60
|
+
- **The Library's Way**: It uses `new Function('return ' + str)()`. This is a powerful (and dangerous) trick that allows you to write actual JavaScript inside your HTML attributes, including functions or arrays, which Lego then evaluates into a real object.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### The Edge Case: `b-data` as a "Ref"
|
|
64
|
+
|
|
65
|
+
If the `b-data` attribute starts with a `$`, like `b-data="$someGlobal"`, the library treats it as a pointer to a global variable instead of a raw object string. This allows for deep data sharing between the component and the outside world.
|
|
66
|
+
|
|
67
|
+
----------
|
|
68
|
+
|
|
69
|
+
**Summary**: A component's data is a "Cake" with three layers. The Global Logic is the bottom, the Template Logic is the middle, and the Instance Logic is the frosting. The "Frosting" (Instance) is what the user ultimately sees.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# State Management
|
|
2
|
+
|
|
3
|
+
Let's see how the "Cake" of data we merged in Topic 9 is actually brought to life. This is the moment a static object becomes a **Reactive State**, which the library stores in a property called `_studs`.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## The `reactive` State (`_studs`)
|
|
7
|
+
|
|
8
|
+
Once `snap()` has merged the data from the SFC (Tier 1), the Template (Tier 2), and the Instance (Tier 3), it doesn't just save that object to the element. It transforms it.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
//...rest of code
|
|
12
|
+
const tpl = templateNode.content.cloneNode(true);
|
|
13
|
+
const shadow = el.attachShadow({ mode: 'open' });
|
|
14
|
+
|
|
15
|
+
// TIER 1: Logic from Lego.define (SFC)
|
|
16
|
+
const scriptLogic = sfcLogic.get(name) || {};
|
|
17
|
+
|
|
18
|
+
// TIER 2: Logic from the <template b-data="..."> attribute
|
|
19
|
+
const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
|
|
20
|
+
|
|
21
|
+
// TIER 3: Logic from the <my-comp b-data="..."> tag
|
|
22
|
+
const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
|
|
23
|
+
|
|
24
|
+
// Priority: Script < Template < Instance
|
|
25
|
+
el._studs = reactive({
|
|
26
|
+
...scriptLogic,
|
|
27
|
+
...templateLogic,
|
|
28
|
+
...instanceLogic
|
|
29
|
+
}, el);
|
|
30
|
+
//... rest of code
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 1. Why the name `_studs`?
|
|
34
|
+
|
|
35
|
+
In the physical world, "studs" are the bumps on top of a Lego brick that allow it to connect to others. In this library:
|
|
36
|
+
|
|
37
|
+
- **`el._studs`** represents the connection point between your JavaScript logic and the DOM.
|
|
38
|
+
|
|
39
|
+
- It is the "source of truth" for the component. Every `{{variable}}` you write in your HTML is looking for a matching key inside `_studs`.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### 2. The Transformation
|
|
43
|
+
|
|
44
|
+
The library executes this line during the snap process:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
el._studs = reactive({ ...mergedLogic }, el);
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **The `reactive` call**: This wraps the merged object in the **Proxy** we discussed in Topic 4.
|
|
52
|
+
|
|
53
|
+
- **The `el` argument**: Crucially, the proxy is given a reference to the DOM element (`el`). This allows the Proxy's `set` trap to know exactly which component needs to be added to the `globalBatcher` when a property changes.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### 3. Contextual Binding (The `this` Keyword)
|
|
57
|
+
|
|
58
|
+
After the `_studs` proxy is created, the library ensures that your methods work correctly. When you define a method in your SFC, you expect `this` to point to your data.
|
|
59
|
+
|
|
60
|
+
- Inside `snap()`, lifecycle hooks are called like this: `el._studs.mounted.call(el._studs)`.
|
|
61
|
+
|
|
62
|
+
- By using `.call(el._studs)`, the library forces the execution context of your functions to be the reactive proxy.
|
|
63
|
+
|
|
64
|
+
- **Result**: When you write `this.count++` in your code, you are actually interacting with the **Proxy**, which triggers the `set` trap, which notifies the **Batcher**, which triggers the **Render**.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### 4. Visibility vs. Privacy
|
|
68
|
+
|
|
69
|
+
- **`_studs`**: This is attached directly to the DOM element. You can actually type `document.querySelector('my-component')._studs` in your browser console to see and even modify the live state of any component.
|
|
70
|
+
|
|
71
|
+
- **`privateData`**: Unlike `_studs`, the internal "housekeeping" (like whether the component has already snapped) is kept in a `WeakMap` called `privateData`, making it inaccessible to the outside world.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
----------
|
|
75
|
+
|
|
76
|
+
**Summary**: `_studs` is the reactive engine of your component. It is created by merging all data tiers into a Proxy that is uniquely linked to that specific DOM element.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Scanner
|
|
2
|
+
|
|
3
|
+
Now let's get into the "eyes" of the rendering engine. For LegoDOM to be able to update the DOM efficiently, it needs to know which specific parts of your HTML are static (never change) and which parts are dynamic (contain {{mustaches}} or b- directives).
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Scanning for Bindings (The `TreeWalker`)
|
|
7
|
+
|
|
8
|
+
In `main.js`, the `scanForBindings` function is called the very first time a component renders. Instead of re-scanning the DOM every time a variable changes, Lego scans **once**, creates a "map" of all the dynamic spots, and saves that map in the element's `privateData`.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const scanForBindings = (container) => {
|
|
12
|
+
const bindings = [];
|
|
13
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
14
|
+
let node;
|
|
15
|
+
while (node = walker.nextNode()) {
|
|
16
|
+
const isInsideBFor = (n) => {
|
|
17
|
+
let curr = n.parentNode;
|
|
18
|
+
while (curr && curr !== container) {
|
|
19
|
+
if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
|
|
20
|
+
if (curr.tagName && curr.tagName.includes('-') && registry[curr.tagName.toLowerCase()]) return true;
|
|
21
|
+
curr = curr.parentNode;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
25
|
+
if (isInsideBFor(node)) continue;
|
|
26
|
+
|
|
27
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
28
|
+
if (node.hasAttribute('b-show')) bindings.push({ type: 'b-show', node, expr: node.getAttribute('b-show') });
|
|
29
|
+
if (node.hasAttribute('b-for')) {
|
|
30
|
+
const match = node.getAttribute('b-for').match(/^\s*(\w+)\s+in\s+(.+)\s*$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
bindings.push({
|
|
33
|
+
type: 'b-for',
|
|
34
|
+
node,
|
|
35
|
+
itemName: match[1],
|
|
36
|
+
listName: match[2].trim(),
|
|
37
|
+
template: node.innerHTML
|
|
38
|
+
});
|
|
39
|
+
node.innerHTML = '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (node.hasAttribute('b-text')) bindings.push({ type: 'b-text', node, path: node.getAttribute('b-text') });
|
|
43
|
+
if (node.hasAttribute('b-sync')) bindings.push({ type: 'b-sync', node });
|
|
44
|
+
[...node.attributes].forEach(attr => {
|
|
45
|
+
if (attr.value.includes('{{')) bindings.push({ type: 'attr', node, attrName: attr.name, template: attr.value });
|
|
46
|
+
});
|
|
47
|
+
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('{{')) {
|
|
48
|
+
bindings.push({ type: 'text', node, template: node.textContent });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return bindings;
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 1. The `TreeWalker` Efficiency
|
|
56
|
+
|
|
57
|
+
LegoDOM uses a native browser tool called a `TreeWalker`.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **Why not `querySelectorAll('*')`?** A `TreeWalker` is much faster and more memory-efficient. It allows the library to step through every single node (including text nodes) one by one.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### 2. The "Nested Component" Shield
|
|
68
|
+
|
|
69
|
+
One of the smartest parts of this function is the `isInsideBFor` check.
|
|
70
|
+
|
|
71
|
+
- If the scanner finds an element that is inside a `b-for` loop or belongs to a **different** custom component (i.e. user-profile, product-card etc.), it stops.
|
|
72
|
+
|
|
73
|
+
- **Reasoning**: You don't want the parent component to try and manage the internal text of a child component. This maintains **Encapsulation**.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
### 3. What it looks for
|
|
77
|
+
|
|
78
|
+
As it walks the tree, it populates a `bindings` array with "Instruction Objects":
|
|
79
|
+
|
|
80
|
+
- **Directives**: It looks for `b-show`, `b-text`, `b-sync`, and `b-for` attributes.
|
|
81
|
+
|
|
82
|
+
- **Mustaches in Text**: It looks for text nodes containing <code v-pre>{{ }}</code>. It saves the original template (e.g., `"Hello {{name}}"`) so it can swap the name later without losing the "Hello".
|
|
83
|
+
|
|
84
|
+
- **Mustaches in Attributes**: It scans every attribute (like <code v-pre>class="btn {{color}}"</code>) to see if it needs to be dynamic.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
### 4. The "Instruction Object" Structure
|
|
88
|
+
|
|
89
|
+
When it finds something, it pushes an object like this into the list:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
node: [The actual TextNode],
|
|
95
|
+
template: 'Hello {{user.name}}'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
By storing the **actual Node reference**, the library can update the screen later with surgical precision. It doesn't have to search the DOM again; it just goes straight to that specific memory address and updates the value.
|
|
101
|
+
|
|
102
|
+
----------
|
|
103
|
+
|
|
104
|
+
**Summary**: `scanForBindings` is a one-time "reconnaissance mission." it maps out every dynamic part of your component so that future updates are lightning-fast.
|