lightview 1.8.2 → 2.0.1
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/.codacy/cli.sh +149 -0
- package/.codacy/codacy.yaml +15 -0
- package/.github/instructions/codacy.instructions.md +72 -0
- package/.wranglerignore +21 -0
- package/README.md +1330 -19
- package/_headers +4 -0
- package/build.js +70 -0
- package/components/actions/button.js +151 -0
- package/components/actions/dropdown.js +120 -0
- package/components/actions/modal.js +146 -0
- package/components/actions/swap.js +118 -0
- package/components/daisyui.js +288 -0
- package/components/data-display/accordion.js +128 -0
- package/components/data-display/alert.js +112 -0
- package/components/data-display/avatar.js +170 -0
- package/components/data-display/badge.js +82 -0
- package/components/data-display/card.js +151 -0
- package/components/data-display/carousel.js +94 -0
- package/components/data-display/chart.js +220 -0
- package/components/data-display/chat.js +128 -0
- package/components/data-display/collapse.js +103 -0
- package/components/data-display/countdown.js +69 -0
- package/components/data-display/diff.js +111 -0
- package/components/data-display/kbd.js +65 -0
- package/components/data-display/loading.js +75 -0
- package/components/data-display/progress.js +79 -0
- package/components/data-display/radial-progress.js +88 -0
- package/components/data-display/skeleton.js +66 -0
- package/components/data-display/stats.js +159 -0
- package/components/data-display/table.js +146 -0
- package/components/data-display/timeline.js +146 -0
- package/components/data-display/toast.js +72 -0
- package/components/data-display/tooltip.js +74 -0
- package/components/data-input/checkbox.js +253 -0
- package/components/data-input/file-input.js +224 -0
- package/components/data-input/input.js +264 -0
- package/components/data-input/radio.js +338 -0
- package/components/data-input/range.js +204 -0
- package/components/data-input/rating.js +219 -0
- package/components/data-input/select.js +287 -0
- package/components/data-input/textarea.js +287 -0
- package/components/data-input/toggle.js +201 -0
- package/components/index.js +137 -0
- package/components/layout/divider.js +72 -0
- package/components/layout/drawer.js +142 -0
- package/components/layout/footer.js +100 -0
- package/components/layout/hero.js +109 -0
- package/components/layout/indicator.js +90 -0
- package/components/layout/join.js +78 -0
- package/components/layout/navbar.js +110 -0
- package/components/navigation/breadcrumbs.js +91 -0
- package/components/navigation/dock.js +103 -0
- package/components/navigation/menu.js +126 -0
- package/components/navigation/pagination.js +105 -0
- package/components/navigation/steps.js +89 -0
- package/components/navigation/tabs.css +177 -0
- package/components/navigation/tabs.js +123 -0
- package/components/theme/theme-switch.css +65 -0
- package/components/theme/theme-switch.js +177 -0
- package/docs/about.html +164 -0
- package/docs/api/computed.html +184 -0
- package/docs/api/effects.html +173 -0
- package/docs/api/elements.html +180 -0
- package/docs/api/enhance.html +225 -0
- package/docs/api/hypermedia.html +165 -0
- package/docs/api/index.html +178 -0
- package/docs/api/nav.html +18 -0
- package/docs/api/signals.html +136 -0
- package/docs/api/state.html +217 -0
- package/docs/assets/images/logo-favicon.svg +42 -0
- package/docs/assets/images/logo-static.svg +40 -0
- package/docs/assets/images/logo.svg +66 -0
- package/docs/assets/js/examplify.js +395 -0
- package/docs/assets/styles/site.css +1102 -0
- package/docs/assets/styles/themes.css +236 -0
- package/docs/components/accordion.html +439 -0
- package/docs/components/alert.html +528 -0
- package/docs/components/avatar.html +586 -0
- package/docs/components/badge.html +531 -0
- package/docs/components/breadcrumbs.html +278 -0
- package/docs/components/button.html +579 -0
- package/docs/components/card.html +561 -0
- package/docs/components/carousel.html +286 -0
- package/docs/components/chart-area.html +702 -0
- package/docs/components/chart-bar.html +782 -0
- package/docs/components/chart-column.html +735 -0
- package/docs/components/chart-line.html +794 -0
- package/docs/components/chart-pie.html +823 -0
- package/docs/components/chart.html +610 -15
- package/docs/components/chat.html +547 -0
- package/docs/components/checkbox.html +641 -0
- package/docs/components/collapse.html +536 -0
- package/docs/components/component-nav.html +53 -0
- package/docs/components/countdown.html +470 -0
- package/docs/components/diff.html +245 -0
- package/docs/components/divider.html +240 -0
- package/docs/components/dock.html +277 -0
- package/docs/components/drawer.html +515 -0
- package/docs/components/dropdown.html +479 -0
- package/docs/components/file-input.html +591 -0
- package/docs/components/footer.html +301 -0
- package/docs/components/gallery.html +504 -0
- package/docs/components/hero.html +264 -0
- package/docs/components/index.css +840 -0
- package/docs/components/index.html +735 -0
- package/docs/components/indicator.html +342 -0
- package/docs/components/input.html +644 -0
- package/docs/components/join.html +285 -0
- package/docs/components/kbd.html +322 -0
- package/docs/components/loading.html +521 -0
- package/docs/components/menu.html +461 -0
- package/docs/components/modal.html +639 -0
- package/docs/components/navbar.html +321 -0
- package/docs/components/pagination.html +279 -0
- package/docs/components/progress.html +514 -0
- package/docs/components/radial-progress.html +434 -0
- package/docs/components/radio.html +655 -0
- package/docs/components/range.html +611 -0
- package/docs/components/rating.html +642 -0
- package/docs/components/select.html +696 -0
- package/docs/components/sidebar-setup.js +93 -0
- package/docs/components/skeleton.html +447 -0
- package/docs/components/spinner.html +68 -0
- package/docs/components/stats.html +486 -0
- package/docs/components/steps.html +356 -0
- package/docs/components/swap.html +517 -0
- package/docs/components/switch.html +68 -0
- package/docs/components/table.html +668 -0
- package/docs/components/tabs.html +506 -0
- package/docs/components/text-input.html +68 -0
- package/docs/components/textarea.html +603 -0
- package/docs/components/timeline.html +485 -42
- package/docs/components/toast.html +474 -0
- package/docs/components/toggle.html +564 -0
- package/docs/components/tooltip.html +423 -0
- package/docs/examples/getting-started-example.html +40 -0
- package/docs/examples/index.html +93 -0
- package/docs/getting-started/index.html +739 -0
- package/docs/getting-started/reviews.html +23 -0
- package/docs/getting-started/reviews.odom +108 -0
- package/docs/getting-started/reviews.vdom +84 -0
- package/docs/index.html +132 -42
- package/docs/playground.html +416 -0
- package/docs/router.html +285 -0
- package/docs/styles/index.html +190 -0
- package/functions/_middleware.js +32 -0
- package/index.html +309 -0
- package/lightview-router.js +364 -0
- package/lightview-x.js +1577 -0
- package/lightview.js +659 -1200
- package/middleware/locale.js +25 -0
- package/middleware/markdown.js +44 -0
- package/middleware/notFound.js +37 -0
- package/package.json +27 -41
- package/watch.js +92 -0
- package/wrangler.toml +12 -0
- package/.idea/lightview.iml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/LICENSE +0 -21
- package/codepen-no-tabs-embed.css +0 -2
- package/docs/CNAME +0 -1
- package/docs/api.html +0 -674
- package/docs/blank.html +0 -10
- package/docs/comparedto.html +0 -89
- package/docs/components/chart-repl.html +0 -69
- package/docs/components/components.js +0 -113
- package/docs/components/contents.html +0 -17
- package/docs/components/gantt-repl.html +0 -61
- package/docs/components/gantt.html +0 -42
- package/docs/components/gauge-repl.html +0 -66
- package/docs/components/gauge.html +0 -20
- package/docs/components/orgchart-repl.html +0 -64
- package/docs/components/orgchart.html +0 -41
- package/docs/components/repl-as-src.html +0 -17
- package/docs/components/repl-repl.html +0 -95
- package/docs/components/repl.html +0 -527
- package/docs/components/timeline-repl.html +0 -72
- package/docs/components.html +0 -14
- package/docs/css/highlightjs.min.css +0 -9
- package/docs/css/tutorial.css +0 -35
- package/docs/examples/anchor.html +0 -11
- package/docs/examples/chart.html +0 -34
- package/docs/examples/counter.html +0 -26
- package/docs/examples/counter.test.mjs +0 -47
- package/docs/examples/counter2.html +0 -26
- package/docs/examples/directives.html +0 -79
- package/docs/examples/foreign.html +0 -50
- package/docs/examples/forgeinform.html +0 -98
- package/docs/examples/form.html +0 -61
- package/docs/examples/gauge.html +0 -18
- package/docs/examples/invalid-template-literals.html +0 -44
- package/docs/examples/medium/remote.html +0 -60
- package/docs/examples/message.html +0 -18
- package/docs/examples/nested.html +0 -11
- package/docs/examples/object-bound-form.html +0 -34
- package/docs/examples/remote-server.js +0 -51
- package/docs/examples/remote.html +0 -34
- package/docs/examples/remote.json +0 -1
- package/docs/examples/scratch.html +0 -69
- package/docs/examples/sensors/index.html +0 -44
- package/docs/examples/sensors/sensor-server.js +0 -30
- package/docs/examples/shared.html +0 -41
- package/docs/examples/template.html +0 -33
- package/docs/examples/timeline.html +0 -21
- package/docs/examples/todo.html +0 -40
- package/docs/examples/top.html +0 -10
- package/docs/examples/types.html +0 -94
- package/docs/examples/xor.html +0 -62
- package/docs/examples.html +0 -25
- package/docs/javascript/codejar.min.js +0 -8
- package/docs/javascript/highlightjs.min.js +0 -1173
- package/docs/javascript/isomorphic-git.js +0 -9
- package/docs/javascript/json5.min.js +0 -1
- package/docs/javascript/lightning-fs.js +0 -1
- package/docs/javascript/lightview.js +0 -1285
- package/docs/javascript/marked.min.js +0 -6
- package/docs/javascript/peerjs.min.js +0 -70
- package/docs/javascript/turndown.js +0 -973
- package/docs/javascript/types.js +0 -606
- package/docs/javascript/utils.js +0 -45
- package/docs/lightview.html +0 -63
- package/docs/old_index.html +0 -965
- package/docs/old_index.md +0 -1132
- package/docs/slidein.html +0 -51
- package/docs/tutorial/0-getting-started.html +0 -67
- package/docs/tutorial/1-intro-to-variables.html +0 -103
- package/docs/tutorial/10-template-components.html +0 -80
- package/docs/tutorial/11-linked-components.html +0 -76
- package/docs/tutorial/12-imported-components.html +0 -67
- package/docs/tutorial/13-input-binding.html +0 -94
- package/docs/tutorial/14-automatic-variable-creation.html +0 -74
- package/docs/tutorial/15-form-binding.html +0 -110
- package/docs/tutorial/16-if-directive.html +0 -60
- package/docs/tutorial/17-loop-directives.html +0 -83
- package/docs/tutorial/18-sanitizing-and-escaping-input.html +0 -79
- package/docs/tutorial/2-imported-and-exported-variables.html +0 -80
- package/docs/tutorial/3-data-types.html +0 -89
- package/docs/tutorial/4-extended-data-types.html +0 -83
- package/docs/tutorial/5-extended-functional-types.html +0 -96
- package/docs/tutorial/5.1-extended-functional-types.html +0 -79
- package/docs/tutorial/5.2-extended-functional-types.html +0 -70
- package/docs/tutorial/6-conventional-javascript.html +0 -75
- package/docs/tutorial/7-monitoring-with-observers.html +0 -107
- package/docs/tutorial/8-event-listeners.html +0 -65
- package/docs/tutorial/9-intro-to-components.html +0 -91
- package/docs/tutorial/contents.html +0 -32
- package/docs/tutorial/my-component.html +0 -29
- package/docs/tutorial/remote-value.json +0 -4
- package/docs/websiterepl.html +0 -46
- package/jest-puppeteer.config.js +0 -5
- package/jest.config.json +0 -12
- package/lightview.min.js +0 -1
- package/lightview_good.js +0 -1267
- package/lightview_optimized.js +0 -1274
- package/repl_hold.html +0 -320
- package/test/basic.html +0 -104
- package/test/basic.test.mjs +0 -315
- package/test/extended.html +0 -29
- package/test/extended.test.mjs +0 -448
- package/types.js +0 -607
- package/unsplash.key +0 -1
package/README.md
CHANGED
|
@@ -1,41 +1,1352 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- SEO-friendly SPA Shim -->
|
|
2
|
+
<script src="./lightview-router.js"></script>
|
|
3
|
+
<script>
|
|
4
|
+
if (window.LightviewRouter) {
|
|
5
|
+
LightviewRouter.base('index.html');
|
|
6
|
+
}
|
|
7
|
+
</script>
|
|
8
|
+
# Lightview: README.md
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
A lightweight reactive UI library with signal-based reactivity and a clean API. Build dynamic UIs with automatic DOM synchronization.
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
Access the full documentaion at [lightview.dev](/index.html).
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
**Core**: ~6KB | **With Hypermedia Extensions and Component Library Support**: ~18KB total
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
Fast: This [gallery of components](/docs/components) loads in about 1 second:
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
## Modular Architecture
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
Lightview is split into two files:
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
- **`lightview.js`** - Core reactivity (signals, state, effects, elements)
|
|
23
|
+
- **`lightview-x.js`** - Hypermedia extension (src fetching, href navigation, template literals, named registries, Object DOM syntax, UI component library support)
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
### API Behavior
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
| Usage | Core Only | With `-x` |
|
|
28
|
+
|-------|-----------|-----------|
|
|
29
|
+
| `signal(5)` | ✅ Works | ✅ Works |
|
|
30
|
+
| `signal(5, "count")` | ⚠️ Ignores name | ✅ Registers |
|
|
31
|
+
| `signal.get("count")` | ❌ Undefined | ✅ Works |
|
|
32
|
+
| `signal.get("count", 0)` | ❌ Undefined | ✅ Creates if missing |
|
|
33
|
+
| `state({...})` | ✅ Works | ✅ Works |
|
|
34
|
+
| `state({...}, "app")` | ⚠️ Ignores name | ✅ Registers |
|
|
35
|
+
| `state.get("app")` | ❌ Undefined | ✅ Works |
|
|
36
|
+
| `state.get("app", {...})` | ❌ Undefined | ✅ Creates if missing |
|
|
37
|
+
| `"${signal.get('count').value}"` | ❌ No processing | ✅ Reactive template |
|
|
38
|
+
| `<div src="page.html">` | ❌ No fetching | ✅ Loads content |
|
|
39
|
+
| `<span href="other.html">` | ❌ No navigation | ✅ Reactive navigation |
|
|
40
|
+
| `{ div: { class: "x" } }` | ❌ Not recognized | ✅ Object DOM syntax |
|
|
41
|
+
| `enhance('#btn', {...})` | ❌ Undefined | ✅ Enhances existing DOM |
|
|
20
42
|
|
|
21
|
-
|
|
43
|
+
### Installation
|
|
22
44
|
|
|
23
|
-
|
|
45
|
+
```html
|
|
46
|
+
<!-- Core only (reactivity) -->
|
|
47
|
+
<script src="lightview.js"></script>
|
|
24
48
|
|
|
25
|
-
|
|
49
|
+
<!-- Full features (hypermedia + templates) -->
|
|
50
|
+
<script src="lightview.js"></script>
|
|
51
|
+
<script src="lightview-x.js"></script>
|
|
52
|
+
```
|
|
26
53
|
|
|
27
|
-
|
|
54
|
+
## Core Concepts
|
|
28
55
|
|
|
29
|
-
|
|
56
|
+
**Lightview** provides four ways to build UIs:
|
|
30
57
|
|
|
31
|
-
1
|
|
58
|
+
1. **Tagged API** - Concise, Bau.js-style syntax: `tags.div(...)`
|
|
59
|
+
2. **Element Function** - Explicit: `element('div', {}, [...])`
|
|
60
|
+
3. **vDOM Syntax** - JSON data structures: `{ tag: "div", attributes: {}, children: [] }`
|
|
61
|
+
4. **Object DOM Syntax** *(lightview-x)* - Compact: `{ div: { class: "foo", children: [] } }`
|
|
32
62
|
|
|
33
|
-
|
|
63
|
+
All four approaches use the same underlying reactive system based on **signals**.
|
|
34
64
|
|
|
35
|
-
|
|
65
|
+
## Installation
|
|
36
66
|
|
|
37
|
-
|
|
67
|
+
```html
|
|
68
|
+
<script src="lightview.js"></script>
|
|
69
|
+
```
|
|
38
70
|
|
|
71
|
+
## Quick Start
|
|
39
72
|
|
|
73
|
+
### Style 1: Tagged API
|
|
40
74
|
|
|
75
|
+
```javascript
|
|
76
|
+
const lv = new Lightview();
|
|
77
|
+
const { signal, computed, tags } = lv;
|
|
78
|
+
const { div, h1, p, button } = tags;
|
|
41
79
|
|
|
80
|
+
const count = signal(0);
|
|
81
|
+
const doubled = computed(() => count.value * 2);
|
|
82
|
+
|
|
83
|
+
const app = div({ class: 'container' },
|
|
84
|
+
h1('Counter App'),
|
|
85
|
+
p(() => `Count: ${count.value}`),
|
|
86
|
+
p(() => `Doubled: ${doubled.value}`),
|
|
87
|
+
button({ onclick: () => count.value++ }, 'Increment'),
|
|
88
|
+
button({ onclick: () => count.value-- }, 'Decrement')
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
document.body.appendChild(app.domEl);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Style 2: Element Function
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const { signal, element } = new Lightview();
|
|
98
|
+
|
|
99
|
+
const count = signal(0);
|
|
100
|
+
|
|
101
|
+
const app = element('div', { class: 'container' }, [
|
|
102
|
+
element('h1', {}, ['Counter App']),
|
|
103
|
+
element('p', {}, [() => `Count: ${count.value}`]),
|
|
104
|
+
element('button', {
|
|
105
|
+
onclick: () => count.value++
|
|
106
|
+
}, ['Increment'])
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
document.body.appendChild(app.domEl);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Style 3: vDOM Syntax (Plain JSON)
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
const { signal, element } = new Lightview();
|
|
116
|
+
|
|
117
|
+
const count = signal(0);
|
|
118
|
+
|
|
119
|
+
const app = element('div', { class: 'container' }, [
|
|
120
|
+
{
|
|
121
|
+
tag: 'h1',
|
|
122
|
+
attributes: {},
|
|
123
|
+
children: ['Counter App']
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
tag: 'p',
|
|
127
|
+
attributes: {},
|
|
128
|
+
children: [() => `Count: ${count()}`] // or count.value
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
tag: 'button',
|
|
132
|
+
attributes: { onclick: () => count.value++ }, // or count(count() + 1)
|
|
133
|
+
children: ['Increment']
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
document.body.appendChild(app.domEl);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Style 4: Object DOM Syntax (lightview-x)
|
|
142
|
+
|
|
143
|
+
Object DOM syntax provides a more compact way to define elements. Instead of `{ tag, attributes, children }`, you use `{ tag: { ...attributes, children } }`.
|
|
144
|
+
|
|
145
|
+
**Requires lightview-x.js** and must be enabled:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// Enable Object DOM syntax (call once at startup)
|
|
149
|
+
LightviewX.useObjectDOMSyntax(); // Non-strict mode (default)
|
|
150
|
+
LightviewX.useObjectDOMSyntax(true); // Strict mode - validates HTML tag names
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
const { signal, element, tags } = Lightview;
|
|
155
|
+
const { div, button } = tags;
|
|
156
|
+
|
|
157
|
+
// Enable Object DOM syntax
|
|
158
|
+
LightviewX.useObjectDOMSyntax();
|
|
159
|
+
|
|
160
|
+
const count = signal(0);
|
|
161
|
+
|
|
162
|
+
// Object DOM syntax in children arrays
|
|
163
|
+
const app = div({ class: 'container' },
|
|
164
|
+
{ h1: { children: ['Counter App'] } },
|
|
165
|
+
{ p: { children: [() => `Count: ${count.value}`] } },
|
|
166
|
+
{ button: { onclick: () => count.value++, children: ['Increment'] } }
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
document.body.appendChild(app.domEl);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Comparison:**
|
|
173
|
+
|
|
174
|
+
| vDOM Syntax | Object DOM Syntax |
|
|
175
|
+
|-------------|-------------------|
|
|
176
|
+
| `{ tag: 'div', attributes: { class: 'box' }, children: ['Hello'] }` | `{ div: { class: 'box', children: ['Hello'] } }` |
|
|
177
|
+
|
|
178
|
+
**Pros & Cons:**
|
|
179
|
+
|
|
180
|
+
| Aspect | vDOM Syntax | Object DOM Syntax |
|
|
181
|
+
|--------|-------------|-------------------|
|
|
182
|
+
| **Verbosity** | More verbose | More compact |
|
|
183
|
+
| **Explicit** | ✅ Clear structure, easy to validate | ⚠️ Tag name is a dynamic key |
|
|
184
|
+
| **Serialization** | ✅ Easy to serialize/deserialize | ⚠️ Requires detection logic |
|
|
185
|
+
| **Reserved words** | ✅ None - `children` is just a property | ⚠️ `children` is reserved |
|
|
186
|
+
| **TypeScript** | ✅ Easy to type | ⚠️ Harder to provide autocomplete |
|
|
187
|
+
| **Dynamic tags** | ✅ `{ tag: myVar, ... }` | ⚠️ Requires `{ [myVar]: {...} }` |
|
|
188
|
+
| **Multiple elements** | ✅ Can have array of objects | ⚠️ One element per object |
|
|
189
|
+
| **Readability** | Familiar to React/vDOM users | Cleaner for static templates |
|
|
190
|
+
|
|
191
|
+
**Why vDOM is the default:**
|
|
192
|
+
|
|
193
|
+
1. **Unambiguous parsing** - The presence of `tag` clearly identifies an element. Object DOM requires heuristics to detect (single key that's a valid tag name).
|
|
194
|
+
|
|
195
|
+
2. **No reserved attribute names** - In vDOM, you can have an attribute literally named `children`. In Object DOM, `children` is reserved for child elements.
|
|
196
|
+
|
|
197
|
+
3. **Better for data interchange** - vDOM objects can be safely serialized to JSON and parsed back without any special handling. They're self-describing.
|
|
198
|
+
|
|
199
|
+
4. **Predictable validation** - Easy to check `if (obj.tag)` vs. finding the unknown key and checking if it's a valid tag.
|
|
200
|
+
|
|
201
|
+
5. **Works without extensions** - vDOM is supported by core `lightview.js`. Object DOM requires `lightview-x.js`.
|
|
202
|
+
|
|
203
|
+
Object DOM is ideal for **hand-written templates** where brevity matters, or **configuration files** where you want a cleaner syntax. Use vDOM when you need **programmatic generation**, **serialization**, or **maximum compatibility**.
|
|
204
|
+
|
|
205
|
+
**Nested Example:**
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
// Object DOM - compact and readable
|
|
209
|
+
{ div: {
|
|
210
|
+
class: 'card',
|
|
211
|
+
children: [
|
|
212
|
+
{ h2: { children: ['Title'] } },
|
|
213
|
+
{ p: { style: 'color: gray', children: ['Description'] } },
|
|
214
|
+
{ button: { onclick: handleClick, children: ['Action'] } }
|
|
215
|
+
]
|
|
216
|
+
}}
|
|
217
|
+
|
|
218
|
+
// Equivalent vDOM
|
|
219
|
+
{ tag: 'div', attributes: { class: 'card' }, children: [
|
|
220
|
+
{ tag: 'h2', attributes: {}, children: ['Title'] },
|
|
221
|
+
{ tag: 'p', attributes: { style: 'color: gray' }, children: ['Description'] },
|
|
222
|
+
{ tag: 'button', attributes: { onclick: handleClick }, children: ['Action'] }
|
|
223
|
+
]}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Strict Mode:**
|
|
227
|
+
|
|
228
|
+
When `useObjectDOMSyntax(true)` is called, tag names are validated using the browser's own HTML parser. Unknown tags like `foo` or `notreal` will be rejected, while standard HTML tags and valid custom elements (with hyphens) are accepted.
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
LightviewX.useObjectDOMSyntax(true); // Enable strict validation
|
|
232
|
+
|
|
233
|
+
// Valid - browser recognizes these
|
|
234
|
+
{ div: { children: ['OK'] } } // Standard HTML tag
|
|
235
|
+
{ 'my-widget': { children: ['OK'] } } // Custom element (valid in browser)
|
|
236
|
+
|
|
237
|
+
// Invalid in strict mode (HTMLUnknownElement - won't be detected as Object DOM)
|
|
238
|
+
{ notarealtag: { children: ['Nope'] } } // Browser returns HTMLUnknownElement
|
|
239
|
+
|
|
240
|
+
// You can also check directly:
|
|
241
|
+
LightviewX.isKnownHTMLTag('div'); // true
|
|
242
|
+
LightviewX.isKnownHTMLTag('my-widget'); // true (custom elements are valid)
|
|
243
|
+
LightviewX.isKnownHTMLTag('faketag'); // false (HTMLUnknownElement)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Style 5: Component Functions
|
|
247
|
+
|
|
248
|
+
The `tag` property (or Object DOM key) can be a **function** instead of a string. This enables reusable components that return HTML, DOM nodes, vDOM, or Object DOM.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
const { element, signal, tags } = Lightview;
|
|
252
|
+
const { div, button } = tags;
|
|
253
|
+
|
|
254
|
+
// Define a component function
|
|
255
|
+
const Card = (props) => ({
|
|
256
|
+
div: {
|
|
257
|
+
class: 'card',
|
|
258
|
+
style: `border: 1px solid ${props.borderColor || '#ccc'}; padding: 16px;`,
|
|
259
|
+
children: [
|
|
260
|
+
{ h3: { children: [props.title] } },
|
|
261
|
+
{ p: { children: [props.description] } },
|
|
262
|
+
...(props.children || [])
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Use component with element() - tag is a function
|
|
268
|
+
const app = element('div', {}, [
|
|
269
|
+
{ tag: Card, attributes: { title: 'Hello', description: 'A card component' } },
|
|
270
|
+
{ tag: Card, attributes: { title: 'World', borderColor: 'blue', children: [
|
|
271
|
+
button({ onclick: () => alert('Clicked!') }, 'Click Me')
|
|
272
|
+
]}}
|
|
273
|
+
]);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Component Return Types:**
|
|
277
|
+
|
|
278
|
+
Components can return any of these formats:
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
// 1. Object DOM (recommended for simplicity)
|
|
282
|
+
const Badge = (props) => ({
|
|
283
|
+
span: { class: 'badge', children: [props.text] }
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// 2. vDOM
|
|
287
|
+
const Badge = (props) => ({
|
|
288
|
+
tag: 'span',
|
|
289
|
+
attributes: { class: 'badge' },
|
|
290
|
+
children: [props.text]
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// 3. HTML string
|
|
294
|
+
const Badge = (props) => `<span class="badge">${props.text}</span>`;
|
|
295
|
+
|
|
296
|
+
// 4. DOM node
|
|
297
|
+
const Badge = (props) => {
|
|
298
|
+
const el = document.createElement('span');
|
|
299
|
+
el.className = 'badge';
|
|
300
|
+
el.textContent = props.text;
|
|
301
|
+
return el;
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Registering Components:**
|
|
306
|
+
|
|
307
|
+
If you add a component to Ligtvhiew.tags, then you can treate it ligke any other tag. It will even work with Object DOM syntax.
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
// Register individual components
|
|
311
|
+
Lightview.tags['Badge'] = Badge;
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
const { div, Badge } = tags;
|
|
316
|
+
const app = div(
|
|
317
|
+
Badge({ text: 'New' })
|
|
318
|
+
);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
// Now use by name in Object DOM syntax
|
|
323
|
+
LightviewX.useObjectDOMSyntax();
|
|
324
|
+
|
|
325
|
+
const app2 = div(
|
|
326
|
+
{ Badge: { text: 'New' } }
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**With Global Scope:**
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
// Define component globally
|
|
334
|
+
window.Alert = (props) => ({
|
|
335
|
+
div: {
|
|
336
|
+
class: `alert alert-${props.type || 'info'}`,
|
|
337
|
+
children: [props.message]
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Enable global lookup
|
|
342
|
+
LightviewX.useObjectDOMSyntax();
|
|
343
|
+
|
|
344
|
+
// Use directly
|
|
345
|
+
const app = div(
|
|
346
|
+
{ Alert: { type: 'success', message: 'Operation completed!' } }
|
|
347
|
+
);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## API Reference
|
|
351
|
+
|
|
352
|
+
### Lightview Class
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
const lv = new Lightview();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Creates a Lightview instance with:
|
|
359
|
+
- `signal(value)` - Create reactive state
|
|
360
|
+
- `computed(fn)` - Create derived state
|
|
361
|
+
- `effect(fn)` - Run side effects
|
|
362
|
+
- `state(obj)` - Create deep reactive store
|
|
363
|
+
- `element(tag, attrs, children)` - Create elements
|
|
364
|
+
- `tags` - Proxy for creating elements: `tags.div()`, `tags.button()`, etc.
|
|
365
|
+
|
|
366
|
+
### Signals
|
|
367
|
+
|
|
368
|
+
Signals in Lightview are versatile. They can be used as **function calls** or by accessing the `.value` property. This dual API allows for flexible coding styles.
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
const name = signal('John');
|
|
372
|
+
|
|
373
|
+
// 1. Property Access Style
|
|
374
|
+
// Read
|
|
375
|
+
console.log(name.value); // 'John'
|
|
376
|
+
// Write
|
|
377
|
+
name.value = 'Jane';
|
|
378
|
+
|
|
379
|
+
// 2. Function Call Style
|
|
380
|
+
// Read
|
|
381
|
+
console.log(name()); // 'Jane'
|
|
382
|
+
// Write
|
|
383
|
+
name('Bob');
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
You can choose whichever style you prefer or mix them as needed. Both methods are fully reactive and interoperable.
|
|
387
|
+
|
|
388
|
+
### Working with Objects
|
|
389
|
+
|
|
390
|
+
Lightview signals are **shallow** by design. This keeps the library extremely small and fast (performant). When working with nested objects, use immutable patterns to update state:
|
|
391
|
+
|
|
392
|
+
```javascript
|
|
393
|
+
const user = signal({ name: "Joe", address: { city: "NYC" } });
|
|
394
|
+
|
|
395
|
+
// ❌ Don't mutate directly (won't trigger updates)
|
|
396
|
+
// user.value.address.city = "LA";
|
|
397
|
+
|
|
398
|
+
// ✅ Do use immutable patterns
|
|
399
|
+
user.value = {
|
|
400
|
+
...user.value,
|
|
401
|
+
address: { ...user.value.address, city: "LA" }
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
### Computed Signals
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
const firstName = signal('John');
|
|
410
|
+
const lastName = signal('Doe');
|
|
411
|
+
|
|
412
|
+
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
|
413
|
+
|
|
414
|
+
console.log(fullName.value); // 'John Doe'
|
|
415
|
+
firstName.value = 'Jane';
|
|
416
|
+
console.log(fullName.value); // 'Jane Doe'
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### State (Store)
|
|
420
|
+
|
|
421
|
+
For deeply nested objects or grouped state, use `state()`. It creates a reactive proxy where every property is automatically backed by a signal.
|
|
422
|
+
|
|
423
|
+
```javascript
|
|
424
|
+
/*
|
|
425
|
+
Creates a deep reactive store.
|
|
426
|
+
Accessing properties (e.g. s.user.name) automatically tracks dependencies.
|
|
427
|
+
Setting properties automatically triggers updates.
|
|
428
|
+
*/
|
|
429
|
+
const appState = state({
|
|
430
|
+
user: {
|
|
431
|
+
name: 'Alice',
|
|
432
|
+
settings: { theme: 'dark' }
|
|
433
|
+
},
|
|
434
|
+
count: 0
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Reading tracks dependencies
|
|
438
|
+
effect(() => {
|
|
439
|
+
console.log(`${appState.user.name} likes ${appState.user.settings.theme} mode`);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Writing triggers updates
|
|
443
|
+
appState.user.name = 'Bob';
|
|
444
|
+
appState.user.settings.theme = 'light';
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Note: `state` objects read/write like normal JavaScript objects (no `.value` needed).
|
|
448
|
+
|
|
449
|
+
### Reactive Arrays and Dates
|
|
450
|
+
|
|
451
|
+
**Lightview has a unique feature**: it can make Arrays and Date objects fully reactive, even when using their native methods. This is something most reactive libraries don't support!
|
|
452
|
+
|
|
453
|
+
#### Reactive Arrays
|
|
454
|
+
|
|
455
|
+
When you wrap an array in `state()`, Lightview automatically tracks changes to the array's `length` property. This means array mutation methods like `push()`, `pop()`, `splice()`, `shift()`, `unshift()`, etc. will trigger reactive updates:
|
|
456
|
+
|
|
457
|
+
```javascript
|
|
458
|
+
// Style 1: Tagged API
|
|
459
|
+
const { state, effect, tags } = new Lightview();
|
|
460
|
+
const { div, ul, li, button } = tags;
|
|
461
|
+
|
|
462
|
+
const items = state(['Apple', 'Banana']);
|
|
463
|
+
|
|
464
|
+
// This effect automatically re-runs when the array length changes
|
|
465
|
+
effect(() => {
|
|
466
|
+
console.log(`Array has ${items.length} items`);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// All these methods trigger reactivity!
|
|
470
|
+
items.push('Cherry'); // Logs: "Array has 3 items"
|
|
471
|
+
items.pop(); // Logs: "Array has 2 items"
|
|
472
|
+
items.splice(1, 0, 'Date'); // Logs: "Array has 3 items"
|
|
473
|
+
|
|
474
|
+
// Use in UI
|
|
475
|
+
const app = div(
|
|
476
|
+
() => ul(
|
|
477
|
+
...items.map(item => li(item))
|
|
478
|
+
),
|
|
479
|
+
button({ onclick: () => items.push('New Item') }, 'Add Item')
|
|
480
|
+
);
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Array elements now have full deep reactivity!** Both the array's `length` and individual element properties are tracked:
|
|
484
|
+
|
|
485
|
+
```javascript
|
|
486
|
+
// Style 3: Plain JSON - state works with any API style
|
|
487
|
+
const { state, element } = new Lightview();
|
|
488
|
+
|
|
489
|
+
const items = state([
|
|
490
|
+
{ name: 'Item 1', done: false },
|
|
491
|
+
{ name: 'Item 2', done: true }
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
// Using Plain JSON structure with reactive list:
|
|
495
|
+
const list = element('div', {}, [
|
|
496
|
+
// Wrap in a function to re-render when items.length changes
|
|
497
|
+
() => ({
|
|
498
|
+
tag: 'ul',
|
|
499
|
+
attributes: {},
|
|
500
|
+
children: items.map(item => ({
|
|
501
|
+
tag: 'li',
|
|
502
|
+
attributes: { class: () => item.done ? 'completed' : '' },
|
|
503
|
+
children: [() => item.name]
|
|
504
|
+
}))
|
|
505
|
+
}),
|
|
506
|
+
{
|
|
507
|
+
tag: 'button',
|
|
508
|
+
attributes: { onclick: () => items.push({ name: `Item ${items.length + 1}`, done: false }) },
|
|
509
|
+
children: ['Add Item']
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
tag: 'button',
|
|
513
|
+
attributes: { onclick: () => items[0].done = !items[0].done },
|
|
514
|
+
children: ['Toggle First']
|
|
515
|
+
}
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
// All of these trigger UI updates:
|
|
519
|
+
// items.push({ name: 'Item 3', done: false }); // ✅ Reactive (length changed)
|
|
520
|
+
// items[0].done = true; // ✅ Reactive (element property changed)
|
|
521
|
+
// items[1].name = 'Updated Item'; // ✅ Reactive (nested property changed)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### Reactive Dates
|
|
525
|
+
|
|
526
|
+
Date objects are notoriously difficult to make reactive in most frameworks because their mutation methods (`setDate()`, `setHours()`, etc.) change internal state without changing the object reference. Lightview solves this by monitoring the `getTime()` value:
|
|
527
|
+
|
|
528
|
+
```javascript
|
|
529
|
+
// Style 2: Element Function
|
|
530
|
+
const { state, effect, element } = new Lightview();
|
|
531
|
+
|
|
532
|
+
const currentDate = state(new Date());
|
|
533
|
+
|
|
534
|
+
// This effect re-runs whenever the date's timestamp changes
|
|
535
|
+
effect(() => {
|
|
536
|
+
console.log(`Current time: ${currentDate.getTime()}`);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// All date mutation methods trigger reactivity!
|
|
540
|
+
currentDate.setHours(12); // Triggers update
|
|
541
|
+
currentDate.setDate(15); // Triggers update
|
|
542
|
+
currentDate.setFullYear(2025); // Triggers update
|
|
543
|
+
|
|
544
|
+
// Use in UI
|
|
545
|
+
const clock = element('div', {}, [
|
|
546
|
+
element('p', {}, [() => `Time: ${currentDate.toLocaleTimeString()}`]),
|
|
547
|
+
element('button', {
|
|
548
|
+
onclick: () => currentDate.setTime(Date.now())
|
|
549
|
+
}, ['Update to Now'])
|
|
550
|
+
]);
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Why this matters**: In most reactive libraries (Vue, Solid, Svelte), you'd need to create a new Date object to trigger updates:
|
|
554
|
+
|
|
555
|
+
```javascript
|
|
556
|
+
// What you'd have to do in Vue/Solid: Although, it will still work in Lightview
|
|
557
|
+
myDate.value = new Date(myDate.value.setHours(12)); // ❌ Awkward!
|
|
558
|
+
|
|
559
|
+
// What you can do in Lightview:
|
|
560
|
+
myDate.setHours(12); // ✅ Just works!
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Effects
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
```javascript
|
|
567
|
+
const count = signal(0);
|
|
568
|
+
|
|
569
|
+
effect(() => {
|
|
570
|
+
console.log(`Count is now: ${count.value}`);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
count.value = 1; // Logs: "Count is now: 1"
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Element Structure
|
|
577
|
+
|
|
578
|
+
Every element has:
|
|
579
|
+
- `tag` - HTML tag name or component function
|
|
580
|
+
- `attributes` - Object of attributes
|
|
581
|
+
- `children` - Array of child elements/text/functions
|
|
582
|
+
- `domEl` - The actual DOM node (read-only getter)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
## Reactive Features
|
|
587
|
+
|
|
588
|
+
### Reactive Text
|
|
589
|
+
|
|
590
|
+
```javascript
|
|
591
|
+
// Style 2: Element Function
|
|
592
|
+
const { element, signal } = new Lightview();
|
|
593
|
+
|
|
594
|
+
const name = signal('World');
|
|
595
|
+
|
|
596
|
+
const app = element('div', {}, [
|
|
597
|
+
element('h1', {}, [() => `Hello, ${name.value}!`]),
|
|
598
|
+
element('button', { onclick: () => name.value = 'Lightview' }, ['Change Name'])
|
|
599
|
+
]);
|
|
600
|
+
// Click the button to see the greeting update
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Reactive Attributes
|
|
604
|
+
|
|
605
|
+
```javascript
|
|
606
|
+
// Style 1: Tagged API
|
|
607
|
+
const { tags, signal } = new Lightview();
|
|
608
|
+
const { div, button } = tags;
|
|
609
|
+
|
|
610
|
+
const isActive = signal(false);
|
|
611
|
+
|
|
612
|
+
const app = div(
|
|
613
|
+
button({
|
|
614
|
+
class: () => isActive.value ? 'active' : 'inactive',
|
|
615
|
+
disabled: () => !isActive.value
|
|
616
|
+
}, 'I am toggled'),
|
|
617
|
+
button({ onclick: () => isActive.value = !isActive.value }, 'Toggle Active')
|
|
618
|
+
);
|
|
619
|
+
// Click 'Toggle Active' to enable/disable the first button
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Reactive Styles
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
// Style 3: Plain JSON
|
|
626
|
+
const { element, signal } = new Lightview();
|
|
627
|
+
|
|
628
|
+
const color = signal('blue');
|
|
629
|
+
|
|
630
|
+
const box = element('div', {}, [
|
|
631
|
+
{
|
|
632
|
+
tag: 'div',
|
|
633
|
+
attributes: {
|
|
634
|
+
style: () => ({
|
|
635
|
+
backgroundColor: color.value,
|
|
636
|
+
padding: '20px',
|
|
637
|
+
transition: 'background-color 0.3s'
|
|
638
|
+
})
|
|
639
|
+
},
|
|
640
|
+
children: [() => `Color: ${color.value}`]
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
tag: 'button',
|
|
644
|
+
attributes: { onclick: () => color.value = 'red' },
|
|
645
|
+
children: ['Red']
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
tag: 'button',
|
|
649
|
+
attributes: { onclick: () => color.value = 'green' },
|
|
650
|
+
children: ['Green']
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
tag: 'button',
|
|
654
|
+
attributes: { onclick: () => color.value = 'blue' },
|
|
655
|
+
children: ['Blue']
|
|
656
|
+
}
|
|
657
|
+
]);
|
|
658
|
+
// Click the color buttons to change the box color
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Reactive Lists
|
|
662
|
+
|
|
663
|
+
```javascript
|
|
664
|
+
// Style 2: Element Function
|
|
665
|
+
const { element, signal } = new Lightview();
|
|
666
|
+
|
|
667
|
+
const items = signal(['Apple', 'Banana', 'Cherry']);
|
|
668
|
+
|
|
669
|
+
const list = element('div', {}, [
|
|
670
|
+
() => element('ul', {},
|
|
671
|
+
items.value.map(item =>
|
|
672
|
+
element('li', {}, [item])
|
|
673
|
+
)
|
|
674
|
+
),
|
|
675
|
+
element('button', {
|
|
676
|
+
onclick: () => items.value = [...items.value, 'New Fruit']
|
|
677
|
+
}, ['Add Fruit']),
|
|
678
|
+
element('button', {
|
|
679
|
+
onclick: () => items.value = items.value.slice(0, -1)
|
|
680
|
+
}, ['Remove Last'])
|
|
681
|
+
]);
|
|
682
|
+
// Click buttons to add or remove items from the list
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
## Lifecycle & Cleanup
|
|
686
|
+
|
|
687
|
+
### Lifecycle Hooks
|
|
688
|
+
|
|
689
|
+
Lightview provides built-in lifecycle hooks for elements, allowing you to run code when an element enters or leaves the DOM.
|
|
690
|
+
|
|
691
|
+
```javascript
|
|
692
|
+
// Style 2: Element Function
|
|
693
|
+
const { element } = new Lightview();
|
|
694
|
+
|
|
695
|
+
/*
|
|
696
|
+
onmount: Called when the element is added to the DOM.
|
|
697
|
+
onunmount: Called when the element is removed from the DOM.
|
|
698
|
+
*/
|
|
699
|
+
const timer = element('div', {
|
|
700
|
+
onmount: (el) => {
|
|
701
|
+
console.log('Timer mounted!');
|
|
702
|
+
el._interval = setInterval(() => console.log('Tick'), 1000);
|
|
703
|
+
},
|
|
704
|
+
onunmount: (el) => {
|
|
705
|
+
console.log('Timer removed!');
|
|
706
|
+
clearInterval(el._interval);
|
|
707
|
+
}
|
|
708
|
+
}, ['I am a timer']);
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
These hooks are robust and triggered whether you remove elements via Lightview logic or standard DOM methods (like `element.remove()` or `innerHTML = ''`).
|
|
712
|
+
|
|
713
|
+
### Automatic Cleanup
|
|
714
|
+
|
|
715
|
+
One of Lightview's most powerful features is its **fully automatic memory management**.
|
|
716
|
+
|
|
717
|
+
* **Self-Cleaning Effects**: When an element is removed from the DOM, Lightview automatically stops all reactive effects (signals, computed values) attached to it.
|
|
718
|
+
* **Leak Prevention**: You don't need to manually unsubscribe from signals. The built-in `MutationObserver` watches the document and cleans up dependencies instantly when nodes are detached.
|
|
719
|
+
|
|
720
|
+
## Complete Examples
|
|
721
|
+
|
|
722
|
+
### Todo App
|
|
723
|
+
|
|
724
|
+
```javascript
|
|
725
|
+
const lv = new Lightview();
|
|
726
|
+
const { signal, tags } = lv;
|
|
727
|
+
const { div, h1, input, button, span } = tags;
|
|
728
|
+
|
|
729
|
+
const state = {
|
|
730
|
+
todos: signal([]),
|
|
731
|
+
input: signal(''),
|
|
732
|
+
filter: signal('all')
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const addTodo = () => {
|
|
736
|
+
if (state.input.value.trim()) {
|
|
737
|
+
state.todos.value = [...state.todos.value, {
|
|
738
|
+
id: Date.now(),
|
|
739
|
+
text: signal(state.input.value),
|
|
740
|
+
done: signal(false)
|
|
741
|
+
}];
|
|
742
|
+
state.input.value = '';
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const filteredTodos = lv.computed(() => {
|
|
747
|
+
const todos = state.todos.value;
|
|
748
|
+
const filter = state.filter.value;
|
|
749
|
+
|
|
750
|
+
if (filter === 'active') return todos.filter(t => !t.done.value);
|
|
751
|
+
if (filter === 'completed') return todos.filter(t => t.done.value);
|
|
752
|
+
return todos;
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const app = div({ class: 'todo-app' },
|
|
756
|
+
h1('Lightview Todos'),
|
|
757
|
+
|
|
758
|
+
div({ class: 'input-row' },
|
|
759
|
+
input({
|
|
760
|
+
placeholder: 'What needs to be done?',
|
|
761
|
+
value: () => state.input.value,
|
|
762
|
+
oninput: (e) => state.input.value = e.target.value,
|
|
763
|
+
onkeypress: (e) => { if (e.key === 'Enter') addTodo(); }
|
|
764
|
+
}),
|
|
765
|
+
button({ onclick: addTodo }, 'Add')
|
|
766
|
+
),
|
|
767
|
+
|
|
768
|
+
div({ class: 'filters' },
|
|
769
|
+
button({
|
|
770
|
+
class: () => state.filter.value === 'all' ? 'active' : '',
|
|
771
|
+
onclick: () => state.filter.value = 'all'
|
|
772
|
+
}, 'All'),
|
|
773
|
+
button({
|
|
774
|
+
class: () => state.filter.value === 'active' ? 'active' : '',
|
|
775
|
+
onclick: () => state.filter.value = 'active'
|
|
776
|
+
}, 'Active'),
|
|
777
|
+
button({
|
|
778
|
+
class: () => state.filter.value === 'completed' ? 'active' : '',
|
|
779
|
+
onclick: () => state.filter.value = 'completed'
|
|
780
|
+
}, 'Completed')
|
|
781
|
+
),
|
|
782
|
+
|
|
783
|
+
() => div({ class: 'todo-list' },
|
|
784
|
+
...filteredTodos.value.map(todo =>
|
|
785
|
+
div({ class: 'todo-item' },
|
|
786
|
+
input({
|
|
787
|
+
type: 'checkbox',
|
|
788
|
+
checked: () => todo.done.value,
|
|
789
|
+
onchange: () => todo.done.value = !todo.done.value
|
|
790
|
+
}),
|
|
791
|
+
span({
|
|
792
|
+
class: () => todo.done.value ? 'completed' : '',
|
|
793
|
+
ondblclick: () => {
|
|
794
|
+
const newText = prompt('Edit:', todo.text.value);
|
|
795
|
+
if (newText !== null) todo.text.value = newText;
|
|
796
|
+
}
|
|
797
|
+
}, () => todo.text.value),
|
|
798
|
+
button({
|
|
799
|
+
class: 'delete',
|
|
800
|
+
onclick: () => {
|
|
801
|
+
state.todos.value = state.todos.value.filter(t => t.id !== todo.id);
|
|
802
|
+
}
|
|
803
|
+
}, '×')
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
document.body.appendChild(app.domEl);
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### Form with Validation
|
|
813
|
+
|
|
814
|
+
```javascript
|
|
815
|
+
const lv = new Lightview();
|
|
816
|
+
const { signal, computed, tags } = lv;
|
|
817
|
+
const { form, div, label, input, span, button } = tags;
|
|
818
|
+
|
|
819
|
+
const email = signal('');
|
|
820
|
+
const password = signal('');
|
|
821
|
+
|
|
822
|
+
const isValidEmail = computed(() =>
|
|
823
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
const isValidPassword = computed(() =>
|
|
827
|
+
password.value.length >= 8
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const canSubmit = computed(() =>
|
|
831
|
+
isValidEmail.value && isValidPassword.value
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
const formEl = form({
|
|
835
|
+
onsubmit: (e) => {
|
|
836
|
+
e.preventDefault();
|
|
837
|
+
if (canSubmit.value) { // or canSubmit()
|
|
838
|
+
alert('Form submitted!');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
div(
|
|
843
|
+
label('Email:'),
|
|
844
|
+
input({
|
|
845
|
+
type: 'email',
|
|
846
|
+
value: () => email.value,
|
|
847
|
+
oninput: (e) => email.value = e.target.value,
|
|
848
|
+
class: () => email.value && !isValidEmail.value ? 'invalid' : ''
|
|
849
|
+
}),
|
|
850
|
+
() => email.value && !isValidEmail.value
|
|
851
|
+
? span({ class: 'error' }, 'Invalid email')
|
|
852
|
+
: span()
|
|
853
|
+
),
|
|
854
|
+
|
|
855
|
+
div(
|
|
856
|
+
label('Password:'),
|
|
857
|
+
input({
|
|
858
|
+
type: 'password',
|
|
859
|
+
value: () => password.value,
|
|
860
|
+
oninput: (e) => password.value = e.target.value,
|
|
861
|
+
class: () => password.value && !isValidPassword.value ? 'invalid' : ''
|
|
862
|
+
}),
|
|
863
|
+
() => password.value && !isValidPassword.value
|
|
864
|
+
? span({ class: 'error' }, 'Must be 8+ characters')
|
|
865
|
+
: span()
|
|
866
|
+
),
|
|
867
|
+
|
|
868
|
+
button({
|
|
869
|
+
type: 'submit',
|
|
870
|
+
disabled: () => !canSubmit.value
|
|
871
|
+
}, 'Submit')
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
document.body.appendChild(formEl.domEl);
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Dynamic Chart
|
|
878
|
+
|
|
879
|
+
```javascript
|
|
880
|
+
const lv = new Lightview();
|
|
881
|
+
const { signal, tags } = lv;
|
|
882
|
+
const { div, button } = tags;
|
|
883
|
+
|
|
884
|
+
const data = signal([10, 20, 15, 30, 25]);
|
|
885
|
+
|
|
886
|
+
const addDataPoint = () => {
|
|
887
|
+
data.value = [...data.value, Math.floor(Math.random() * 50)];
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const chart = div({ class: 'chart' },
|
|
891
|
+
button({ onclick: addDataPoint }, 'Add Data Point'),
|
|
892
|
+
|
|
893
|
+
() => div({ class: 'bars' },
|
|
894
|
+
...data.value.map((value, i) =>
|
|
895
|
+
div({
|
|
896
|
+
class: 'bar',
|
|
897
|
+
style: () => ({
|
|
898
|
+
height: `${value * 3}px`,
|
|
899
|
+
width: '40px',
|
|
900
|
+
backgroundColor: `hsl(${i * 40}, 70%, 50%)`,
|
|
901
|
+
display: 'inline-block',
|
|
902
|
+
margin: '0 2px',
|
|
903
|
+
transition: 'height 0.3s'
|
|
904
|
+
})
|
|
905
|
+
}, value.toString())
|
|
906
|
+
)
|
|
907
|
+
)
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
document.body.appendChild(chart.domEl);
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
## Hypermedia
|
|
914
|
+
|
|
915
|
+
Lightview enhances the use of 'href' and 'src' across any element.
|
|
916
|
+
|
|
917
|
+
## Smart `src` Attribute
|
|
918
|
+
|
|
919
|
+
Lightview enhances the `src` attribute with smart loading capabilities. You can use it to inject content from external files or other parts of the DOM.
|
|
920
|
+
|
|
921
|
+
### 1. Fetching Content (HTML/JSON)
|
|
922
|
+
If `src` is a file path, Lightview fetches it:
|
|
923
|
+
|
|
924
|
+
```javascript
|
|
925
|
+
// Style 2: Element Function
|
|
926
|
+
const { element } = new Lightview();
|
|
927
|
+
|
|
928
|
+
// Fetches header.html and parses it into reactive elements
|
|
929
|
+
element('div', { src: '/components/header.html' }, [])
|
|
930
|
+
|
|
931
|
+
// Fetches data.json and converts it to elements
|
|
932
|
+
element('div', { src: '/api/data.json' }, [])
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### 2. DOM Cloning
|
|
936
|
+
If `src` is a CSS selector, Lightview clones the targeted DOM elements:
|
|
937
|
+
|
|
938
|
+
```javascript
|
|
939
|
+
// Style 3: Plain JSON
|
|
940
|
+
const { element } = new Lightview();
|
|
941
|
+
|
|
942
|
+
// Clones the element with id="template-sidebar"
|
|
943
|
+
element('div', {}, [
|
|
944
|
+
{
|
|
945
|
+
tag: 'div',
|
|
946
|
+
attributes: { src: '#template-sidebar' },
|
|
947
|
+
children: []
|
|
948
|
+
}
|
|
949
|
+
])
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
This is useful for using hidden templates or duplicating content.
|
|
953
|
+
|
|
954
|
+
### 3. Interactive `href` Attribute
|
|
955
|
+
On elements (like `div`, `button`, etc.), a non-standard `href` attribute acts as a click trigger for the `src` behavior. When the element is clicked, its `src` attribute is set to the value of its `href`, triggering the content loading or cloning.
|
|
956
|
+
|
|
957
|
+
```javascript
|
|
958
|
+
// Style 1: Tagged API
|
|
959
|
+
const { tags } = new Lightview();
|
|
960
|
+
const { div, button } = tags;
|
|
961
|
+
|
|
962
|
+
// On click, this div will load 'content.html' into itself
|
|
963
|
+
div({ href: 'content.html' }, 'Click to load content')
|
|
964
|
+
|
|
965
|
+
// On click, this button will clone #modal-template into itself
|
|
966
|
+
div({ href: '#modal-template' }, 'Open Modal')
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
## HTML Template Literals
|
|
970
|
+
|
|
971
|
+
Lightview-X supports reactive template literals in external HTML and JSON files. This allows you to create reusable templates that automatically bind to your global signals and state objects using standard JavaScript template literal syntax.
|
|
972
|
+
|
|
973
|
+
**Note:** This feature requires `lightview-x.js` and uses named signals/state registered globally.
|
|
974
|
+
|
|
975
|
+
### Template Literals in HTML Files
|
|
976
|
+
|
|
977
|
+
Create HTML templates with `${...}` expressions that reference named signals:
|
|
978
|
+
|
|
979
|
+
**template.html:**
|
|
980
|
+
```html
|
|
981
|
+
<div class="card">
|
|
982
|
+
<h3>Welcome, User!</h3>
|
|
983
|
+
<p>Your status: <strong>${signal.get('userStatus').value}</strong></p>
|
|
984
|
+
<p>Messages: <span>${signal.get('messageCount').value}</span></p>
|
|
985
|
+
<p>Last updated: ${new Date().toLocaleTimeString()}</p>
|
|
986
|
+
</div>
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**main.js:**
|
|
990
|
+
```javascript
|
|
991
|
+
const { signal, tags } = Lightview;
|
|
992
|
+
const { section, button } = tags;
|
|
993
|
+
|
|
994
|
+
// Register named signals that the template will reference
|
|
995
|
+
const userStatus = signal('Online', 'userStatus');
|
|
996
|
+
const messageCount = signal(5, 'messageCount');
|
|
997
|
+
|
|
998
|
+
// Load and render the template
|
|
999
|
+
const app = section({ src: './template.html' });
|
|
1000
|
+
|
|
1001
|
+
// Update signals - reload template to see changes
|
|
1002
|
+
userStatus.value = 'Away';
|
|
1003
|
+
messageCount.value++;
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
### Template Literals in JSON Files
|
|
1007
|
+
|
|
1008
|
+
JSON templates use the same `${...}` syntax within string values to access registered state, provide defaults, or do calculations and conditional logic. You can load these with the 'src' attribute on any element.
|
|
1009
|
+
|
|
1010
|
+
#### Supported JSON Formats
|
|
1011
|
+
|
|
1012
|
+
JSON files loaded via `src` support both **vDOM** and **Object DOM** formats:
|
|
1013
|
+
|
|
1014
|
+
| Format | Structure | Requirement |
|
|
1015
|
+
|--------|-----------|-------------|
|
|
1016
|
+
| **vDOM** | `{ "tag": "div", "attributes": {...}, "children": [...] }` | Works by default |
|
|
1017
|
+
| **Object DOM** | `{ "div": { "class": "foo", "children": [...] } }` | Requires `LightviewX.useObjectDOMSyntax()` |
|
|
1018
|
+
|
|
1019
|
+
#### vDOM Format (Default)
|
|
1020
|
+
|
|
1021
|
+
**template.json:**
|
|
1022
|
+
```json
|
|
1023
|
+
[
|
|
1024
|
+
{
|
|
1025
|
+
"tag": "div",
|
|
1026
|
+
"attributes": { "class": "card" },
|
|
1027
|
+
"children": [
|
|
1028
|
+
{
|
|
1029
|
+
"tag": "h3",
|
|
1030
|
+
"children": ["Product: ${state.get('product',{name:'Widget One',price:0,inStock:3}).name}"]
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
"tag": "p",
|
|
1034
|
+
"children": ["Price: $${state.get('product').price}"]
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
"tag": "p",
|
|
1038
|
+
"children": ["In Stock: ${state.get('product').inStock ? 'Yes ✅' : 'No ❌'}"]
|
|
1039
|
+
}
|
|
1040
|
+
]
|
|
1041
|
+
}
|
|
1042
|
+
]
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
#### Object DOM Format
|
|
1046
|
+
|
|
1047
|
+
To use the more compact Object DOM format in JSON files, enable it before loading:
|
|
1048
|
+
|
|
1049
|
+
**template-objectdom.json:**
|
|
1050
|
+
```json
|
|
1051
|
+
[
|
|
1052
|
+
{
|
|
1053
|
+
"div": {
|
|
1054
|
+
"class": "card",
|
|
1055
|
+
"children": [
|
|
1056
|
+
{ "h3": { "children": ["Product: ${state.get('product').name}"] } },
|
|
1057
|
+
{ "p": { "children": ["Price: $${state.get('product').price}"] } },
|
|
1058
|
+
{ "p": { "children": ["In Stock: ${state.get('product').inStock ? 'Yes ✅' : 'No ❌'}"] } }
|
|
1059
|
+
]
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
]
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**main.js:**
|
|
1066
|
+
```javascript
|
|
1067
|
+
// Enable Object DOM syntax BEFORE loading JSON templates
|
|
1068
|
+
LightviewX.useObjectDOMSyntax();
|
|
1069
|
+
|
|
1070
|
+
const { state, tags } = Lightview;
|
|
1071
|
+
const { section } = tags;
|
|
1072
|
+
|
|
1073
|
+
const product = state({ name: 'Widget', price: 29.99, inStock: true }, 'product');
|
|
1074
|
+
|
|
1075
|
+
// Now JSON files can use Object DOM format
|
|
1076
|
+
const app = section({ src: './template-objectdom.json' });
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
**main.js:**
|
|
1080
|
+
```javascript
|
|
1081
|
+
const { state, tags } = Lightview;
|
|
1082
|
+
const { section, button } = tags;
|
|
1083
|
+
|
|
1084
|
+
// Register named state that the template will reference
|
|
1085
|
+
const product = state({
|
|
1086
|
+
name: 'Lightview Widget',
|
|
1087
|
+
price: 29.99,
|
|
1088
|
+
inStock: true
|
|
1089
|
+
}, 'product');
|
|
1090
|
+
|
|
1091
|
+
// Load and render the JSON template
|
|
1092
|
+
const app = section({ src: './template.json' });
|
|
1093
|
+
|
|
1094
|
+
// Update state values
|
|
1095
|
+
product.name = 'Super Widget';
|
|
1096
|
+
product.price = 39.99;
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### Reloading Templates
|
|
1100
|
+
|
|
1101
|
+
To see updated values after changing signals/state, reload the template by re-setting the `src` attribute:
|
|
1102
|
+
|
|
1103
|
+
```javascript
|
|
1104
|
+
const { signal, state, tags } = Lightview;
|
|
1105
|
+
const { section, button, div } = tags;
|
|
1106
|
+
|
|
1107
|
+
// Named signal and state
|
|
1108
|
+
const counter = signal(0, 'counter');
|
|
1109
|
+
const user = state({ name: 'Alice' }, 'user');
|
|
1110
|
+
|
|
1111
|
+
const app = div(
|
|
1112
|
+
section({ src: './dashboard.html', id: 'dashboard' }),
|
|
1113
|
+
|
|
1114
|
+
button({ onclick: () => counter.value++ }, 'Increment'),
|
|
1115
|
+
button({ onclick: () => user.name = 'Bob' }, 'Change User'),
|
|
1116
|
+
|
|
1117
|
+
button({ onclick: () => {
|
|
1118
|
+
// Reload template to reflect updated values
|
|
1119
|
+
const container = document.getElementById('dashboard');
|
|
1120
|
+
const el = Lightview.internals.domToElement.get(container);
|
|
1121
|
+
if (el) {
|
|
1122
|
+
el.attributes = { ...el.attributes, src: './dashboard.html?' + Date.now() };
|
|
1123
|
+
}
|
|
1124
|
+
}}, 'Reload Template')
|
|
1125
|
+
);
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
### Why Use Template Literals?
|
|
1129
|
+
|
|
1130
|
+
- **Separation of Concerns**: Keep HTML structure in `.html` files, logic in `.js` files
|
|
1131
|
+
- **Reusable Templates**: Share templates across different parts of your application
|
|
1132
|
+
- **Server-Side Templates**: Generate templates on the server with dynamic `${...}` expressions
|
|
1133
|
+
- **CMS Integration**: Non-developers can edit HTML templates without touching JavaScript
|
|
1134
|
+
|
|
1135
|
+
## Enhancing Existing DOM Elements
|
|
1136
|
+
|
|
1137
|
+
Lightview-X provides the `enhance()` function to add reactivity to existing DOM elements. This is useful for progressive enhancement - adding interactivity to server-rendered HTML or gradually migrating a codebase without rebuilding the entire page.
|
|
1138
|
+
|
|
1139
|
+
**Note:** This feature requires `lightview-x.js`.
|
|
1140
|
+
|
|
1141
|
+
### Basic Usage
|
|
1142
|
+
|
|
1143
|
+
```html
|
|
1144
|
+
<!-- Existing HTML (server-rendered, static HTML, etc.) -->
|
|
1145
|
+
<button id="counter-btn" class="btn">Clicked 0 times</button>
|
|
1146
|
+
<div id="status-display">Status: Unknown</div>
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
```javascript
|
|
1150
|
+
const { signal, state } = Lightview;
|
|
1151
|
+
|
|
1152
|
+
// Get or create a named signal with default value
|
|
1153
|
+
const counter = signal.get('counter', 0);
|
|
1154
|
+
|
|
1155
|
+
// Enhance the button with reactivity
|
|
1156
|
+
LightviewX.enhance('#counter-btn', {
|
|
1157
|
+
innerText: () => `Clicked ${counter.value} times`,
|
|
1158
|
+
onclick: () => counter.value++
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
// Enhance with innerHTML for richer content
|
|
1162
|
+
LightviewX.enhance('#status-display', {
|
|
1163
|
+
innerHTML: () => `Status: <strong>${counter.value > 5 ? 'Active' : 'Idle'}</strong>`
|
|
1164
|
+
});
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
### API
|
|
1168
|
+
|
|
1169
|
+
```javascript
|
|
1170
|
+
LightviewX.enhance(selectorOrNode, options)
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
**Parameters:**
|
|
1174
|
+
- `selectorOrNode` - CSS selector string or DOM element
|
|
1175
|
+
- `options` - Object containing:
|
|
1176
|
+
- `innerText` - Static string or reactive function for text content
|
|
1177
|
+
- `innerHTML` - Static string or reactive function for HTML content
|
|
1178
|
+
- `on*` - Event handlers (`onclick`, `oninput`, etc.)
|
|
1179
|
+
- Any other attribute (reactive functions supported)
|
|
1180
|
+
|
|
1181
|
+
**Returns:** Lightview reactive element wrapper, or `null` if element not found.
|
|
1182
|
+
|
|
1183
|
+
### signal.get() and state.get() with Defaults
|
|
1184
|
+
|
|
1185
|
+
When using `enhance()`, you often need to access or create global state. The `.get()` method now supports a default value that creates and registers the signal/state if it doesn't exist:
|
|
1186
|
+
|
|
1187
|
+
```javascript
|
|
1188
|
+
// If 'counter' doesn't exist, creates signal(0) and registers it as 'counter'
|
|
1189
|
+
const counter = signal.get('counter', 0);
|
|
1190
|
+
|
|
1191
|
+
// If 'user' doesn't exist, creates state({name: 'Guest'}) and registers it as 'user'
|
|
1192
|
+
const user = state.get('user', { name: 'Guest' });
|
|
1193
|
+
|
|
1194
|
+
// Without default - returns undefined if not registered
|
|
1195
|
+
const maybeCounter = signal.get('counter');
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
This pattern is similar to `getOrCreate` - the default value is only used if the signal/state hasn't been registered yet.
|
|
1199
|
+
|
|
1200
|
+
### Complete Example
|
|
1201
|
+
|
|
1202
|
+
```html
|
|
1203
|
+
<!DOCTYPE html>
|
|
1204
|
+
<html>
|
|
1205
|
+
<body>
|
|
1206
|
+
<!-- Server-rendered content -->
|
|
1207
|
+
<div class="card">
|
|
1208
|
+
<h2 id="greeting">Hello, Guest!</h2>
|
|
1209
|
+
<p id="click-count">Clicks: 0</p>
|
|
1210
|
+
<button id="increment">Click Me</button>
|
|
1211
|
+
<button id="change-name">Change Name</button>
|
|
1212
|
+
</div>
|
|
1213
|
+
|
|
1214
|
+
<script src="lightview.js"></script>
|
|
1215
|
+
<script src="lightview-x.js"></script>
|
|
1216
|
+
<script>
|
|
1217
|
+
const { signal, state } = Lightview;
|
|
1218
|
+
|
|
1219
|
+
// Create or get global state with defaults
|
|
1220
|
+
const clicks = signal.get('clicks', 0);
|
|
1221
|
+
const user = state.get('user', { name: 'Guest' });
|
|
1222
|
+
|
|
1223
|
+
// Enhance existing elements
|
|
1224
|
+
LightviewX.enhance('#greeting', {
|
|
1225
|
+
innerText: () => `Hello, ${user.name}!`
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
LightviewX.enhance('#click-count', {
|
|
1229
|
+
innerText: () => `Clicks: ${clicks.value}`
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
LightviewX.enhance('#increment', {
|
|
1233
|
+
onclick: () => clicks.value++
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
LightviewX.enhance('#change-name', {
|
|
1237
|
+
onclick: () => {
|
|
1238
|
+
user.name = user.name === 'Guest' ? 'Alice' : 'Guest';
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
</script>
|
|
1242
|
+
</body>
|
|
1243
|
+
</html>
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
### When to Use enhance()
|
|
1247
|
+
|
|
1248
|
+
- **Progressive Enhancement**: Add interactivity to static HTML
|
|
1249
|
+
- **Server-Side Rendering**: Hydrate server-rendered content
|
|
1250
|
+
- **Legacy Integration**: Add reactivity to existing applications
|
|
1251
|
+
- **CMS Content**: Enhance content from a CMS without rebuilding
|
|
1252
|
+
- **Third-Party Widgets**: Add reactive behavior to elements you don't control
|
|
1253
|
+
|
|
1254
|
+
### enhance() vs element()
|
|
1255
|
+
|
|
1256
|
+
| Use Case | Use `enhance()` | Use `element()` |
|
|
1257
|
+
|----------|-----------------|------------------|
|
|
1258
|
+
| Existing HTML | ✅ | ❌ |
|
|
1259
|
+
| Build from scratch | ❌ | ✅ |
|
|
1260
|
+
| Server-rendered | ✅ | ❌ |
|
|
1261
|
+
| Full control over structure | ❌ | ✅ |
|
|
1262
|
+
| Progressive enhancement | ✅ | ❌ |
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
## Framework Comparison
|
|
1266
|
+
|
|
1267
|
+
Lightview offers a unique combination of features that sets it apart from other reactive libraries. Here's how it compares:
|
|
1268
|
+
|
|
1269
|
+
### Features
|
|
1270
|
+
|
|
1271
|
+
| Feature | Lightview | Vue 3 | SolidJS | Svelte | Bau.js | Juris.js |
|
|
1272
|
+
|---------|-----------|-------|---------|--------|--------|----------|
|
|
1273
|
+
| **Bundle Size** | ~6.5KB | ~33KB | ~7KB | ~2KB* | ~2KB | ~50KB |
|
|
1274
|
+
| **Reactivity Model** | Signals/Proxy | Proxy | Signals | Compiler | Proxy | Intentional |
|
|
1275
|
+
| **No Build Required** | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
|
1276
|
+
| **No JSX/Templates** | ✅ | ❌ (SFC) | ❌ (JSX) | ❌ (Templates) | ✅ | ✅ |
|
|
1277
|
+
| **Tagged API** | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
|
1278
|
+
| **Plain Objects** | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
1279
|
+
| **Hypermedia** *| ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
1280
|
+
| **HTML Template Literals** *| ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
1281
|
+
|
|
1282
|
+
*Svelte size is after compilation. However, substantive Svelete apps are often larger than those created with other libraries due to the lack of rutime abstractions after compilation/transpilation.
|
|
1283
|
+
|
|
1284
|
+
*Lighview Hypermedia and HTML Template Literals require lightview-x, an additional 5K.
|
|
1285
|
+
|
|
1286
|
+
### Reactive Capabilities
|
|
1287
|
+
|
|
1288
|
+
| Feature | Lightview | Vue 3 | SolidJS | Svelte | Bau.js | Juris.js |
|
|
1289
|
+
|---------|-----------|-------|---------|--------|--------|----------|
|
|
1290
|
+
| **Array Mutations** | ✅ Auto | ✅ Auto | ❌ Manual** | ✅ Auto | ✅ Auto | ❌ Manual |
|
|
1291
|
+
| **Array Element Mutations** | ✅ **Auto** | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual |
|
|
1292
|
+
| **Date Mutations** | ✅ **Auto** | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual |
|
|
1293
|
+
| **Deep Reactivity** | ✅ | ✅ | ✅*** | ✅ | ✅ | ✅ |
|
|
1294
|
+
| **Computed Values** | ✅ | ✅ | ✅ | ✅ | ✅ (derive) | ✅ |
|
|
1295
|
+
| **Ehancing Existing HTML** | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
1296
|
+
| **Fine-grained Updates** | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
|
1297
|
+
| **Auto Cleanup** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
1298
|
+
|
|
1299
|
+
**SolidJS supports array mutations with `createStore`, but not with `createSignal`
|
|
1300
|
+
***SolidJS requires `createStore` for deep reactivity
|
|
1301
|
+
|
|
1302
|
+
### Developer Experience
|
|
1303
|
+
|
|
1304
|
+
| Feature | Lightview | Vue 3 | SolidJS | Svelte | Bau.js | Juris.js |
|
|
1305
|
+
|---------|-----------|-------|---------|--------|--------|----------|
|
|
1306
|
+
| **Learning Curve** | Low | Medium | Medium | Low | Low | Medium |
|
|
1307
|
+
| **TypeScript** | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
|
1308
|
+
| **DevTools** | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
|
1309
|
+
| **Lifecycle Hooks** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
1310
|
+
| **Progressive Enhancement** | ✅ (src/href) | ❌ | ❌ | ❌ | ❌ | ✅ (enhance) |
|
|
1311
|
+
| **Multiple Instances** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
|
1312
|
+
|
|
1313
|
+
### Unique Features
|
|
1314
|
+
|
|
1315
|
+
**Lightview's Standout Features:**
|
|
1316
|
+
- 🎯 **Reactive Date Objects** - Only library with automatic Date mutation tracking
|
|
1317
|
+
- 📊 **Reactive Array Elements** - Full deep reactivity for array elements (rare feature!)
|
|
1318
|
+
- 🏷️ **Tagged API** - Bau.js-inspired concise syntax (`tags.div()`)
|
|
1319
|
+
- 🔗 **Smart src/href** - Load HTML/JSON or clone DOM elements declaratively
|
|
1320
|
+
- 📝 **HTML Template Literals** - Use `${signal.get('name').value}` in external HTML/JSON files
|
|
1321
|
+
- 🔧 **Progressive Enhancement** - Enhance existing DOM with `enhance()` for server-rendered content
|
|
1322
|
+
- 🧩 **Component Functions** - Use functions as tags, with registry and global scope support
|
|
1323
|
+
- 🧹 **Automatic Cleanup** - MutationObserver-based memory management
|
|
1324
|
+
- 📦 **Zero Dependencies** - Pure JavaScript, no build tools needed
|
|
1325
|
+
- 🎨 **Five API Styles** - Tagged, Element function, vDOM JSON, Object DOM JSON, Component functions
|
|
1326
|
+
|
|
1327
|
+
**When to Choose Lightview:**
|
|
1328
|
+
- ✅ You want minimal bundle size with maximum features
|
|
1329
|
+
- ✅ You need reactive Date objects (calendars, timers, scheduling apps)
|
|
1330
|
+
- ✅ You prefer no build step and pure JavaScript
|
|
1331
|
+
- ✅ You like the simplicity of Bau.js but want more power
|
|
1332
|
+
- ✅ You're building hypermedia-driven applications
|
|
1333
|
+
|
|
1334
|
+
**When to Choose Alternatives:**
|
|
1335
|
+
- Vue 3: Large ecosystem, TypeScript, mature tooling
|
|
1336
|
+
- SolidJS: Maximum performance, fine-grained reactivity
|
|
1337
|
+
- Svelte: Best DX, compiler optimizations
|
|
1338
|
+
- Bau.js: Even simpler API, minimal features
|
|
1339
|
+
- Juris.js: Object-first architecture, intentional reactivity
|
|
1340
|
+
|
|
1341
|
+
## Browser Support
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
Modern browsers with Proxy support (ES6+):
|
|
1345
|
+
- Chrome 49+
|
|
1346
|
+
- Firefox 18+
|
|
1347
|
+
- Safari 10+
|
|
1348
|
+
- Edge 12+
|
|
1349
|
+
|
|
1350
|
+
## License
|
|
1351
|
+
|
|
1352
|
+
MIT
|