lego-dom 1.0.0 → 1.3.4
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/.legodom +87 -0
- package/CHANGELOG.md +87 -3
- package/cdn.html +10 -5
- package/docs/.vitepress/config.js +23 -7
- package/docs/api/config.md +95 -0
- package/docs/api/define.md +29 -2
- package/docs/api/directives.md +10 -2
- package/docs/api/index.md +1 -0
- package/docs/contributing/01-welcome.md +2 -0
- package/docs/contributing/02-registry.md +37 -3
- package/docs/contributing/06-init.md +13 -2
- package/docs/contributing/07-observer.md +3 -0
- package/docs/contributing/08-snap.md +15 -1
- package/docs/contributing/10-studs.md +3 -1
- package/docs/contributing/11-scanner.md +13 -0
- package/docs/contributing/12-render.md +32 -10
- package/docs/contributing/13-directives.md +19 -1
- package/docs/contributing/14-events.md +1 -1
- package/docs/contributing/15-router.md +49 -1
- package/docs/contributing/16-state.md +9 -10
- package/docs/contributing/17-legodom.md +1 -8
- package/docs/contributing/index.md +23 -4
- package/docs/examples/form.md +1 -1
- package/docs/examples/index.md +3 -3
- package/docs/examples/routing.md +10 -10
- package/docs/examples/sfc-showcase.md +1 -1
- package/docs/examples/todo-app.md +7 -7
- package/docs/guide/cdn-usage.md +44 -18
- package/docs/guide/components.md +18 -12
- package/docs/guide/directives.md +131 -22
- package/docs/guide/directory-structure.md +248 -0
- package/docs/guide/faq.md +210 -0
- package/docs/guide/getting-started.md +14 -10
- package/docs/guide/index.md +1 -1
- package/docs/guide/lifecycle.md +32 -0
- package/docs/guide/quick-start.md +4 -4
- package/docs/guide/reactivity.md +2 -2
- package/docs/guide/routing.md +69 -8
- package/docs/guide/server-side.md +134 -0
- package/docs/guide/sfc.md +96 -13
- package/docs/guide/templating.md +62 -57
- package/docs/index.md +9 -9
- package/docs/router/basic-routing.md +8 -8
- package/docs/router/cold-entry.md +2 -2
- package/docs/router/history.md +7 -7
- package/docs/router/index.md +1 -1
- package/docs/router/resolver.md +5 -5
- package/docs/router/surgical-swaps.md +5 -5
- package/docs/tutorial/01-project-setup.md +152 -0
- package/docs/tutorial/02-your-first-component.md +226 -0
- package/docs/tutorial/03-adding-routes.md +279 -0
- package/docs/tutorial/04-multi-page-app.md +329 -0
- package/docs/tutorial/05-state-and-globals.md +285 -0
- package/docs/tutorial/index.md +40 -0
- package/examples/vite-app/index.html +1 -0
- package/examples/vite-app/src/app.js +2 -2
- package/examples/vite-app/src/components/side-menu.lego +46 -0
- package/examples/vite-app/vite.config.js +2 -1
- package/main.js +261 -72
- package/main.min.js +7 -0
- package/monitoring-plugin.js +111 -0
- package/package.json +4 -2
- package/parse-lego.js +49 -22
- package/tests/error.test.js +74 -0
- package/tests/main.test.js +2 -2
- package/tests/memory.test.js +68 -0
- package/tests/monitoring.test.js +74 -0
- package/tests/naming.test.js +74 -0
- package/tests/parse-lego.test.js +2 -2
- package/tests/security.test.js +67 -0
- package/tests/server.test.js +114 -0
- package/tests/syntax.test.js +67 -0
- package/vite-plugin.js +3 -2
- package/docs/guide/contributing.md +0 -32
package/docs/guide/directives.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Directives are special attributes that add reactive behavior to elements.
|
|
4
4
|
|
|
5
|
+
## b-if
|
|
6
|
+
|
|
7
|
+
Conditional rendering (adds/removes from DOM).
|
|
8
|
+
|
|
9
|
+
### Basic Usage
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<p b-if="isLoggedIn">Welcome back!</p>
|
|
13
|
+
<p b-if="!isLoggedIn">Please log in</p>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### With Expressions
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<div b-if="count > 0">Count is [[ count ]]</div>
|
|
20
|
+
<div b-if="items.length === 0">No items</div>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
::: tip b-if vs b-show
|
|
24
|
+
`b-if` adds or removes the element from the DOM.
|
|
25
|
+
`b-show` toggles `display: none`.
|
|
26
|
+
|
|
27
|
+
Use `b-if` if the condition rarely changes.
|
|
28
|
+
Use `b-show` if you toggle often.
|
|
29
|
+
:::
|
|
30
|
+
|
|
5
31
|
## b-show
|
|
6
32
|
|
|
7
33
|
Conditional rendering using `display: none`.
|
|
@@ -16,7 +42,7 @@ Conditional rendering using `display: none`.
|
|
|
16
42
|
### With Expressions
|
|
17
43
|
|
|
18
44
|
```html
|
|
19
|
-
<div b-show="count > 0">Count is
|
|
45
|
+
<div b-show="count > 0">Count is [[ count ]]</div>
|
|
20
46
|
<div b-show="items.length === 0">No items</div>
|
|
21
47
|
<div b-show="user && user.role === 'admin'">Admin panel</div>
|
|
22
48
|
```
|
|
@@ -32,6 +58,73 @@ Conditional rendering using `display: none`.
|
|
|
32
58
|
`b-show` sets `display: none` when the condition is false. The element stays in the DOM but is hidden.
|
|
33
59
|
:::
|
|
34
60
|
|
|
61
|
+
## b-html
|
|
62
|
+
|
|
63
|
+
Renders raw HTML content.
|
|
64
|
+
|
|
65
|
+
> [!WARNING]
|
|
66
|
+
> Only use on trusted content. This exposes you to XSS vulnerabilities if used with user input.
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<div b-html="rawContent"></div>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
{
|
|
74
|
+
rawContent: '<b>Bold</b> and <i>Italic</i>'
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## b-text
|
|
79
|
+
|
|
80
|
+
Sets the text content of an element.
|
|
81
|
+
|
|
82
|
+
> [!WARNING] Limitation
|
|
83
|
+
> **`b-text` is extremely weak.** Unlike `[[ ]]`, it **does not** support JavaScript expressions (math, logic, functions). It ONLY supports simple property paths.
|
|
84
|
+
|
|
85
|
+
### Functionality
|
|
86
|
+
|
|
87
|
+
| Syntax | Supported? | Example |
|
|
88
|
+
| :--- | :--- | :--- |
|
|
89
|
+
| **Property Path** | Yes | `b-text="user.name"` |
|
|
90
|
+
| **Math** | No | `b-text="count + 1"` |
|
|
91
|
+
| **Logic** | No | `b-text="isActive ? 'Yes' : 'No'"` |
|
|
92
|
+
| **Methods** | No | `b-text="formatDate(date)"` |
|
|
93
|
+
|
|
94
|
+
Use `[[ ]]` for anything complex. `b-text` is strictly for direct property binding.
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<!-- Works -->
|
|
98
|
+
<span b-text="user.name"></span>
|
|
99
|
+
|
|
100
|
+
<!-- Does NOT Work (Use [[ ]] instead) -->
|
|
101
|
+
<span b-text="user.firstName + ' ' + user.lastName"></span>
|
|
102
|
+
<span b-text="count + 1"></span>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## b-var
|
|
106
|
+
|
|
107
|
+
Creates a reference to a DOM element accessible via `this.$vars`.
|
|
108
|
+
|
|
109
|
+
Use `b-var` when you need direct DOM access (e.g., `.focus()`, `.click()`, `.play()`).
|
|
110
|
+
|
|
111
|
+
### Usage
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<input type="file" b-var="fileInput" style="display:none">
|
|
115
|
+
<button @click="$vars.fileInput.click()">Upload</button>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or in script:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
export default {
|
|
122
|
+
openPicker() {
|
|
123
|
+
this.$vars.fileInput.click();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
35
128
|
## b-for
|
|
36
129
|
|
|
37
130
|
List rendering.
|
|
@@ -40,7 +133,7 @@ List rendering.
|
|
|
40
133
|
|
|
41
134
|
```html
|
|
42
135
|
<ul>
|
|
43
|
-
<li b-for="item in items">
|
|
136
|
+
<li b-for="item in items">[[ item ]]</li>
|
|
44
137
|
</ul>
|
|
45
138
|
```
|
|
46
139
|
|
|
@@ -49,7 +142,7 @@ List rendering.
|
|
|
49
142
|
```html
|
|
50
143
|
<ul>
|
|
51
144
|
<li b-for="todo in todos">
|
|
52
|
-
|
|
145
|
+
[[ todo.text ]] - [[ todo.done ? 'Done' : 'Pending' ]]
|
|
53
146
|
</li>
|
|
54
147
|
</ul>
|
|
55
148
|
```
|
|
@@ -61,7 +154,7 @@ Use `$index` (implicit variable):
|
|
|
61
154
|
```html
|
|
62
155
|
<ul>
|
|
63
156
|
<li b-for="item in items">
|
|
64
|
-
#
|
|
157
|
+
#[[ $index + 1 ]]: [[ item.name ]]
|
|
65
158
|
</li>
|
|
66
159
|
</ul>
|
|
67
160
|
```
|
|
@@ -70,10 +163,10 @@ Use `$index` (implicit variable):
|
|
|
70
163
|
|
|
71
164
|
```html
|
|
72
165
|
<div b-for="category in categories">
|
|
73
|
-
<h3>
|
|
166
|
+
<h3>[[ category.name ]]</h3>
|
|
74
167
|
<ul>
|
|
75
168
|
<li b-for="product in category.products">
|
|
76
|
-
|
|
169
|
+
[[ product.name ]]
|
|
77
170
|
</li>
|
|
78
171
|
</ul>
|
|
79
172
|
</div>
|
|
@@ -83,8 +176,8 @@ Use `$index` (implicit variable):
|
|
|
83
176
|
|
|
84
177
|
```html
|
|
85
178
|
<li b-for="user in users">
|
|
86
|
-
<span b-show="user.active">✅
|
|
87
|
-
<span b-show="!user.active">❌
|
|
179
|
+
<span b-show="user.active">✅ [[ user.name ]]</span>
|
|
180
|
+
<span b-show="!user.active">❌ [[ user.name ]]</span>
|
|
88
181
|
</li>
|
|
89
182
|
```
|
|
90
183
|
|
|
@@ -96,7 +189,7 @@ Two-way data binding for form inputs.
|
|
|
96
189
|
|
|
97
190
|
```html
|
|
98
191
|
<input b-sync="username" placeholder="Enter username">
|
|
99
|
-
<p>Hello,
|
|
192
|
+
<p>Hello, [[ username ]]!</p>
|
|
100
193
|
```
|
|
101
194
|
|
|
102
195
|
### Checkbox
|
|
@@ -112,7 +205,7 @@ Two-way data binding for form inputs.
|
|
|
112
205
|
<input type="radio" name="size" value="small" b-sync="selectedSize">
|
|
113
206
|
<input type="radio" name="size" value="medium" b-sync="selectedSize">
|
|
114
207
|
<input type="radio" name="size" value="large" b-sync="selectedSize">
|
|
115
|
-
<p>Selected:
|
|
208
|
+
<p>Selected: [[ selectedSize ]]</p>
|
|
116
209
|
```
|
|
117
210
|
|
|
118
211
|
### Select Dropdown
|
|
@@ -123,14 +216,14 @@ Two-way data binding for form inputs.
|
|
|
123
216
|
<option value="uk">United Kingdom</option>
|
|
124
217
|
<option value="ca">Canada</option>
|
|
125
218
|
</select>
|
|
126
|
-
<p>Country:
|
|
219
|
+
<p>Country: [[ country ]]</p>
|
|
127
220
|
```
|
|
128
221
|
|
|
129
222
|
### Textarea
|
|
130
223
|
|
|
131
224
|
```html
|
|
132
225
|
<textarea b-sync="message" rows="4"></textarea>
|
|
133
|
-
<p>
|
|
226
|
+
<p>[[ message.length ]] characters</p>
|
|
134
227
|
```
|
|
135
228
|
|
|
136
229
|
### In b-for Loops
|
|
@@ -138,7 +231,7 @@ Two-way data binding for form inputs.
|
|
|
138
231
|
```html
|
|
139
232
|
<li b-for="todo in todos">
|
|
140
233
|
<input type="checkbox" b-sync="todo.done">
|
|
141
|
-
<span class="
|
|
234
|
+
<span class="[[ todo.done ? 'done' : '' ]]">[[ todo.text ]]</span>
|
|
142
235
|
</li>
|
|
143
236
|
```
|
|
144
237
|
|
|
@@ -220,8 +313,8 @@ Client-side navigation (prevents page reload).
|
|
|
220
313
|
### With Dynamic Routes
|
|
221
314
|
|
|
222
315
|
```html
|
|
223
|
-
<a href="/user/
|
|
224
|
-
<a href="/product/
|
|
316
|
+
<a href="/user/[[ userId ]]" b-link>View Profile</a>
|
|
317
|
+
<a href="/product/[[ productId ]]" b-link>[[ productName ]]</a>
|
|
225
318
|
```
|
|
226
319
|
|
|
227
320
|
::: tip Router Required
|
|
@@ -272,7 +365,7 @@ Lego.define('user-card', `...`, {
|
|
|
272
365
|
|
|
273
366
|
```html
|
|
274
367
|
<li b-for="item in items" b-show="item.visible">
|
|
275
|
-
|
|
368
|
+
[[ item.name ]]
|
|
276
369
|
</li>
|
|
277
370
|
```
|
|
278
371
|
|
|
@@ -281,7 +374,7 @@ Lego.define('user-card', `...`, {
|
|
|
281
374
|
```html
|
|
282
375
|
<li b-for="todo in todos">
|
|
283
376
|
<input type="checkbox" b-sync="todo.done">
|
|
284
|
-
|
|
377
|
+
[[ todo.text ]]
|
|
285
378
|
</li>
|
|
286
379
|
```
|
|
287
380
|
|
|
@@ -303,7 +396,7 @@ Lego.define('user-card', `...`, {
|
|
|
303
396
|
<div b-show="showPanel">Panel content</div>
|
|
304
397
|
|
|
305
398
|
<!-- ❌ Verbose -->
|
|
306
|
-
<div style="display:
|
|
399
|
+
<div style="display: [[ showPanel ? 'block' : 'none' ]]">Panel content</div>
|
|
307
400
|
```
|
|
308
401
|
|
|
309
402
|
### 2. Keep Event Handlers Simple
|
|
@@ -350,7 +443,7 @@ Move complex logic to methods.
|
|
|
350
443
|
|
|
351
444
|
```html
|
|
352
445
|
<!-- For frequent toggling -->
|
|
353
|
-
<div class="
|
|
446
|
+
<div class="[[ visible ? '' : 'hidden' ]]">Content</div>
|
|
354
447
|
```
|
|
355
448
|
|
|
356
449
|
```css
|
|
@@ -377,7 +470,7 @@ Paginate large lists:
|
|
|
377
470
|
```
|
|
378
471
|
|
|
379
472
|
```html
|
|
380
|
-
<li b-for="item in visibleItems()">
|
|
473
|
+
<li b-for="item in visibleItems()">[[ item.name ]]</li>
|
|
381
474
|
```
|
|
382
475
|
|
|
383
476
|
## Common Patterns
|
|
@@ -393,7 +486,7 @@ Paginate large lists:
|
|
|
393
486
|
|
|
394
487
|
```html
|
|
395
488
|
<button @click="count--">-</button>
|
|
396
|
-
<span>
|
|
489
|
+
<span>[[ count ]]</span>
|
|
397
490
|
<button @click="count++">+</button>
|
|
398
491
|
```
|
|
399
492
|
|
|
@@ -404,7 +497,7 @@ Paginate large lists:
|
|
|
404
497
|
<ul>
|
|
405
498
|
<li b-for="todo in todos">
|
|
406
499
|
<input type="checkbox" b-sync="todo.done">
|
|
407
|
-
<span class="
|
|
500
|
+
<span class="[[ todo.done ? 'done' : '' ]]">[[ todo.text ]]</span>
|
|
408
501
|
</li>
|
|
409
502
|
</ul>
|
|
410
503
|
```
|
|
@@ -423,6 +516,22 @@ Paginate large lists:
|
|
|
423
516
|
<div b-show="activeTab === 'settings'">Settings content</div>
|
|
424
517
|
```
|
|
425
518
|
|
|
519
|
+
## See Also
|
|
520
|
+
|
|
521
|
+
Some directives are specific to certain features and are documented in their respective guides:
|
|
522
|
+
|
|
523
|
+
### Component Directives
|
|
524
|
+
|
|
525
|
+
- **`b-id`**: Defines a component from a template.
|
|
526
|
+
- **`b-styles`**: Applies shared styles to a component.
|
|
527
|
+
- See [Components Guide](/guide/components)
|
|
528
|
+
|
|
529
|
+
### Routing Directives
|
|
530
|
+
|
|
531
|
+
- **`b-target`**: Specifies the target element for surgical routing updates.
|
|
532
|
+
- **`b-link`**: Controls browser history behavior for links.
|
|
533
|
+
- See [Routing Guide](/guide/routing)
|
|
534
|
+
|
|
426
535
|
## Next Steps
|
|
427
536
|
|
|
428
537
|
- See [directive examples](/examples/)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Chaos Tends To A Minimum
|
|
2
|
+
|
|
3
|
+
> [!NOTE] This is a Recommendation, Not a Bible
|
|
4
|
+
> This page is one person's take on how to structure large LegoDOM applications. It is not enforced by the framework. You are encouraged to adapt, improve, or completely ignore it. If you find a better pattern, please share it.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
In LegoDOM land everything is a `.lego`. A `block` is a lego, a `widget` is a lego, a `component` is a lego, a `page` is a lego.
|
|
9
|
+
|
|
10
|
+
But what do they *mean*?
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## The Four Levels
|
|
14
|
+
|
|
15
|
+
| Level | Role | Knows About | Example |
|
|
16
|
+
| :--- | :--- | :--- | :--- |
|
|
17
|
+
| **Block** | Identity | Its own visuals | `<block-avatar>`, `<block-button>` |
|
|
18
|
+
| **Widget** | Intent | How an interaction works | `<widget-file-trigger>`, `<widget-dropdown>` |
|
|
19
|
+
| **Component** | Computation | Business data & API calls | `<comp-profile-settings>`, `<comp-checkout-form>` |
|
|
20
|
+
| **Page** | Coordination | Layout & Routing | `<page-dashboard>`, `<page-login>` |
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## The Litmus Test: The Avatar Upload
|
|
24
|
+
|
|
25
|
+
**The Question:**
|
|
26
|
+
> I have an avatar. When I click it, it opens a file picker. When the file changes, it POSTs to `/v1/avatars`. What is it? A Block? A Widget? A Component?
|
|
27
|
+
|
|
28
|
+
**The Technical Answer:** `main.js` allows all of this in one file. It will work.
|
|
29
|
+
|
|
30
|
+
**The Architectural Answer:** You have conflated three distinct responsibilities into one:
|
|
31
|
+
1. **Identity** Looking like an avatar.
|
|
32
|
+
2. **Intent** Picking a file.
|
|
33
|
+
3. **Computation** Saving to *your specific server*.
|
|
34
|
+
|
|
35
|
+
**The Consequence:**
|
|
36
|
+
If you later want to display that avatar in a read-only user list, you *cannot reuse this component* because clicking it triggers unwanted upload logic. You will be forced to create a new, duplicate `<block-avatar>` just for display.
|
|
37
|
+
|
|
38
|
+
### The "Lego Way" Solution
|
|
39
|
+
|
|
40
|
+
Split this into its three atomic truths to maximize reusability.
|
|
41
|
+
|
|
42
|
+
#### 1. The Block (Identity)
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<!-- block-avatar.lego -->
|
|
46
|
+
<template>
|
|
47
|
+
<img class="avatar" src="[[ src ]]" alt="[[ alt ]]">
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
|
|
52
|
+
</style>
|
|
53
|
+
|
|
54
|
+
<script>
|
|
55
|
+
export default {
|
|
56
|
+
src: '/default-avatar.png',
|
|
57
|
+
alt: 'User'
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
```
|
|
61
|
+
- **Role:** Just the visuals. Circular crop, fallback, size classes.
|
|
62
|
+
- **Logic:** None. Zero business knowledge.
|
|
63
|
+
- **Reusability:** Used *everywhere* e.g. Navbar, User List, Profile Page, Comments.
|
|
64
|
+
|
|
65
|
+
#### 2. The Widget (Intent)
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<!-- widget-file-trigger.lego -->
|
|
69
|
+
<template>
|
|
70
|
+
<div @click="openPicker()">
|
|
71
|
+
<slot></slot>
|
|
72
|
+
<input type="file" style="display:none" b-var="avatarFileElement" @change="onFileChange">
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
export default {
|
|
78
|
+
openPicker() {
|
|
79
|
+
this.$vars.avatarFileElement.click();
|
|
80
|
+
},
|
|
81
|
+
onFileChange(event) {
|
|
82
|
+
// It doesn't know about /v1/avatars. It just hands you the file.
|
|
83
|
+
this.$emit('file-selected', { file: event.target.files[0] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
```
|
|
88
|
+
- **Role:** The mechanic. Wraps any slotted content and makes it clickable to open a file dialog.
|
|
89
|
+
- **Logic:** Handles the hidden `<input type="file">`, listens for `change`, and **emits an event**.
|
|
90
|
+
- **Boundary:** It uses `$emit` (from `main.js`) to broadcast what happened. It never makes API calls.
|
|
91
|
+
|
|
92
|
+
#### 3. The Component (Computation a.k.a. Context)
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<!-- comp-profile-settings.lego -->
|
|
96
|
+
<template>
|
|
97
|
+
<h2>Your Profile</h2>
|
|
98
|
+
<widget-file-trigger @file-selected="uploadAvatar">
|
|
99
|
+
<block-avatar src="[[ user.avatarUrl ]]"></block-avatar>
|
|
100
|
+
</widget-file-trigger>
|
|
101
|
+
<p>Click avatar to change</p>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<script>
|
|
105
|
+
export default {
|
|
106
|
+
user: { avatarUrl: '/me.jpg' },
|
|
107
|
+
|
|
108
|
+
async uploadAvatar(event) {
|
|
109
|
+
const file = event.detail.file;
|
|
110
|
+
// BUSINESS LOGIC LIVES HERE
|
|
111
|
+
const formData = new FormData();
|
|
112
|
+
formData.append('avatar', file);
|
|
113
|
+
const res = await fetch('/v1/avatars', { method: 'POST', body: formData });
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
this.user.avatarUrl = data.url; // Reactivity updates the block-avatar
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
```
|
|
120
|
+
- **Role:** The boss. Assembles the parts and owns the API call.
|
|
121
|
+
- **Logic:** Knows about `user`, knows about `/v1/avatars`, handles the business outcome.
|
|
122
|
+
- **Boundary:** This is the *only* place that knows about your specific backend.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## The Definitions
|
|
126
|
+
|
|
127
|
+
### Blocks (Atoms)
|
|
128
|
+
|
|
129
|
+
> **TL;DR** A Block is an irreducible thing. A UI with identity, but no narrative intent.
|
|
130
|
+
|
|
131
|
+
A Block cannot be broken down into smaller Blocks. It is self-contained.
|
|
132
|
+
|
|
133
|
+
- **State:** Visual only. It can track "Is my mouse over me?" or "Am I spinning?". It never knows about User IDs, Auth tokens, or business data.
|
|
134
|
+
- **Naming:** `block-avatar`, `block-button`, `block-spinner`, `block-card`.
|
|
135
|
+
- **Rule:** If you find yourself nesting Blocks inside other Blocks, you have graduated to a Widget.
|
|
136
|
+
|
|
137
|
+
> [!WARNING] Don't Confuse Styling with Blocks
|
|
138
|
+
> You can use CSS to style many `<h1>` elements. That doesn't mean you need a `<block-title-header>`. Only create a Block when it has distinct *behavior* or *identity* beyond mere styling.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
### Widgets (Molecules)
|
|
142
|
+
|
|
143
|
+
> **TL;DR** A Widget is an interaction, not a thing.
|
|
144
|
+
|
|
145
|
+
A Widget gives Blocks a reason to exist together. It defines *how* an interaction works without embedding *who* it's for or *what* business outcome it serves.
|
|
146
|
+
|
|
147
|
+
- **State:** Internal UI state only. "Is the dropdown open?" "Which tab is active?"
|
|
148
|
+
- **Naming:** `widget-dropdown`, `widget-modal`, `widget-datepicker`, `widget-file-trigger`.
|
|
149
|
+
- **Rule:** Widgets are portable. You should be able to copy a Widget to a completely different project and it should still work.
|
|
150
|
+
- **Communication:** Uses `$emit()` to broadcast events. Never makes API calls itself.
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
### Components (Organisms)
|
|
154
|
+
|
|
155
|
+
> **TL;DR** Components are where your app features come to life.
|
|
156
|
+
|
|
157
|
+
A Component is a Widget (or set of Widgets) bound to specific data, rules, and responsibility. It's where interaction becomes meaningful to *this* application.
|
|
158
|
+
|
|
159
|
+
- **State:** Domain-specific. Owns data fetched from APIs. Knows about the current user.
|
|
160
|
+
- **Naming:** `comp-profile-settings`, `comp-order-history`, `comp-payroll-table`.
|
|
161
|
+
- **Rule:** A Component knows *who* it is for, *what* data it owns, and *what* outcome it must produce.
|
|
162
|
+
- **Communication:** Listens to Widget events (like `@file-selected`) and performs business transactions.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
### Pages (Coordination)
|
|
166
|
+
|
|
167
|
+
> **TL;DR** A Page is the top-level host that routing targets.
|
|
168
|
+
|
|
169
|
+
Pages are the uppermost hosts. They orchestrate the layout of Components within the context of a LegoDOM application.
|
|
170
|
+
|
|
171
|
+
- **Role:** Define the grid. Orchestrate Components. Handle route parameters.
|
|
172
|
+
- **Naming:** `page-dashboard`, `page-login`, `page-user-profile`.
|
|
173
|
+
- **Rule:** Pages are the *only* UI units directly known to `<lego-router>`. While a Component owns the *logic* of a feature, a Page owns the *real estate*.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
## Recommended Directory Structure
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
src/
|
|
180
|
+
├── blocks/ # Design System primitives
|
|
181
|
+
│ ├── block-avatar.lego
|
|
182
|
+
│ ├── block-button.lego
|
|
183
|
+
│ └── block-input.lego
|
|
184
|
+
├── widgets/ # Generic, portable UI tools
|
|
185
|
+
│ ├── widget-dropdown.lego
|
|
186
|
+
│ ├── widget-modal.lego
|
|
187
|
+
│ └── widget-file-trigger.lego
|
|
188
|
+
├── components/ # Domain-specific features
|
|
189
|
+
│ ├── comp-profile-settings.lego
|
|
190
|
+
│ └── comp-order-history.lego
|
|
191
|
+
├── pages/ # Route targets
|
|
192
|
+
│ ├── page-dashboard.lego
|
|
193
|
+
│ └── page-login.lego
|
|
194
|
+
└── main.js # App entry, routes, globals
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
## Scaling to Multi-Domain Apps
|
|
198
|
+
|
|
199
|
+
For large enterprise apps with multiple business domains (HRIS, Finance, Planning, Messages), the flat structure breaks down. Use a **Domain-First** approach.
|
|
200
|
+
|
|
201
|
+
### Domain-First Structure
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
src/
|
|
205
|
+
├── shared/ # Truly universal (used by 3+ domains)
|
|
206
|
+
│ ├── blocks/
|
|
207
|
+
│ │ ├── block-button.lego
|
|
208
|
+
│ │ └── block-avatar.lego
|
|
209
|
+
│ └── widgets/
|
|
210
|
+
│ ├── widget-datepicker.lego
|
|
211
|
+
│ └── widget-modal.lego
|
|
212
|
+
│
|
|
213
|
+
├── hris/ # Human Resources domain
|
|
214
|
+
│ ├── blocks/
|
|
215
|
+
│ ├── widgets/
|
|
216
|
+
│ │ └── widget-leave-calendar.lego
|
|
217
|
+
│ ├── components/
|
|
218
|
+
│ │ └── comp-employee-list.lego
|
|
219
|
+
│ └── pages/
|
|
220
|
+
│ └── page-employees.lego
|
|
221
|
+
│
|
|
222
|
+
├── finance/ # Finance domain
|
|
223
|
+
│ ├── widgets/
|
|
224
|
+
│ ├── components/
|
|
225
|
+
│ └── pages/
|
|
226
|
+
│
|
|
227
|
+
├── planning/ # Planning domain
|
|
228
|
+
│ ├── widgets/
|
|
229
|
+
│ ├── components/
|
|
230
|
+
│ └── pages/
|
|
231
|
+
│
|
|
232
|
+
└── main.js
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Rules for Multi-Domain
|
|
236
|
+
|
|
237
|
+
1. **Shared** = Only what is used by 3+ domains. Be ruthless.
|
|
238
|
+
2. **Domain folders** = Each domain owns its own `blocks/`, `widgets/`, `components/`, `pages/`.
|
|
239
|
+
3. **No cross-domain Component imports.** If HRIS needs Finance data, go through a shared service or global state, not by importing `finance/components/...`.
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
## Summary
|
|
243
|
+
|
|
244
|
+
| If you keep it all in one file... | The "Enterprise" Standard |
|
|
245
|
+
| :--- | :--- |
|
|
246
|
+
| Name it `<comp-avatar-upload>` and accept it is not reusable. | Let the **Widget** handle the interaction. Let the **Component** handle the transaction. |
|
|
247
|
+
|
|
248
|
+
**This is a recommendation.** If you find a pattern that works better for your team, use it. The goal is clarity, reusability, and reducing arguments - not rigid adherence to a doctrine.
|