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,103 @@
|
|
|
1
|
+
# Basic Routing: The Global Outlet
|
|
2
|
+
|
|
3
|
+
Before we dive into surgical swaps, we must understand how LegoJS handles the initial entry into your application. Every application needs a primary gateway—a place where the main content lives. In Lego, this is the `<lego-router>`.
|
|
4
|
+
|
|
5
|
+
## The Entry Point
|
|
6
|
+
|
|
7
|
+
In your `index.html`, you define a single custom element that acts as the "Master Outlet":
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<body>
|
|
11
|
+
<my-application-nav></my-application-nav>
|
|
12
|
+
<lego-router></lego-router>
|
|
13
|
+
</body>
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
When the page loads, the LegoJS router looks at the current browser URL and searches its internal "Route Map" for a match.
|
|
19
|
+
|
|
20
|
+
## Defining Your First Routes
|
|
21
|
+
|
|
22
|
+
Routes are defined using the `Lego.route(path, componentName)` method. This creates a contract between a URL pattern and a Web Component.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// Static Route
|
|
26
|
+
Lego.route('/', 'home-page');
|
|
27
|
+
|
|
28
|
+
// Dynamic Route (with parameters)
|
|
29
|
+
Lego.route('/user/:id', 'profile-page');
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### How the Matching Works
|
|
34
|
+
|
|
35
|
+
1. **The Match**: Lego uses Regex to compare the window location with your defined paths.
|
|
36
|
+
|
|
37
|
+
2. **The Injection**: If a match is found, Lego creates an instance of the associated component (e.g., `<home-page>`) and injects it into the `<lego-router>` tag.
|
|
38
|
+
|
|
39
|
+
3. **The Hydration**: The framework then "snaps" the component to life, initializing its state and running its `mounted()` lifecycle hook.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## The Global Fallback
|
|
43
|
+
|
|
44
|
+
If no route matches the current URL, LegoJS looks for a special fallback route. This is essential for handling **404 Not Found** states gracefully.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
Lego.route('*', 'not-found-page');
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Handling Parameters (`$params`)
|
|
52
|
+
|
|
53
|
+
When you use a dynamic path like `/user/:id`, Lego automatically parses the URL and makes the data available to the component via the global `$route` object.
|
|
54
|
+
|
|
55
|
+
In your component template:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<template b-id="profile-page">
|
|
59
|
+
<h1>User Profile</h1>
|
|
60
|
+
<p>Viewing ID: {{ $route.params.id }}</p>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In your component logic:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
mounted() {
|
|
69
|
+
const userId = this.$route.params.id;
|
|
70
|
+
this.fetchUserData(userId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**or**
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<!-- profile-page.lego -->
|
|
80
|
+
<style>
|
|
81
|
+
/* nothing here for now */
|
|
82
|
+
</style>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<h1>User Profile</h1>
|
|
86
|
+
<p>Viewing ID: {{ $params.id }}</p>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
export default {
|
|
91
|
+
mounted() {
|
|
92
|
+
const userId = this.$route.params.id;
|
|
93
|
+
this.fetchUserData(userId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Summary
|
|
100
|
+
|
|
101
|
+
The `<lego-router>` is the foundation. It handles the "Big Swaps" of your application. However, in a modern "Enterprise" application, we rarely want to swap the _entire_ page content every time the URL changes.
|
|
102
|
+
|
|
103
|
+
In the next section, we will learn about **Surgical Swaps**, where we move beyond the global router and start updating specific pieces of the DOM using `b-target`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
# The Cold Start: Self-Healing Layouts
|
|
3
|
+
|
|
4
|
+
In the previous section, we learned how to use `b-target` to surgically swap fragments while the app is running. But what happens when a user types `myapp.com/messaging/123` directly into the address bar or hits **Refresh (F5)**?
|
|
5
|
+
|
|
6
|
+
In a traditional nested router, the framework handles this automatically. In LegoJS, we use a more powerful, explicit pattern called **Self-Healing Layouts**.
|
|
7
|
+
|
|
8
|
+
## The Cold Entry/Start Challenge
|
|
9
|
+
|
|
10
|
+
When a "Cold Start" occurs:
|
|
11
|
+
|
|
12
|
+
1. The browser requests the URL from the server.
|
|
13
|
+
|
|
14
|
+
2. The server returns the base `index.html`.
|
|
15
|
+
|
|
16
|
+
3. LegoJS matches the route `/messaging/:id` to the `messaging-shell` component.
|
|
17
|
+
|
|
18
|
+
4. The `messaging-shell` mounts, but the `#chat-window` target is empty (or showing its default "Select a conversation" text).
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
The user is at the correct URL, but the **fragment** (the specific email or chat) is missing.
|
|
22
|
+
|
|
23
|
+
## The Solution: The "Self-Healing" Mounted Hook
|
|
24
|
+
|
|
25
|
+
To fix this, the Parent Shell must be responsible for "healing" its own internal state if it detects a parameter in the URL on mount.
|
|
26
|
+
|
|
27
|
+
### Step 1: Define the Shared Route
|
|
28
|
+
|
|
29
|
+
First, ensure your route configuration points both the list and the detail view to the same Shell component.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
Lego.route('/messaging', 'messaging-shell');
|
|
33
|
+
Lego.route('/messaging/:id', 'messaging-shell');
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Step 2: Implement the Healing Logic
|
|
38
|
+
|
|
39
|
+
Inside your `messaging-shell` component, use the `mounted()` hook to check for the presence of a parameter.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
Lego.define('messaging-shell', `
|
|
43
|
+
<div class="layout">
|
|
44
|
+
<aside class="sidebar">
|
|
45
|
+
<!-- List of threads -->
|
|
46
|
+
</aside>
|
|
47
|
+
|
|
48
|
+
<main id="chat-window">
|
|
49
|
+
<!-- Default content -->
|
|
50
|
+
<p>Select a conversation.</p>
|
|
51
|
+
</main>
|
|
52
|
+
</div>
|
|
53
|
+
`, {
|
|
54
|
+
mounted() {
|
|
55
|
+
// Check if we arrived here via a deep-link (e.g., /messaging/123)
|
|
56
|
+
if (this.$route.params.id) {
|
|
57
|
+
// SURGICAL HEALING:
|
|
58
|
+
// Tell Lego to render the current URL into the local target.
|
|
59
|
+
// This pulls the 'messaging-details' fragment into the main window.
|
|
60
|
+
this.$go(window.location.pathname, '#chat-window').get();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Why This Pattern is Superior
|
|
68
|
+
|
|
69
|
+
1. **Explicit Control**: You decide exactly when and how the child fragment appears.
|
|
70
|
+
|
|
71
|
+
2. **Layout Persistence**: The Sidebar and Header are rendered immediately. The user sees the "Shell" instantly, while the specific data fragment "pops" in a few milliseconds later. This improves the **Perceived Performance**.
|
|
72
|
+
|
|
73
|
+
3. **No Nested Config Hell**: You don't need a complex tree of routes. The DOM structure of your shell defines the nesting naturally.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Using Slots for Better Defaults
|
|
77
|
+
|
|
78
|
+
You can use the native `<slot>` tag inside your target area to provide a better "Loading" or "Empty" experience before the healing happens.
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<main id="chat-window">
|
|
82
|
+
<slot>
|
|
83
|
+
<div class="skeleton-loader">Loading your message...</div>
|
|
84
|
+
</slot>
|
|
85
|
+
</main>
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Summary
|
|
90
|
+
|
|
91
|
+
"Self-Healing" is the bridge between a static website and a dynamic application. It ensures that your surgical routing works perfectly even on hard refreshes. By using the `mounted()` hook and the `$go` helper, your components become intelligent enough to manage their own internal layout based on the global URL state.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
# Smart History & The Back Button
|
|
3
|
+
|
|
4
|
+
One of the biggest frustrations in modern web development is "breaking the back button." When you use JavaScript to update only part of a page, the browser often doesn't realize a navigation event occurred. If the user hits "Back," they might be booted out of your app entirely.
|
|
5
|
+
|
|
6
|
+
LegoJS solves this using a system called **Smart History**. It ensures that even surgical, fragment-level updates are recorded and reversible.
|
|
7
|
+
|
|
8
|
+
## How Traditional Routers Fail
|
|
9
|
+
|
|
10
|
+
In a standard SPA, the router usually manages a single "current view." When you navigate:
|
|
11
|
+
|
|
12
|
+
1. The URL changes.
|
|
13
|
+
|
|
14
|
+
2. The old component is destroyed.
|
|
15
|
+
|
|
16
|
+
3. The new component is mounted.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Because the whole page (or the main outlet) is replaced, the browser's history stack is simple: Page A -> Page B. But in a complex layout like LinkedIn, you might have changed the chat window 5 times while the sidebar stayed exactly the same. Users expect the "Back" button to cycle through those chats, not take them back to the login screen.
|
|
20
|
+
|
|
21
|
+
## The LegoJS Approach: Target Tracking
|
|
22
|
+
|
|
23
|
+
When you use `b-link` with a `b-target`, LegoJS does two things simultaneously:
|
|
24
|
+
|
|
25
|
+
1. It updates the browser URL using the History API.
|
|
26
|
+
|
|
27
|
+
2. It stores a "snapshot" of the target information in the history state.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Inside the History State
|
|
31
|
+
|
|
32
|
+
Every time a surgical swap happens, LegoJS saves the target selector in the `history.state`. It looks something like this:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// Internal representation
|
|
36
|
+
history.pushState({
|
|
37
|
+
legoTargets: '#chat-window'
|
|
38
|
+
}, '', '/messaging/124');
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The "Popstate" Magic
|
|
43
|
+
|
|
44
|
+
When a user hits the **Back** or **Forward** button, the browser triggers a `popstate` event. LegoJS intercepts this event and checks if the incoming state contains `legoTargets`.
|
|
45
|
+
|
|
46
|
+
- **If `legoTargets` exists:** LegoJS performs a surgical swap. It takes the URL the browser is moving to and renders it _only_ into the specified target (e.g., `#chat-window`).
|
|
47
|
+
|
|
48
|
+
- **If no target exists:** It performs a global swap in the `<lego-router>`.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Why This Matters for User Experience
|
|
52
|
+
|
|
53
|
+
### 1. Persistence of Sibling State
|
|
54
|
+
|
|
55
|
+
Because only the target is swapped during history navigation, your sidebar remains untouched. If the user had scrolled halfway down a list of 500 emails, they stay exactly at that scroll position as they hit "Back" and "Forward" to view different email details.
|
|
56
|
+
|
|
57
|
+
### 2. Zero-Config History
|
|
58
|
+
|
|
59
|
+
You don't have to write a single line of code to make the back button work. As long as you are using `b-link` and `b-target`, the framework handles the history reconciliation automatically.
|
|
60
|
+
|
|
61
|
+
### 3. Deep Link Consistency
|
|
62
|
+
|
|
63
|
+
Because the history state uses the same URLs as your standard links, a "Back" navigation and a "Hard Refresh" result in the same UI state (thanks to the Self-Healing logic we covered in the previous section).
|
|
64
|
+
|
|
65
|
+
## Summary
|
|
66
|
+
|
|
67
|
+
Smart History is the "glue" that makes a multi-fragment interface feel like a single, cohesive application. It respects the user's intent by making the browser's navigation tools work for specific parts of the page, not just the whole page.
|
|
68
|
+
|
|
69
|
+
Next, we'll dive into the mechanics of how LegoJS finds these targets in complex, nested DOM trees in **Target Resolver: Scoping and Logic**.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
# LegoDOM vs Traditional SPA Router
|
|
3
|
+
|
|
4
|
+
Traditional Single Page Application (SPA) routers (like React Router, Vue Router, or Angular's Router) rely on a **Centralized Configuration Tree**. As your project grows, this JSON or JavaScript object becomes a "source of truth" that is fragile, hard to maintain, and forces a rigid hierarchy on your UI.
|
|
5
|
+
|
|
6
|
+
**LegoJS breaks this pattern.**
|
|
7
|
+
|
|
8
|
+
In Lego, we don't nest routes in a config file. Instead, we use the **URL as the Data Source** and the **DOM as the Layout Engine**.
|
|
9
|
+
|
|
10
|
+
## The Shift in Thinking
|
|
11
|
+
|
|
12
|
+
|Concept | Traditional SPAs | LegoDOM |
|
|
13
|
+
|-----------|---------------|--------|
|
|
14
|
+
| **Route Map** | Nested JSON Objects | Flat Component List |
|
|
15
|
+
| **Nesting** | Defined in JS | Defined by your HTML structure |
|
|
16
|
+
| **UI Updates** | "Nuclear" (Re-renders parent + children) | "Surgical" (Swaps exactly one fragment"|
|
|
17
|
+
| **State** | Lost unless lifted to Global Store | Persists naturally in unaffected siblings |
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## The "Flat Route" Philosophy
|
|
21
|
+
|
|
22
|
+
In a large project, your route definitions should remain a flat list of "Shells" (Layouts).
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// A flat list of structural shells
|
|
26
|
+
Lego.route('/', 'home-shell');
|
|
27
|
+
Lego.route('/messaging', 'messaging-shell');
|
|
28
|
+
Lego.route('/messaging/:id', 'messaging-shell');
|
|
29
|
+
Lego.route('/settings', 'settings-shell');
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
By pointing `/messaging` and `/messaging/:id` to the same **Shell**, you are telling Lego: _"I want the same layout for both URLs, but I'll decide how to fill the holes inside the component based on the URL parameters."_
|
|
34
|
+
|
|
35
|
+
## Why This Scales
|
|
36
|
+
|
|
37
|
+
In a LinkedIn-sized project, a traditional router would need to know that the `ChatWindow` is a child of the `MessagingLayout`. If you wanted to move that `ChatWindow` to the `HomeLayout`, you’d have to refactor your entire route tree.
|
|
38
|
+
|
|
39
|
+
In Lego, you just change the **Target**. Because the components are agnostic, they don't care who their parent is. They just care about where the `b-target` tells them to go.
|
|
40
|
+
|
|
41
|
+
### Defining with SFCs
|
|
42
|
+
|
|
43
|
+
LegoDOM supports the use of Single File Components and this works seamlessly with the Router. You can define your Shell components via the `<template>` tag. This keeps our architectural "Shells" clean and readable.
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<style></style>
|
|
47
|
+
<template b-id="home-shell">
|
|
48
|
+
<div class="layout">
|
|
49
|
+
<nav-bar></nav-bar>
|
|
50
|
+
<lego-router></lego-router> <!-- The global outlet -->
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
<script>
|
|
54
|
+
export default {
|
|
55
|
+
name: 'HomeShell'
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Key Vocabulary for the Tutorial
|
|
61
|
+
|
|
62
|
+
Throughout this guide, we will refer to three primary concepts:
|
|
63
|
+
|
|
64
|
+
1. **The Shell**: The high-level SFC that provides the grid, navigation, and sidebar.
|
|
65
|
+
|
|
66
|
+
2. **The Fragment**: A self-contained SFC (like a chat thread or a profile header) that is surgically injected into a shell.
|
|
67
|
+
|
|
68
|
+
3. **The Target**: A CSS selector, Tag Name, or ID that acts as the "destination hole" for a fragment.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## What's Next?
|
|
72
|
+
|
|
73
|
+
We will move away from the theory and look at **Basic Routing**. We'll see how the `<lego-router>` element acts as the default gateway for your application and how Lego identifies which component to mount when the page first loads.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
# 06. Target Resolver: Scoping and Logic
|
|
3
|
+
|
|
4
|
+
In the previous chapters, we used `b-target` to send content to different parts of the page. But how does LegoJS actually find those "holes" in a complex, nested DOM? This is where the **Target Resolver** comes in.
|
|
5
|
+
|
|
6
|
+
The Target Resolver is a prioritized logic engine that ensures your links always find the correct destination, even in massive applications with thousands of components.
|
|
7
|
+
|
|
8
|
+
## The Problem with Global Selectors
|
|
9
|
+
|
|
10
|
+
In traditional JavaScript, if you use `document.querySelector('#detail-view')`, the browser searches the entire page. This is fine for small sites, but in a "Web Component First" architecture, it causes two major issues:
|
|
11
|
+
|
|
12
|
+
1. **ID Collisions**: If you accidentally use the same ID in two different components, your link might update the wrong part of the page.
|
|
13
|
+
|
|
14
|
+
2. **Tight Coupling**: Your sidebar link has to know the exact global ID of the main content area, making it harder to move components around.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## The Resolver Hierarchy
|
|
18
|
+
|
|
19
|
+
When you click a `b-link` with a `b-target`, LegoJS follows a strict search order to resolve the target:
|
|
20
|
+
|
|
21
|
+
### 1. Local Component Scope (The Primary Search)
|
|
22
|
+
|
|
23
|
+
LegoJS first looks for the target **inside the component that contains the link**.
|
|
24
|
+
|
|
25
|
+
If you use `b-target="email-view"`, Lego looks for an `<email-view>` tag _within the current parent shell_. This allows you to have multiple instances of the same layout on one screen without them interfering with each other.
|
|
26
|
+
|
|
27
|
+
### 2. Tag Name Resolution (The Web Component Way)
|
|
28
|
+
|
|
29
|
+
If your target doesn't start with a `#`, Lego treats it as a **Component Tag Name**.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
<!-- Lego looks for a <thread-view> tag nearby -->
|
|
33
|
+
<a href="/chat/1" b-link b-target="thread-view">Open Chat</a>
|
|
34
|
+
|
|
35
|
+
<thread-view></thread-view>
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This is the most "Enterprise" way to build. It makes your HTML self-documenting. You aren't targeting a generic `div`; you are targeting a specific functional slot.
|
|
40
|
+
|
|
41
|
+
### 3. Global ID Fallback
|
|
42
|
+
|
|
43
|
+
If no local tag or element matches, the resolver expands its search to the entire `document` using ID selectors.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
<!-- Lego looks for any element with id="global-sidebar" -->
|
|
47
|
+
<a href="/menu" b-link b-target="#global-sidebar">Menu</a>
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 4. The Router Default
|
|
52
|
+
|
|
53
|
+
If the resolver exhausts all options and still can't find the target, it defaults to the `<lego-router>`. This ensures that even if you make a typo in your target name, the user still sees the content (it just might take over the whole page instead of a small fragment).
|
|
54
|
+
|
|
55
|
+
## Advanced: The Functional Target
|
|
56
|
+
|
|
57
|
+
For highly dynamic UIs, `b-target` can even be a function or a dynamic expression.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
<!-- Logic decides the target based on screen size -->
|
|
61
|
+
<a href="/settings" b-link b-target="{{ isMobile ? '#main' : 'settings-pane' }}">
|
|
62
|
+
Settings
|
|
63
|
+
</a>
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Why This Matters
|
|
68
|
+
|
|
69
|
+
By prioritizing local tags over global IDs, LegoJS encourages **Encapsulation**. Your components become "Black Boxes" that manage their own internal routing targets. This means you can take a complex "Messaging Shell" and drop it into a "Dashboard Shell" without changing a single line of routing code—the targets will still resolve correctly because they are scoped to their parents.
|
|
70
|
+
|
|
71
|
+
## Summary
|
|
72
|
+
|
|
73
|
+
The Target Resolver turns string attributes into intelligent DOM navigation. It respects the boundaries of your Web Components while providing a robust fallback system.
|
|
74
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Surgical Swaps: Mastering b-target
|
|
2
|
+
|
|
3
|
+
The true power of LegoJS lies in its ability to perform **Surgical Swaps**. In a traditional application, clicking a link often causes the entire page to re-render, destroying the state of your sidebar, header, or scroll position.
|
|
4
|
+
|
|
5
|
+
With `b-target` (and optionally `b-link`), we can choose to update only a specific "fragment" of the page.
|
|
6
|
+
|
|
7
|
+
## The Problem with "Nuclear" Navigation
|
|
8
|
+
|
|
9
|
+
Imagine a messaging app (like LinkedIn or Slack). You have a sidebar full of conversations. When you click a message, you want the chat window to update, but you **don't** want the sidebar to reload.
|
|
10
|
+
|
|
11
|
+
If the sidebar reloads:
|
|
12
|
+
|
|
13
|
+
1. The scroll position is lost.
|
|
14
|
+
|
|
15
|
+
2. Any search text in the sidebar is cleared.
|
|
16
|
+
|
|
17
|
+
3. The UI "flickers," making the app feel slow.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## The Solution: `b-target`
|
|
21
|
+
|
|
22
|
+
The `b-target` directive allows a link to specify exactly where the new component should be rendered. It implies `b-link` (history update) by default.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Example: messaging-shell.html
|
|
26
|
+
|
|
27
|
+
In this SFC, we define a layout with a sidebar and a main content area. Clicking a contact updates _only_ the `<main>` area.
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<!-- messaging-shell.html -->
|
|
31
|
+
<template>
|
|
32
|
+
<div class="messaging-layout">
|
|
33
|
+
<aside class="sidebar">
|
|
34
|
+
<h2>Contacts</h2>
|
|
35
|
+
<nav>
|
|
36
|
+
<a href="/chat/alice" b-target="#chat-window">Alice</a>
|
|
37
|
+
<a href="/chat/bob" b-target="#chat-window">Bob</a>
|
|
38
|
+
</nav>
|
|
39
|
+
</aside>
|
|
40
|
+
|
|
41
|
+
<main id="chat-window">
|
|
42
|
+
<p>Select a contact to start chatting.</p>
|
|
43
|
+
</main>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
export default {
|
|
49
|
+
mounted() {
|
|
50
|
+
console.log("Messaging shell ready.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.messaging-layout {
|
|
57
|
+
display: flex;
|
|
58
|
+
height: 100vh;
|
|
59
|
+
}
|
|
60
|
+
.sidebar {
|
|
61
|
+
width: 300px;
|
|
62
|
+
border-right: 1px solid #ccc;
|
|
63
|
+
}
|
|
64
|
+
#chat-window {
|
|
65
|
+
flex: 1;
|
|
66
|
+
padding: 20px;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 1. Targeting by ID
|
|
73
|
+
|
|
74
|
+
You can tell Lego to find a specific element by its ID and replace its contents.
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<!-- messaging-shell.html -->
|
|
78
|
+
<template>
|
|
79
|
+
<div class="layout">
|
|
80
|
+
<aside class="sidebar">
|
|
81
|
+
<div b-for="chat in threads">
|
|
82
|
+
<!-- Parent component (this shell) binds data to these links -->
|
|
83
|
+
<a href="/messaging/{{chat.id}}" b-target="#chat-window">
|
|
84
|
+
{{chat.userName}}
|
|
85
|
+
</a>
|
|
86
|
+
</div>
|
|
87
|
+
</aside>
|
|
88
|
+
|
|
89
|
+
<main id="chat-window">
|
|
90
|
+
<!-- Only this area will change when a link is clicked -->
|
|
91
|
+
<p>Select a conversation to begin.</p>
|
|
92
|
+
</main>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Targeting by Component Tag (The Web Component Way)
|
|
99
|
+
|
|
100
|
+
Because Lego is built on Custom Elements, you can target a component tag directly. The framework will find that tag and swap its internal content.
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<a href="/profile/settings" b-target="settings-view">Edit Settings</a>
|
|
104
|
+
|
|
105
|
+
<settings-view>
|
|
106
|
+
<!-- Content gets swapped here -->
|
|
107
|
+
</settings-view>
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## How the Target Resolver Works
|
|
112
|
+
|
|
113
|
+
When you click a link with a `b-target`, the LegoJS **Target Resolver** follows a specific hierarchy:
|
|
114
|
+
|
|
115
|
+
1. **Local Scope**: It looks for the target inside the current component first. This prevents "ID collisions" if you have multiple instances of a layout.
|
|
116
|
+
|
|
117
|
+
2. **Component Match**: If the target doesn't start with `#`, it treats it as a tag name (e.g., `thread-view`).
|
|
118
|
+
|
|
119
|
+
3. **Global Fallback**: If it can't find a local match, it searches the entire document.
|
|
120
|
+
|
|
121
|
+
4. **Router Fallback**: If no target is found, it defaults back to the `<lego-router>`.
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
## Smart History
|
|
125
|
+
|
|
126
|
+
Even though we are only swapping a small part of the DOM, LegoJS is smart enough to update the browser's address bar.
|
|
127
|
+
|
|
128
|
+
When a surgical swap happens, Lego saves the "target" information into the browser's history state (`history.state.legoTargets`). This means that when a user hits the **Back Button**, Lego knows exactly which fragment needs to be swapped back to its previous state.
|
|
129
|
+
|
|
130
|
+
## Summary
|
|
131
|
+
|
|
132
|
+
`b-target` turns your web app into a high-performance workspace. By keeping the "Shell" alive and only swapping "Fragments," you maintain state, eliminate flickers, and provide a desktop-like experience.
|
|
133
|
+
|
|
134
|
+
Next, we will tackle the most common question: **"What happens if I refresh the page while looking at a surgical fragment?"** We'll explore the **Cold Start: Self-Healing Layouts**.
|
|
@@ -31,19 +31,11 @@
|
|
|
31
31
|
</head>
|
|
32
32
|
|
|
33
33
|
<body>
|
|
34
|
-
<
|
|
34
|
+
<app-navbar></app-navbar>
|
|
35
|
+
<lego-router id="app-outlet"></lego-router>
|
|
36
|
+
<aside id="outside-router"></aside>
|
|
35
37
|
|
|
36
|
-
<
|
|
37
|
-
<p><strong>✨ These components are auto-discovered from .lego files!</strong></p>
|
|
38
|
-
<p>The Vite plugin automatically finds all .lego files in <code>src/components/</code> and registers them.</p>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<!-- These components are automatically discovered and hydrated -->
|
|
42
|
-
<sample-component></sample-component>
|
|
43
|
-
<greeting-card></greeting-card>
|
|
44
|
-
<todo-list></todo-list>
|
|
45
|
-
|
|
46
|
-
<script type="module" src="/src/main.js"></script>
|
|
38
|
+
<script type="module" src="/src/app.js"></script>
|
|
47
39
|
</body>
|
|
48
40
|
|
|
49
41
|
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Import Tailwind CSS
|
|
2
|
+
import './app.css';
|
|
3
|
+
|
|
4
|
+
// Import Lego core
|
|
5
|
+
import { Lego } from 'lego-dom/main.js';
|
|
6
|
+
|
|
7
|
+
// Import virtual module that auto-discovers and registers all .lego components
|
|
8
|
+
import registerComponents from 'virtual:lego-components';
|
|
9
|
+
|
|
10
|
+
// Register all auto-discovered components
|
|
11
|
+
registerComponents();
|
|
12
|
+
|
|
13
|
+
// 10. Define SPA Routes
|
|
14
|
+
Lego.route('/', 'sample-component');
|
|
15
|
+
Lego.route('/todo', 'todo-list');
|
|
16
|
+
Lego.route('/card', 'user-card');
|
|
17
|
+
Lego.route('/customers/:id/orders', 'customer-orders');
|
|
18
|
+
Lego.route('/customers/:id/details', 'customer-details');
|
|
19
|
+
|
|
20
|
+
// 11. Optional: Add a middleware example
|
|
21
|
+
Lego.route('/admin', 'admin-panel', async (params, globals) => {
|
|
22
|
+
console.log('Checking permissions for', params);
|
|
23
|
+
return globals.isLoggedIn;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Initialize Lego
|
|
27
|
+
await Lego.init(document.body, {
|
|
28
|
+
tailwind: ['/src/app.css']
|
|
29
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
self {
|
|
3
|
+
display: block;
|
|
4
|
+
padding: 1rem;
|
|
5
|
+
background: #f9fafb;
|
|
6
|
+
border-bottom: 1px solid #e0e0e0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
nav {
|
|
10
|
+
display: flex;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
align-items: center;
|
|
13
|
+
}
|
|
14
|
+
</style>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<nav>
|
|
18
|
+
<ul>
|
|
19
|
+
<li><a href="/" b-target="#outside-router" b-link="true">Home</a></li>
|
|
20
|
+
<li><a href="/todo" b-target>Todo</a></li>
|
|
21
|
+
<li><a href="/card" b-target>Card</a></li>
|
|
22
|
+
<li><a href="/customers/1/orders">Customers</a></li>
|
|
23
|
+
</ul>
|
|
24
|
+
</nav>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script>
|
|
28
|
+
export default {
|
|
29
|
+
name: 'AppNavbar',
|
|
30
|
+
mounted() {
|
|
31
|
+
console.log('This is mounted');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<customers-shell>
|
|
3
|
+
<div style="padding: 1rem; border: 1px dashed #4f46e5;">
|
|
4
|
+
<h3>👤 Customer Profile</h3>
|
|
5
|
+
<p>Details for Customer ID: <strong>{{ global.$route.params.id }}</strong></p>
|
|
6
|
+
|
|
7
|
+
<div style="display: grid; gap: 0.5rem; margin-top: 1rem;">
|
|
8
|
+
<div><strong>Status:</strong> Premium Member</div>
|
|
9
|
+
<div><strong>Joined:</strong> January 2024</div>
|
|
10
|
+
<div><strong>Region:</strong> Europe</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div style="margin-top: 1rem;">
|
|
14
|
+
<a href="/customers/{{ global.$route.params.id }}/orders" b-target="#todo-container">View Orders</a>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</customers-shell>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
export default {
|
|
22
|
+
name: 'CustomerDetails'
|
|
23
|
+
}
|
|
24
|
+
</script>
|