@dinoreic/fez 0.4.0 → 0.5.2
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/README.md +723 -198
- package/bin/fez +16 -6
- package/bin/fez-compile +347 -0
- package/bin/fez-debug +25 -0
- package/bin/fez-index +16 -4
- package/bin/refactor +699 -0
- package/dist/fez.js +142 -33
- package/dist/fez.js.map +4 -4
- package/fez.d.ts +533 -0
- package/package.json +25 -15
- package/src/fez/compile.js +396 -164
- package/src/fez/connect.js +250 -143
- package/src/fez/defaults.js +275 -84
- package/src/fez/instance.js +673 -514
- package/src/fez/lib/await-helper.js +64 -0
- package/src/fez/lib/global-state.js +22 -4
- package/src/fez/lib/index.js +140 -0
- package/src/fez/lib/localstorage.js +44 -0
- package/src/fez/lib/n.js +38 -23
- package/src/fez/lib/pubsub.js +208 -0
- package/src/fez/lib/svelte-template-lib.js +339 -0
- package/src/fez/lib/svelte-template.js +472 -0
- package/src/fez/lib/template.js +114 -119
- package/src/fez/morph.js +384 -0
- package/src/fez/root.js +284 -164
- package/src/fez/utility.js +319 -149
- package/src/fez/utils/dump.js +114 -84
- package/src/fez/utils/highlight_all.js +1 -1
- package/src/fez.js +65 -43
- package/src/rollup.js +1 -1
- package/src/svelte-cde-adapter.coffee +21 -12
- package/src/fez/vendor/idiomorph.js +0 -860
package/README.md
CHANGED
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
Check the Demo site https://dux.github.io/fez/
|
|
6
6
|
|
|
7
|
-
FEZ is a small library (
|
|
7
|
+
FEZ is a small library (49KB minified, ~18KB gzipped) that allows writing of [Custom DOM elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components/Using_custom_elements) in a clean and easy-to-understand way.
|
|
8
8
|
|
|
9
9
|
It uses
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Latest version of libs are baked in Fez distro.
|
|
11
|
+
- [Goober](https://goober.js.org/) to enable runtime SCSS (similar to styled components)
|
|
12
|
+
- Custom component-aware DOM differ to morph DOM from one state to another (as React or Stimulus/Turbo does it), with hash-based render skipping for zero-cost no-op renders
|
|
15
13
|
|
|
16
14
|
It uses minimal abstraction. You will learn to use it in 15 minutes, just look at examples, it includes all you need to know.
|
|
17
15
|
|
|
@@ -19,6 +17,57 @@ It uses minimal abstraction. You will learn to use it in 15 minutes, just look a
|
|
|
19
17
|
|
|
20
18
|
`<script src="https://dux.github.io/fez/dist/fez.js"></script>`
|
|
21
19
|
|
|
20
|
+
## CLI Tools
|
|
21
|
+
|
|
22
|
+
Fez provides command-line tools for development:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Compile and validate Fez components
|
|
26
|
+
bunx @dinoreic/fez compile demo/fez/*.fez
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install globally:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bun add -g @dinoreic/fez
|
|
33
|
+
fez compile my-component.fez
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why Fez is Simpler
|
|
37
|
+
|
|
38
|
+
| Concept | React | Svelte 5 | Vue 3 | **Fez** |
|
|
39
|
+
| ----------------- | ------------------------ | --------------- | ---------------------- | ------------------ |
|
|
40
|
+
| State | `useState`, `useReducer` | `$state` rune | `ref`, `reactive` | `this.state.x = y` |
|
|
41
|
+
| Computed | `useMemo` | `$derived` rune | `computed` | Just use a method |
|
|
42
|
+
| Side effects | `useEffect` | `$effect` rune | `watch`, `watchEffect` | `afterRender()` |
|
|
43
|
+
| Global state | Context, Redux, Zustand | stores | Pinia | `this.globalState` |
|
|
44
|
+
| Re-render control | `memo`, `useMemo`, keys | `{#key}` | `v-memo` | Automatic |
|
|
45
|
+
|
|
46
|
+
**No special syntax. No runes. No hooks. No compiler magic.** Just plain JavaScript:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
class MyComponent extends FezBase {
|
|
50
|
+
init() {
|
|
51
|
+
this.state.count = 0; // reactive - nested changes tracked too
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
increment() {
|
|
55
|
+
this.state.count++; // triggers re-render automatically
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get doubled() {
|
|
59
|
+
// computed value - just a getter
|
|
60
|
+
return this.state.count * 2;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The whole mental model:
|
|
66
|
+
|
|
67
|
+
1. Change `this.state` -> component re-renders
|
|
68
|
+
2. Component-aware differ updates only what changed (child components preserved automatically)
|
|
69
|
+
3. Hash-based skip avoids DOM work entirely when template output is identical
|
|
70
|
+
|
|
22
71
|
## Little more details
|
|
23
72
|
|
|
24
73
|
Uses DOM as a source of truth and tries to be as close to vanilla JS as possible. There is nothing to learn or "fight", or overload or "monkey patch" or anything. It just works.
|
|
@@ -29,80 +78,327 @@ It replaces modern JS frameworks by using native Autonomous Custom Elements to c
|
|
|
29
78
|
|
|
30
79
|
This article, [Web Components Will Replace Your Frontend Framework](https://www.dannymoerkerke.com/blog/web-components-will-replace-your-frontend-framework/), is from 2019. Join the future, ditch React, Angular and other never defined, always "evolving" monstrosities. Vanilla is the way :)
|
|
31
80
|
|
|
32
|
-
There is no some "internal state" that is by some magic reflected to DOM. No! All methods Fez use to manipulate DOM are just helpers around native DOM interface. Work on DOM raw, use built in [node builder](https://github.com/dux/fez/blob/main/src/lib/n.js) or full template mapping with
|
|
81
|
+
There is no some "internal state" that is by some magic reflected to DOM. No! All methods Fez use to manipulate DOM are just helpers around native DOM interface. Work on DOM raw, use built in [node builder](https://github.com/dux/fez/blob/main/src/lib/n.js) or full template mapping with DOM morphing.
|
|
33
82
|
|
|
34
83
|
## How it works
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
85
|
+
- define your custom component - `Fez('ui-foo', class UiFoo extends FezBase)`
|
|
86
|
+
- add HTML - `<ui-foo bar="baz" id="node1"></ui-foo>`
|
|
87
|
+
- lib will call `node1.fez.init()` when node is added to DOM and connect your component to dom.
|
|
88
|
+
- use `Fez` helper methods, or do all by yourself, all good.
|
|
40
89
|
|
|
41
90
|
That is all.
|
|
42
91
|
|
|
43
|
-
##
|
|
92
|
+
## Template Syntax (Svelte-like)
|
|
44
93
|
|
|
45
|
-
|
|
94
|
+
Fez uses a Svelte-inspired template syntax with single braces `{ }` for expressions and block directives.
|
|
95
|
+
|
|
96
|
+
### Expressions
|
|
46
97
|
|
|
47
98
|
```html
|
|
48
|
-
<!--
|
|
49
|
-
<
|
|
50
|
-
|
|
99
|
+
<!-- Simple expression -->
|
|
100
|
+
<div>{state.name}</div>
|
|
101
|
+
|
|
102
|
+
<!-- Expressions in attributes (automatically quoted) -->
|
|
103
|
+
<input value={state.text} class={state.active ? 'active' : ''} />
|
|
104
|
+
|
|
105
|
+
<!-- Raw HTML (unescaped) -->
|
|
106
|
+
<div>{@html state.htmlContent}</div>
|
|
107
|
+
|
|
108
|
+
<!-- JSON debug output -->
|
|
109
|
+
{@json state.data}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Conditionals
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
{#if state.isLoggedIn}
|
|
116
|
+
<p>Welcome, {state.username}!</p>
|
|
117
|
+
{:else if state.isGuest}
|
|
118
|
+
<p>Hello, Guest!</p>
|
|
119
|
+
{:else}
|
|
120
|
+
<p>Please log in</p>
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
<!-- Unless (opposite of if) -->
|
|
124
|
+
<!-- renders if state.items is null, undefined, empty array, or empty object -->
|
|
125
|
+
{#unless state.items}
|
|
126
|
+
<p>No items found</p>
|
|
127
|
+
{/unless}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Truthiness rules** for `#if`, `#unless`, and `:else if`:
|
|
131
|
+
|
|
132
|
+
- `null`, `undefined`, `false`, `0`, `""` → **falsy**
|
|
133
|
+
- `[]` (empty array) → **falsy**
|
|
134
|
+
- `{}` (empty object) → **falsy**
|
|
135
|
+
- Non-empty arrays, non-empty objects, and other truthy values → **truthy**
|
|
136
|
+
|
|
137
|
+
### Loops
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<!-- Each loop with implicit index 'i' -->
|
|
141
|
+
{#each state.items as item}
|
|
142
|
+
<li>{item.name} (index: {i})</li>
|
|
143
|
+
{/each}
|
|
144
|
+
|
|
145
|
+
<!-- Each loop with explicit index -->
|
|
146
|
+
{#each state.items as item, index}
|
|
147
|
+
<li>{index}: {item.name}</li>
|
|
148
|
+
{/each}
|
|
149
|
+
|
|
150
|
+
<!-- For loop syntax -->
|
|
151
|
+
{#for item in state.items}
|
|
152
|
+
<li>{item}</li>
|
|
153
|
+
{/for}
|
|
154
|
+
|
|
155
|
+
<!-- For loop with index -->
|
|
156
|
+
{#for item, idx in state.items}
|
|
157
|
+
<li>{idx}: {item}</li>
|
|
158
|
+
{/for}
|
|
159
|
+
|
|
160
|
+
<!-- Object iteration (2-param = key/value pairs) -->
|
|
161
|
+
{#for key, val in state.config}
|
|
162
|
+
<div>{key} = {val}</div>
|
|
163
|
+
{/for}
|
|
164
|
+
|
|
165
|
+
<!-- Object iteration with index (3 params) -->
|
|
166
|
+
{#each state.config as key, value, index}
|
|
167
|
+
<div>{index}. {key} = {value}</div>
|
|
168
|
+
{/each}
|
|
169
|
+
|
|
170
|
+
<!-- Nested values stay intact (not deconstructed) -->
|
|
171
|
+
{#for key, user in state.users}
|
|
172
|
+
<div>{key}: {user.name}</div>
|
|
173
|
+
{/for}
|
|
174
|
+
|
|
175
|
+
<!-- Empty list fallback with :else -->
|
|
176
|
+
{#each state.items as item}
|
|
177
|
+
<li>{item}</li>
|
|
178
|
+
{:else}
|
|
179
|
+
<li>No items found</li>
|
|
180
|
+
{/each}
|
|
181
|
+
|
|
182
|
+
<!-- :else also works with #for -->
|
|
183
|
+
{#for item in state.items}
|
|
184
|
+
<span>{item}</span>
|
|
185
|
+
{:else}
|
|
186
|
+
<p>List is empty</p>
|
|
187
|
+
{/for}
|
|
188
|
+
|
|
189
|
+
<!-- Child components in loops - automatically optimized -->
|
|
190
|
+
<!-- Use :prop="expr" to pass objects/functions (not just strings) -->
|
|
191
|
+
{#each state.users as user}
|
|
192
|
+
<user-card :user="user" />
|
|
193
|
+
{/each}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Loop behavior:**
|
|
197
|
+
|
|
198
|
+
- **null/undefined = empty list** - no errors, renders nothing (or `:else` block if present)
|
|
199
|
+
- **2-param syntax** (`key, val` or `item, idx`) works for both arrays and objects:
|
|
200
|
+
- Arrays: first = value, second = index
|
|
201
|
+
- Objects: first = key, second = value
|
|
202
|
+
- **Brackets optional** - `{#for key, val in obj}` same as `{#for [key, val] in obj}`
|
|
203
|
+
|
|
204
|
+
**Note on passing props:** Use `:prop="expr"` syntax to pass JavaScript objects, arrays, or functions as props. Regular `prop={expr}` will stringify the value.
|
|
205
|
+
|
|
206
|
+
**Component Isolation:** Child components in loops are automatically preserved during parent re-renders. They only re-render when their props actually change - making loops with many items very efficient.
|
|
207
|
+
|
|
208
|
+
### Preserving Elements with `fez:keep`
|
|
209
|
+
|
|
210
|
+
Use `fez:keep` to preserve plain HTML elements across parent re-renders. The element is only recreated when its `fez:keep` value changes.
|
|
211
|
+
|
|
212
|
+
**Important:** `fez:keep` must only be used on plain HTML elements (`div`, `span`, `input`, etc.), **never on fez components**. To preserve a fez component, wrap it in a plain HTML element with `fez:keep`:
|
|
213
|
+
|
|
214
|
+
```html
|
|
215
|
+
<!-- Wrap child components in a plain element with fez:keep -->
|
|
216
|
+
{#each state.users as user}
|
|
217
|
+
<span fez:keep="user-{user.id}">
|
|
218
|
+
<user-card :user="user" />
|
|
219
|
+
</span>
|
|
220
|
+
{/each}
|
|
221
|
+
|
|
222
|
+
<!-- Wrap components in loops -->
|
|
223
|
+
{#for i in [0,1,2,3,4]}
|
|
224
|
+
<span fez:keep="star-{i}-{state.rating}-{state.color}">
|
|
225
|
+
<ui-star fill="{getStarFill(i)}" color="{state.color}" />
|
|
226
|
+
</span>
|
|
227
|
+
{/for}
|
|
228
|
+
|
|
229
|
+
<!-- Preserve form inputs to keep user-entered values -->
|
|
230
|
+
<input fez:keep="search-input" type="text" />
|
|
231
|
+
|
|
232
|
+
<!-- Preserve animation state -->
|
|
233
|
+
<div fez:keep="animated-element" class="slide-in">...</div>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**How it works:**
|
|
237
|
+
|
|
238
|
+
- Same `fez:keep` value → Element is fully preserved (no re-render, all state intact)
|
|
239
|
+
- Different `fez:keep` value → Element is recreated from scratch
|
|
240
|
+
- No `fez:keep` → Element may be recreated on every parent re-render
|
|
241
|
+
|
|
242
|
+
**When to use:**
|
|
243
|
+
|
|
244
|
+
- Wrapping child components in loops that have internal state
|
|
245
|
+
- Form inputs where you want to preserve user-entered values
|
|
246
|
+
- Elements with CSS animations you don't want to restart
|
|
247
|
+
- Any element where preserving DOM state is important
|
|
248
|
+
|
|
249
|
+
**Best practice:** Include all relevant state variables in the `fez:keep` value. This way the element is recreated exactly when it needs to be:
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<!-- Good: wrapper recreates when fill changes, so star is recreated too -->
|
|
253
|
+
<span fez:keep="star-{i}-{getStarFill(i)}">
|
|
254
|
+
<ui-star fill="{getStarFill(i)}" />
|
|
255
|
+
</span>
|
|
256
|
+
|
|
257
|
+
<!-- Bad: wrapper never recreates even when fill changes -->
|
|
258
|
+
<span fez:keep="star-{i}">
|
|
259
|
+
<ui-star fill="{getStarFill(i)}" />
|
|
260
|
+
</span>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Async/Await Blocks
|
|
264
|
+
|
|
265
|
+
Handle promises directly in templates with automatic loading/error states:
|
|
266
|
+
|
|
267
|
+
```html
|
|
268
|
+
<!-- Full syntax with all three states -->
|
|
269
|
+
{#await state.userData}
|
|
270
|
+
<p>Loading user...</p>
|
|
271
|
+
{:then user}
|
|
272
|
+
<div class="profile">
|
|
273
|
+
<h1>{user.name}</h1>
|
|
274
|
+
<p>{user.email}</p>
|
|
275
|
+
</div>
|
|
276
|
+
{:catch error}
|
|
277
|
+
<p class="error">Failed to load: {error.message}</p>
|
|
278
|
+
{/await}
|
|
279
|
+
|
|
280
|
+
<!-- Skip pending state (shows nothing while loading) -->
|
|
281
|
+
{#await state.data}{:then result}
|
|
282
|
+
<p>Result: {result}</p>
|
|
283
|
+
{/await}
|
|
284
|
+
|
|
285
|
+
<!-- With error handling but no pending state -->
|
|
286
|
+
{#await state.data}{:then result}
|
|
287
|
+
<p>{result}</p>
|
|
288
|
+
{:catch err}
|
|
289
|
+
<p>Error: {err.message}</p>
|
|
290
|
+
{/await}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
class {
|
|
51
295
|
init() {
|
|
52
|
-
|
|
53
|
-
this.state.
|
|
296
|
+
// CORRECT - assign promise directly, template handles loading/resolved/rejected states
|
|
297
|
+
this.state.userData = fetch('/api/user').then(r => r.json())
|
|
298
|
+
|
|
299
|
+
// WRONG - using await loses the loading state (value is already resolved)
|
|
300
|
+
// this.state.userData = await fetch('/api/user').then(r => r.json())
|
|
54
301
|
}
|
|
55
302
|
|
|
56
|
-
|
|
57
|
-
|
|
303
|
+
refresh() {
|
|
304
|
+
// Re-assigning a new promise triggers new loading state
|
|
305
|
+
this.state.userData = fetch('/api/user').then(r => r.json())
|
|
58
306
|
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Key points:**
|
|
311
|
+
|
|
312
|
+
- **Assign promises directly** - don't use `await` keyword when assigning to state
|
|
313
|
+
- Template automatically shows pending/resolved/rejected content
|
|
314
|
+
- Re-renders happen automatically when promise settles
|
|
315
|
+
- Non-promise values show `:then` content immediately (no loading state)
|
|
316
|
+
|
|
317
|
+
### Arrow Function Event Handlers
|
|
318
|
+
|
|
319
|
+
Use arrow functions for clean event handling with automatic loop variable interpolation:
|
|
320
|
+
|
|
321
|
+
```html
|
|
322
|
+
<!-- Simple handler -->
|
|
323
|
+
<button onclick="{()" ="">handleClick()}>Click me</button>
|
|
324
|
+
|
|
325
|
+
<!-- With event parameter -->
|
|
326
|
+
<button onclick="{(e)" ="">handleClick(e)}>Click me</button>
|
|
59
327
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
328
|
+
<!-- Inside loops - index is automatically interpolated -->
|
|
329
|
+
{#each state.tasks as task, index}
|
|
330
|
+
<button onclick="{()" ="">removeTask(index)}>Remove #{index}</button>
|
|
331
|
+
<button onclick="{(e)" ="">editTask(index, e)}>Edit</button>
|
|
332
|
+
{/each}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Arrow functions in event attributes are automatically transformed:
|
|
336
|
+
|
|
337
|
+
- `{() => foo()}` becomes `onclick="fez.foo()"`
|
|
338
|
+
- `{(e) => foo(e)}` becomes `onclick="fez.foo(event)"`
|
|
339
|
+
- Loop variables like `index` are evaluated at render time
|
|
340
|
+
|
|
341
|
+
### Self-Closing Custom Elements
|
|
342
|
+
|
|
343
|
+
Custom elements can use self-closing syntax:
|
|
344
|
+
|
|
345
|
+
```html
|
|
346
|
+
<ui-icon name="star" />
|
|
347
|
+
<!-- Automatically converted to: <ui-icon name="star"></ui-icon> -->
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Example: Counter Component
|
|
351
|
+
|
|
352
|
+
Here's a simple counter component that demonstrates Fez's core features:
|
|
353
|
+
|
|
354
|
+
```html
|
|
355
|
+
<!-- Define a counter component in ex-counter.fez -->
|
|
356
|
+
<script>
|
|
357
|
+
class {
|
|
358
|
+
// called when Fez node is connected to DOM
|
|
359
|
+
init() {
|
|
360
|
+
this.MAX = 6
|
|
361
|
+
this.state.count = 0
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
isMax() {
|
|
365
|
+
return this.state.count >= this.MAX
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// if state is changed, template is re-rendered
|
|
369
|
+
more() {
|
|
370
|
+
this.state.count += this.isMax() ? 0 : 1
|
|
371
|
+
}
|
|
63
372
|
}
|
|
64
373
|
</script>
|
|
65
374
|
|
|
66
375
|
<style>
|
|
67
|
-
/*
|
|
68
|
-
/*
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
button {
|
|
77
|
-
position: relative;
|
|
78
|
-
top: -3px;
|
|
79
|
-
}
|
|
376
|
+
/* All styles are locally scoped to the component */
|
|
377
|
+
/* Root-level styles apply to the component root node */
|
|
378
|
+
zoom: 2;
|
|
379
|
+
margin: 10px 0;
|
|
380
|
+
|
|
381
|
+
button {
|
|
382
|
+
position: relative;
|
|
383
|
+
top: -3px;
|
|
384
|
+
}
|
|
80
385
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
386
|
+
span {
|
|
387
|
+
padding: 0 5px;
|
|
84
388
|
}
|
|
85
389
|
</style>
|
|
86
390
|
|
|
87
|
-
<button onclick="
|
|
391
|
+
<button onclick="{()" ="">
|
|
392
|
+
state.count -= 1} disabled={state.count == 1}>-
|
|
393
|
+
</button>
|
|
88
394
|
|
|
89
|
-
<span>
|
|
90
|
-
{{ state.count }}
|
|
91
|
-
</span>
|
|
395
|
+
<span> {state.count} </span>
|
|
92
396
|
|
|
93
|
-
<button onclick="
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{{else}}
|
|
99
|
-
{{#if state.count % 2 }}
|
|
100
|
-
odd
|
|
101
|
-
{{else}}
|
|
102
|
-
even
|
|
103
|
-
{{/if}}
|
|
104
|
-
{{/if}}
|
|
105
|
-
{{/if}}
|
|
397
|
+
<button onclick="{()" ="">more()} disabled={isMax()}>+</button>
|
|
398
|
+
{#if state.count > 0}
|
|
399
|
+
<span>—</span>
|
|
400
|
+
{#if state.count == MAX} MAX {:else} {#if state.count % 2} odd {:else} even
|
|
401
|
+
{/if} {/if} {/if}
|
|
106
402
|
```
|
|
107
403
|
|
|
108
404
|
To use this component in your HTML:
|
|
@@ -112,69 +408,73 @@ To use this component in your HTML:
|
|
|
112
408
|
<script src="https://dux.github.io/fez/dist/fez.js"></script>
|
|
113
409
|
|
|
114
410
|
<!-- Load component via template tag -->
|
|
115
|
-
<template fez="/fez-libs/ex-counter.fez
|
|
411
|
+
<template fez="/fez-libs/ex-counter.fez"></template>
|
|
116
412
|
|
|
117
413
|
<!-- Use the component -->
|
|
118
414
|
<ex-counter></ex-counter>
|
|
119
415
|
```
|
|
120
416
|
|
|
121
417
|
This example showcases:
|
|
418
|
+
|
|
122
419
|
- **Reactive state**: Changes to `this.state` automatically update the DOM
|
|
123
|
-
- **Template syntax**: `{
|
|
124
|
-
- **
|
|
125
|
-
- **Conditional rendering**: `{
|
|
126
|
-
- **Scoped styling**:
|
|
420
|
+
- **Template syntax**: `{ }` for expressions, `{#if}`, `{#each}` for control flow
|
|
421
|
+
- **Arrow function handlers**: `onclick={() => method()}` for clean event binding
|
|
422
|
+
- **Conditional rendering**: `{#if}`, `{:else}` blocks for dynamic UI
|
|
423
|
+
- **Scoped styling**: All styles locally scoped to the component, root-level styles apply to root node
|
|
127
424
|
- **Component lifecycle**: `init()` method called when component mounts
|
|
128
425
|
|
|
129
426
|
## What can it do and why is it great?
|
|
130
427
|
|
|
131
428
|
### Core Features
|
|
132
429
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
430
|
+
- **Native Custom Elements** - Creates and defines Custom HTML tags using the native browser interface for maximum performance
|
|
431
|
+
- **Server-Side Friendly** - Works seamlessly with server-generated HTML, any routing library, and progressive enhancement strategies
|
|
432
|
+
- **Semantic HTML Output** - Transforms custom elements to standard HTML nodes (e.g., `<ui-button>` → `<button class="fez fez-button">`), making components fully stylable with CSS
|
|
433
|
+
- **Single-File Components** - Define CSS, HTML, and JavaScript in one file, no build step required
|
|
434
|
+
- **No Framework Magic** - Plain vanilla JS classes with clear, documented methods. No hooks, runes, or complex abstractions
|
|
435
|
+
- **Runtime SCSS** - Style components using SCSS syntax via [Goober](https://goober.js.org/), compiled at runtime
|
|
436
|
+
- **Smart Memory Management** - MutationObserver automatically cleans up disconnected components and their resources (intervals, event listeners, subscriptions)
|
|
140
437
|
|
|
141
438
|
### Advanced Templating & Styling
|
|
142
439
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
440
|
+
- **Svelte-like Template Engine** - Single brace syntax (`{ }`), control flow (`{#if}`, `{#unless}`, `{#for}`, `{#each}`, `{#await}`), and block templates
|
|
441
|
+
- **Arrow Function Handlers** - Clean event syntax with automatic loop variable interpolation
|
|
442
|
+
- **Reactive State Management** - Built-in reactive `state` object automatically triggers re-renders on property changes
|
|
443
|
+
- **Component-Aware DOM Diffing** - Custom differ that understands Fez component boundaries, preserves element state and CSS animations, with hash-based render skipping for zero-cost no-op renders
|
|
444
|
+
- **Smart Component Isolation** - Child components are preserved during parent re-renders; only re-render when their props actually change
|
|
445
|
+
- **Preserve DOM Elements** - Use `fez:keep="unique-key"` attribute to preserve DOM elements across re-renders (useful for child components, animations, form inputs, or stateful elements)
|
|
446
|
+
- **Auto-ID for Form Inputs** - Elements with `fez:this` automatically get stable IDs, helping the differ preserve input state across re-renders
|
|
447
|
+
- **Import Maps** - Use `Fez.head({importmap: {...}})` to map bare import specifiers to full URLs, avoiding duplicate library instances
|
|
448
|
+
- **Style Macros** - Define custom CSS shortcuts like `Fez.cssMixin('mobile', '@media (max-width: 768px)')` and use as `:mobile { ... }`
|
|
449
|
+
- **Locally Scoped Styles** - All `<style>` content is locally scoped to the component. Root-level styles apply to the component root node. For global styles wrap in `body { ... }`, use `:fez { ... }` inside body block to reference the component root
|
|
150
450
|
|
|
151
451
|
### Developer Experience
|
|
152
452
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
453
|
+
- **Built-in Utilities** - Helpful methods like `formData()`, `setInterval()` (auto-cleanup), `onWindowResize()`, and `fezNextTick()`
|
|
454
|
+
- **Two-Way Data Binding** - Use `fez:bind` directive for automatic form synchronization
|
|
455
|
+
- **Advanced Slot System** - Full `<slot />` support with event listener preservation
|
|
456
|
+
- **Publish/Subscribe** - Built-in pub/sub system for component communication
|
|
457
|
+
- **Global State Management** - Automatic subscription-based global state with `this.globalState` proxy
|
|
458
|
+
- **Dynamic Component Loading** - Load components from URLs with `<template fez="path/to/component.fez">`
|
|
459
|
+
- **Auto HTML Correction** - Fixes invalid self-closing tags (`<ui-icon name="gear" />` → `<ui-icon name="gear"></ui-icon>`)
|
|
160
460
|
|
|
161
461
|
### Performance & Integration
|
|
162
462
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
463
|
+
- **Optimized Rendering** - Batched microtask rendering for flicker-free component initialization
|
|
464
|
+
- **Smart DOM Updates** - Efficient DOM manipulation with minimal reflows
|
|
465
|
+
- **Built-in Fetch with Caching** - `Fez.fetch()` includes automatic response caching and JSON/FormData handling
|
|
466
|
+
- **Global Component Access** - Register components globally with `GLOBAL = 'ComponentName'` for easy access
|
|
467
|
+
- **Rich Lifecycle Hooks** - `init`, `onMount`, `beforeRender`, `afterRender`, `onDestroy`, `onPropsChange`, `onStateChange`, `onGlobalStateChange`
|
|
468
|
+
- **Development Mode** - Enable detailed logging with `Fez.DEV = true`
|
|
169
469
|
|
|
170
470
|
### Why It's Great
|
|
171
471
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
472
|
+
- **Zero Build Step** - Just include the script and start coding
|
|
473
|
+
- **49KB Minified (~18KB gzipped)** - Tiny footprint with powerful features
|
|
474
|
+
- **Framework Agnostic** - Use alongside React, Vue, or any other framework
|
|
475
|
+
- **Progressive Enhancement** - Perfect for modernizing legacy applications one component at a time
|
|
476
|
+
- **Native Performance** - Leverages browser's native Custom Elements API
|
|
477
|
+
- **Intuitive API** - If you know vanilla JavaScript, you already know Fez
|
|
178
478
|
|
|
179
479
|
## Full available interface
|
|
180
480
|
|
|
@@ -196,10 +496,17 @@ Fez('foo-bar', class {
|
|
|
196
496
|
// set element style, set as property or method
|
|
197
497
|
CSS = `scss string... `
|
|
198
498
|
|
|
199
|
-
// define static HTML. calling `this.
|
|
200
|
-
// if you pair it with `
|
|
499
|
+
// define static HTML. calling `this.fezRender()` (no arguments) will refresh current node.
|
|
500
|
+
// if you pair it with `fezReactiveStore()`, to auto update on props change, you will have Svelte or Vue style reactive behaviour.
|
|
201
501
|
HTML = `...`
|
|
202
502
|
|
|
503
|
+
// Control rendering timing to prevent flicker (property or method)
|
|
504
|
+
// If true: renders immediately in main loop (no flicker)
|
|
505
|
+
// If false/undefined: renders in next animation frame (may flicker with nested non-fast elements)
|
|
506
|
+
// Components that don't accept slots or work without slots should set FAST = true
|
|
507
|
+
FAST = true
|
|
508
|
+
FAST = (node) => node.hasAttribute('title') // Function: e.g., ui-btn renders fast if has title attribute
|
|
509
|
+
|
|
203
510
|
// Make it globally accessible as `window.Dialog`
|
|
204
511
|
// The component is automatically appended to the document body as a singleton. See `demo/fez/ui-dialog.fez` for a complete example.
|
|
205
512
|
GLOBAL = 'Dialog'
|
|
@@ -265,18 +572,33 @@ Fez('foo-bar', class {
|
|
|
265
572
|
// set value to a node, uses value or innerHTML
|
|
266
573
|
this.val(selector, value)
|
|
267
574
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
this.
|
|
575
|
+
// Publish/Subscribe system
|
|
576
|
+
// Component-level: publishes bubble up to parent components until a subscriber is found
|
|
577
|
+
this.publish('channel', data) // publish from component, bubbles up to parents
|
|
578
|
+
this.subscribe('channel', (data) => {}) // subscribe in component (auto-cleanup on destroy)
|
|
579
|
+
|
|
580
|
+
// Global-level: publish to all subscribers
|
|
581
|
+
Fez.publish('channel', data) // publish globally
|
|
582
|
+
|
|
583
|
+
// Global subscribe with different targeting options:
|
|
584
|
+
Fez.subscribe('channel', callback) // always fires
|
|
585
|
+
Fez.subscribe(node, 'channel', callback) // fires only if node.isConnected
|
|
586
|
+
Fez.subscribe('#selector', 'channel', callback) // fires only if selector found at publish time
|
|
587
|
+
|
|
588
|
+
// Unsubscribe manually (auto-cleanup for disconnected nodes)
|
|
589
|
+
const unsub = Fez.subscribe('channel', callback)
|
|
590
|
+
unsub() // manually remove subscription
|
|
271
591
|
|
|
272
592
|
// gets root childNodes
|
|
273
|
-
this.childNodes()
|
|
274
|
-
this.childNodes(func)
|
|
593
|
+
this.childNodes() // returns array of child elements
|
|
594
|
+
this.childNodes(func) // map children with function
|
|
595
|
+
this.childNodes(true) // convert to objects: { html, ROOT, ...attrs }
|
|
596
|
+
// html = innerHTML, ROOT = original node, attrs become keys
|
|
275
597
|
|
|
276
598
|
// check if the this.root node is attached to dom
|
|
277
599
|
this.isConnected
|
|
278
600
|
|
|
279
|
-
// this.state has
|
|
601
|
+
// this.state has fezReactiveStore() attached by default. any change will trigger this.fezRender()
|
|
280
602
|
this.state.foo = 123
|
|
281
603
|
|
|
282
604
|
// generic window event handler with automatic cleanup
|
|
@@ -293,7 +615,7 @@ Fez('foo-bar', class {
|
|
|
293
615
|
this.onWindowScroll(func, delay)
|
|
294
616
|
|
|
295
617
|
// requestAnimationFrame wrapper with deduplication
|
|
296
|
-
this.
|
|
618
|
+
this.fezNextTick(func, name)
|
|
297
619
|
|
|
298
620
|
// get unique ID for root node, set one if needed
|
|
299
621
|
this.rootId()
|
|
@@ -307,9 +629,9 @@ Fez('foo-bar', class {
|
|
|
307
629
|
// automatic form submission handling if there is FORM as parent or child node
|
|
308
630
|
this.onSubmit(formData) { ... }
|
|
309
631
|
|
|
310
|
-
// render template and attach result dom to root. uses
|
|
311
|
-
this.
|
|
312
|
-
this.
|
|
632
|
+
// render template and attach result dom to root. uses component-aware DOM differ
|
|
633
|
+
this.fezRender()
|
|
634
|
+
this.fezRender(this.find('.body'), someHtmlTemplate) // you can render to another root too
|
|
313
635
|
})
|
|
314
636
|
|
|
315
637
|
/* Utility methods */
|
|
@@ -329,6 +651,15 @@ Fez.globalCss(`
|
|
|
329
651
|
...
|
|
330
652
|
`)
|
|
331
653
|
|
|
654
|
+
// localStorage with automatic JSON serialization (preserves types)
|
|
655
|
+
Fez.localStorage.set('count', 42)
|
|
656
|
+
Fez.localStorage.get('count') // 42 (number, not string)
|
|
657
|
+
Fez.localStorage.set('user', { name: 'John' })
|
|
658
|
+
Fez.localStorage.get('user') // { name: 'John' }
|
|
659
|
+
Fez.localStorage.get('missing', 'default') // 'default' (fallback value)
|
|
660
|
+
Fez.localStorage.remove('key')
|
|
661
|
+
Fez.localStorage.clear()
|
|
662
|
+
|
|
332
663
|
// internal, get unique ID for a string, poor mans MD5 / SHA1
|
|
333
664
|
Fez.fnv1('some string')
|
|
334
665
|
|
|
@@ -347,6 +678,12 @@ Fez.cssClass(text)
|
|
|
347
678
|
// display information about registered components in console
|
|
348
679
|
Fez.info()
|
|
349
680
|
|
|
681
|
+
// inspect Fez or Svelte element, dumps props/state/template info to console
|
|
682
|
+
Fez.log(nodeOrSelector)
|
|
683
|
+
|
|
684
|
+
// Dev helper: press Cmd/Ctrl + E to toggle overlays highlighting each component on the page.
|
|
685
|
+
// Click a label to call Fez.log for that element automatically.
|
|
686
|
+
|
|
350
687
|
// low-level DOM morphing function
|
|
351
688
|
Fez.morphdom(target, newNode, opts)
|
|
352
689
|
|
|
@@ -359,6 +696,19 @@ Fez.tag(tag, opts, html)
|
|
|
359
696
|
// execute function until it returns true
|
|
360
697
|
Fez.untilTrue(func, pingRate)
|
|
361
698
|
|
|
699
|
+
// Component Index (unified registry for all component data)
|
|
700
|
+
Fez.index['ui-btn'].class // Component class
|
|
701
|
+
Fez.index['ui-btn'].meta // Metadata from META = {...}
|
|
702
|
+
Fez.index['ui-btn'].demo // Demo HTML string
|
|
703
|
+
Fez.index['ui-btn'].info // Info HTML string
|
|
704
|
+
Fez.index['ui-btn'].source // Raw .fez source code
|
|
705
|
+
Fez.index.get('name') // { class, meta, demo: DOMNode, info: DOMNode, source }
|
|
706
|
+
Fez.index.apply('name', el) // Render demo into element and execute scripts
|
|
707
|
+
Fez.index.names() // ['ui-btn', 'ui-card', ...] all component names
|
|
708
|
+
Fez.index.withDemo() // Component names that have demos
|
|
709
|
+
Fez.index.all() // All components as object
|
|
710
|
+
Fez.index.info() // Log all component names to console
|
|
711
|
+
|
|
362
712
|
// resolve and execute a function from string or function reference
|
|
363
713
|
// useful for event handlers that can be either functions or strings
|
|
364
714
|
// Fez.resolveFunction('alert("hi")', element) - creates function and calls with element as this
|
|
@@ -373,27 +723,92 @@ Fez.resolveFunction(pointer, context)
|
|
|
373
723
|
// Load CSS: Fez.head({ css: 'path/to/styles.css' })
|
|
374
724
|
// Load CSS with attributes: Fez.head({ css: 'path/to/styles.css', media: 'print' })
|
|
375
725
|
// Execute inline script: Fez.head({ script: 'console.log("Hello world")' })
|
|
726
|
+
// Load single Fez component: Fez.head({ fez: 'path/to/component.fez' })
|
|
727
|
+
// Load multiple components from txt list: Fez.head({ fez: 'path/to/components.txt' })
|
|
728
|
+
// Import map - rewrites bare specifiers to full URLs at compile time:
|
|
729
|
+
// Fez.head({ importmap: { "three": "https://esm.sh/three@0.160.0", "three/addons/": "https://esm.sh/three@0.160.0/examples/jsm/" } })
|
|
376
730
|
Fez.head(config, callback)
|
|
377
731
|
```
|
|
378
732
|
|
|
733
|
+
## Loading Multiple Components
|
|
734
|
+
|
|
735
|
+
For loading many components at once, use a `.txt` file listing component paths:
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
# components.txt - one component per line
|
|
739
|
+
# Lines starting with # are comments
|
|
740
|
+
ui-button
|
|
741
|
+
ui-dialog
|
|
742
|
+
forms/input-text
|
|
743
|
+
forms/input-select
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Load all components with a single call:
|
|
747
|
+
|
|
748
|
+
```js
|
|
749
|
+
// Load all components listed in components.txt
|
|
750
|
+
// Paths are relative to the txt file location
|
|
751
|
+
Fez.head({ fez: "./demo/components.txt" }, () => {
|
|
752
|
+
console.log("All components loaded!");
|
|
753
|
+
});
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**Path resolution:**
|
|
757
|
+
|
|
758
|
+
- Paths without `/` prefix are relative to the txt file location
|
|
759
|
+
- `.fez` extension is added automatically if not present
|
|
760
|
+
- Paths starting with `/` are absolute from root
|
|
761
|
+
|
|
762
|
+
Example with `./demo/fez.txt`:
|
|
763
|
+
|
|
764
|
+
```
|
|
765
|
+
ui-button # loads ./demo/ui-button.fez
|
|
766
|
+
forms/input # loads ./demo/forms/input.fez
|
|
767
|
+
/lib/shared-comp # loads /lib/shared-comp.fez (absolute)
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
## Import Maps
|
|
771
|
+
|
|
772
|
+
Use `Fez.head({importmap})` to map bare import specifiers to full URLs. This avoids duplicate library instances when multiple sub-modules import the same dependency:
|
|
773
|
+
|
|
774
|
+
```html
|
|
775
|
+
<script>
|
|
776
|
+
Fez.head({importmap: {
|
|
777
|
+
"three": "https://esm.sh/three@0.160.0",
|
|
778
|
+
"three/addons/": "https://esm.sh/three@0.160.0/examples/jsm/"
|
|
779
|
+
}})
|
|
780
|
+
|
|
781
|
+
import * as THREE from 'three'
|
|
782
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
|
783
|
+
|
|
784
|
+
class {
|
|
785
|
+
// ...
|
|
786
|
+
}
|
|
787
|
+
</script>
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
At compile time, Fez rewrites bare specifiers to full URLs (e.g. `from 'three'` becomes `from 'https://esm.sh/three@0.160.0'`). Prefix mappings like `"three/addons/"` expand paths that start with that prefix. The `Fez.head({importmap})` call is removed from the final output.
|
|
791
|
+
|
|
379
792
|
## Fez script loading and definition
|
|
380
793
|
|
|
381
794
|
```html
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
795
|
+
<!-- Remote loading for a component via URL in fez attribute -->
|
|
796
|
+
<!-- Component name is extracted from filename (ui-button) -->
|
|
797
|
+
<!-- If remote HTML contains template/xmp tags with fez attributes, they are compiled -->
|
|
798
|
+
<!-- Otherwise, the entire content is compiled as the component -->
|
|
799
|
+
<script fez="path/to/ui-button.fez"></script>
|
|
387
800
|
|
|
388
|
-
|
|
389
|
-
|
|
801
|
+
<!-- prefix with : to calc before node mount -->
|
|
802
|
+
<foo-bar :size="document.getElementById('icon-range').value"></foo-bar>
|
|
390
803
|
|
|
391
|
-
|
|
392
|
-
|
|
804
|
+
<!-- pass JSON props via data-props -->
|
|
805
|
+
<foo-bar data-props='{"name": "John", "age": 30}'></foo-bar>
|
|
393
806
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
807
|
+
<!-- pass JSON template via data-json-template -->
|
|
808
|
+
<script type="text/template">
|
|
809
|
+
{...}
|
|
810
|
+
</script>
|
|
811
|
+
<foo-bar data-json-template="true"></foo-bar>
|
|
397
812
|
```
|
|
398
813
|
|
|
399
814
|
## Component structure
|
|
@@ -403,6 +818,20 @@ All parts are optional
|
|
|
403
818
|
```html
|
|
404
819
|
<!-- Head elements support (inline only in XML tags) -->
|
|
405
820
|
<xmp tag="some-tag">
|
|
821
|
+
<info>
|
|
822
|
+
<!-- Documentation block - rendered in demo pages -->
|
|
823
|
+
<ul>
|
|
824
|
+
<li>Component description</li>
|
|
825
|
+
<li>Props: <code>name</code>, <code>value</code></li>
|
|
826
|
+
</ul>
|
|
827
|
+
</info>
|
|
828
|
+
|
|
829
|
+
<demo>
|
|
830
|
+
<!-- Example usage - rendered in demo pages -->
|
|
831
|
+
<my-component name="basic"></my-component>
|
|
832
|
+
<my-component name="advanced" value="123"></my-component>
|
|
833
|
+
</demo>
|
|
834
|
+
|
|
406
835
|
<head>
|
|
407
836
|
<!-- everything in head will be copied to document head-->
|
|
408
837
|
<script>console.log('Added to document head, first script to execute.')</script>
|
|
@@ -419,64 +848,74 @@ All parts are optional
|
|
|
419
848
|
</script>
|
|
420
849
|
|
|
421
850
|
<style>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
851
|
+
/* All styles are locally scoped to the component */
|
|
852
|
+
/* Root-level styles apply to the component root node */
|
|
853
|
+
color: red;
|
|
854
|
+
padding: 10px;
|
|
425
855
|
|
|
426
|
-
:
|
|
427
|
-
/* component styles */
|
|
428
|
-
}
|
|
856
|
+
.child { font-weight: bold; }
|
|
429
857
|
</style>
|
|
430
858
|
<style>
|
|
431
|
-
|
|
859
|
+
/* For global styles, wrap in body { ... } */
|
|
860
|
+
/* Use :fez { ... } inside body block to reference the component root */
|
|
861
|
+
body {
|
|
862
|
+
.some-global-class { color: blue; }
|
|
863
|
+
:fez { border: 1px solid red; }
|
|
864
|
+
}
|
|
432
865
|
</style>
|
|
433
866
|
|
|
434
867
|
<div> ... <!-- any other html after head, script or style is considered template-->
|
|
435
|
-
<!--
|
|
436
|
-
|
|
868
|
+
<!-- All fez: attributes use namespace syntax (fez:keep, fez:this, fez:bind, fez:use, fez:class) -->
|
|
869
|
+
<!-- fez-keep also works (fez: is converted to fez- at compile time) -->
|
|
437
870
|
|
|
438
|
-
<!--
|
|
439
|
-
{
|
|
871
|
+
<!-- Conditionals -->
|
|
872
|
+
{#if foo}...{/if}
|
|
873
|
+
{#if foo}...{:else}...{/if}
|
|
874
|
+
{#if foo}...{:else if bar}...{:else}...{/if}
|
|
875
|
+
|
|
876
|
+
<!-- Unless directive - opposite of if -->
|
|
877
|
+
{#unless state.list.length}
|
|
440
878
|
<p>No items to display</p>
|
|
441
|
-
{
|
|
879
|
+
{/unless}
|
|
442
880
|
|
|
443
|
-
<!--
|
|
444
|
-
{
|
|
445
|
-
{
|
|
881
|
+
<!-- Loops -->
|
|
882
|
+
{#each state.list as name, index}...{/each}
|
|
883
|
+
{#for name, index in state.list}...{/for}
|
|
446
884
|
|
|
447
885
|
<!-- Block definitions -->
|
|
448
|
-
{
|
|
449
|
-
<img src={
|
|
450
|
-
{
|
|
451
|
-
{
|
|
452
|
-
{{block:image}} <!-- Use the header block -->
|
|
886
|
+
{@block image}
|
|
887
|
+
<img src={props.src} />
|
|
888
|
+
{/block}
|
|
889
|
+
{@block:image} <!-- Use the block -->
|
|
453
890
|
|
|
454
|
-
{
|
|
455
|
-
{
|
|
891
|
+
{@html data} <!-- unescaped HTML -->
|
|
892
|
+
{@json data} <!-- JSON dump in PRE.json tag -->
|
|
456
893
|
|
|
457
|
-
<!-- fez
|
|
458
|
-
<!--
|
|
459
|
-
|
|
894
|
+
<!-- fez:this will link DOM node to object property (inspired by Svelte) -->
|
|
895
|
+
<!-- links to -> this.listRoot -->
|
|
896
|
+
<!-- also auto-generates stable id="fez-{UID}-listRoot" for stable DOM diffing -->
|
|
897
|
+
<ul fez:this="listRoot">
|
|
460
898
|
|
|
461
|
-
<!-- when node is added to dom fez
|
|
899
|
+
<!-- when node is added to dom fez:use will call object function by name, and pass current node -->
|
|
462
900
|
<!-- this.animate(node) -->
|
|
463
|
-
<li fez
|
|
901
|
+
<li fez:use="animate">
|
|
464
902
|
|
|
465
|
-
<!-- fez
|
|
466
|
-
<input type="text" fez
|
|
903
|
+
<!-- fez:bind for two-way data binding on form elements -->
|
|
904
|
+
<input type="text" fez:bind="state.username" />
|
|
467
905
|
|
|
468
906
|
<!--
|
|
469
|
-
fez
|
|
907
|
+
fez:class for adding classes with optional delay.
|
|
470
908
|
class will be added to SPAN element, 100ms after dom mount (to trigger animations)
|
|
471
909
|
-->
|
|
472
|
-
<span fez
|
|
910
|
+
<span fez:class="active:100">Delayed class</span>
|
|
473
911
|
|
|
474
|
-
<!-- preserve
|
|
475
|
-
<p fez
|
|
912
|
+
<!-- preserve element across re-renders (recreates only when key changes) -->
|
|
913
|
+
<p fez:keep="unique-key">...</p>
|
|
476
914
|
|
|
477
|
-
<!--
|
|
478
|
-
|
|
479
|
-
|
|
915
|
+
<!-- child components in loops - wrap in plain HTML element with fez:keep -->
|
|
916
|
+
<span fez:keep="star-{i}-{rating}">
|
|
917
|
+
<ui-star fill={fill} />
|
|
918
|
+
</span>
|
|
480
919
|
|
|
481
920
|
<!-- :attribute for evaluated attributes (converts to JSON) -->
|
|
482
921
|
<div :data-config="state.config"></div>
|
|
@@ -521,15 +960,17 @@ Fez includes a built-in fetch wrapper with automatic JSON parsing and session-ba
|
|
|
521
960
|
|
|
522
961
|
```js
|
|
523
962
|
// GET request with promise
|
|
524
|
-
const data = await Fez.fetch(
|
|
963
|
+
const data = await Fez.fetch("https://api.example.com/data");
|
|
525
964
|
|
|
526
965
|
// GET request with callback, does not create promise
|
|
527
|
-
Fez.fetch(
|
|
528
|
-
console.log(data)
|
|
529
|
-
})
|
|
966
|
+
Fez.fetch("https://api.example.com/data", (data) => {
|
|
967
|
+
console.log(data);
|
|
968
|
+
});
|
|
530
969
|
|
|
531
970
|
// POST request
|
|
532
|
-
const result = await Fez.fetch(
|
|
971
|
+
const result = await Fez.fetch("POST", "https://api.example.com/data", {
|
|
972
|
+
key: "value",
|
|
973
|
+
});
|
|
533
974
|
```
|
|
534
975
|
|
|
535
976
|
### Features
|
|
@@ -545,11 +986,11 @@ const result = await Fez.fetch('POST', 'https://api.example.com/data', { key: 'v
|
|
|
545
986
|
```js
|
|
546
987
|
// Override default error handler
|
|
547
988
|
Fez.onError = (kind, error) => {
|
|
548
|
-
if (kind ===
|
|
549
|
-
console.error(
|
|
989
|
+
if (kind === "fetch") {
|
|
990
|
+
console.error("Fetch failed:", error);
|
|
550
991
|
// Show user-friendly error message
|
|
551
992
|
}
|
|
552
|
-
}
|
|
993
|
+
};
|
|
553
994
|
```
|
|
554
995
|
|
|
555
996
|
## Default Components
|
|
@@ -557,40 +998,41 @@ Fez.onError = (kind, error) => {
|
|
|
557
998
|
Fez includes several built-in components available when you include `defaults.js`:
|
|
558
999
|
|
|
559
1000
|
### fez-component
|
|
1001
|
+
|
|
560
1002
|
Dynamically includes a Fez component by name:
|
|
1003
|
+
|
|
561
1004
|
```html
|
|
562
1005
|
<fez-component name="some-node" :props="fez.props"></fez-component>
|
|
563
1006
|
```
|
|
564
1007
|
|
|
565
1008
|
### fez-include
|
|
1009
|
+
|
|
566
1010
|
Loads remote HTML content via URL:
|
|
1011
|
+
|
|
567
1012
|
```html
|
|
568
1013
|
<fez-include src="./demo/fez/ui-slider.html"></fez-include>
|
|
569
1014
|
```
|
|
570
1015
|
|
|
571
|
-
### fez-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
<fez-inline :state="{count: 0}">
|
|
575
|
-
<button onclick="fez.state.count += 1">+</button>
|
|
576
|
-
{{ state.count }} * {{ state.count }} = {{ state.count * state.count }}
|
|
577
|
-
</fez-inline>
|
|
578
|
-
```
|
|
1016
|
+
### fez-demo
|
|
1017
|
+
|
|
1018
|
+
Renders all components with their demos. Perfect for component documentation pages:
|
|
579
1019
|
|
|
580
|
-
### fez-memoize
|
|
581
|
-
Memoizes DOM content by key (global scope):
|
|
582
1020
|
```html
|
|
583
|
-
<!--
|
|
584
|
-
<fez-
|
|
585
|
-
<expensive-component></expensive-component>
|
|
586
|
-
</fez-memoize>
|
|
1021
|
+
<!-- Default: loads from ./demo/fez.txt -->
|
|
1022
|
+
<fez-demo></fez-demo>
|
|
587
1023
|
|
|
588
|
-
<!--
|
|
589
|
-
<fez-
|
|
590
|
-
<!-- Content here is ignored, stored version is used -->
|
|
591
|
-
</fez-memoize>
|
|
1024
|
+
<!-- Custom component list -->
|
|
1025
|
+
<fez-demo src="./my-components.txt"></fez-demo>
|
|
592
1026
|
```
|
|
593
1027
|
|
|
1028
|
+
The component loads all components listed in the txt file and displays:
|
|
1029
|
+
|
|
1030
|
+
- Component name and live demo (left side)
|
|
1031
|
+
- Info/documentation block (right side)
|
|
1032
|
+
- Buttons to log demo HTML and component source to console
|
|
1033
|
+
|
|
1034
|
+
See `demo/raw.html` for a minimal example.
|
|
1035
|
+
|
|
594
1036
|
## Global State Management
|
|
595
1037
|
|
|
596
1038
|
Fez includes a built-in global state manager that automatically tracks component subscriptions. It automatically tracks which components use which state variables and only updates exactly what's needed.
|
|
@@ -608,14 +1050,14 @@ Fez includes a built-in global state manager that automatically tracks component
|
|
|
608
1050
|
class Counter extends FezBase {
|
|
609
1051
|
increment() {
|
|
610
1052
|
// Setting global state - all listeners will be notified
|
|
611
|
-
this.globalState.count = (this.globalState.count || 0) + 1
|
|
1053
|
+
this.globalState.count = (this.globalState.count || 0) + 1;
|
|
612
1054
|
}
|
|
613
1055
|
|
|
614
1056
|
render() {
|
|
615
1057
|
// Reading global state - automatically subscribes this component
|
|
616
1058
|
return `<button onclick="fez.increment()">
|
|
617
1059
|
Count: ${this.globalState.count || 0}
|
|
618
|
-
</button
|
|
1060
|
+
</button>`;
|
|
619
1061
|
}
|
|
620
1062
|
}
|
|
621
1063
|
```
|
|
@@ -624,15 +1066,26 @@ class Counter extends FezBase {
|
|
|
624
1066
|
|
|
625
1067
|
```js
|
|
626
1068
|
// Set global state from outside components
|
|
627
|
-
Fez.state.set(
|
|
1069
|
+
Fez.state.set("count", 10);
|
|
628
1070
|
|
|
629
1071
|
// Get global state value
|
|
630
|
-
const count = Fez.state.get(
|
|
1072
|
+
const count = Fez.state.get("count");
|
|
1073
|
+
|
|
1074
|
+
// Subscribe to specific key changes (returns unsubscribe function)
|
|
1075
|
+
const unsubscribe = Fez.state.subscribe("language", (value, oldValue, key) => {
|
|
1076
|
+
console.log(`Language changed from ${oldValue} to ${value}`);
|
|
1077
|
+
});
|
|
1078
|
+
unsubscribe(); // stop listening
|
|
1079
|
+
|
|
1080
|
+
// Subscribe to ALL state changes
|
|
1081
|
+
Fez.state.subscribe((key, value, oldValue) => {
|
|
1082
|
+
console.log(`${key} changed to ${value}`);
|
|
1083
|
+
});
|
|
631
1084
|
|
|
632
1085
|
// Iterate over all components listening to a key
|
|
633
|
-
Fez.state.forEach(
|
|
634
|
-
console.log(`${component.fezName} is listening to count`)
|
|
635
|
-
})
|
|
1086
|
+
Fez.state.forEach("count", (component) => {
|
|
1087
|
+
console.log(`${component.fezName} is listening to count`);
|
|
1088
|
+
});
|
|
636
1089
|
```
|
|
637
1090
|
|
|
638
1091
|
### Optional Change Handler
|
|
@@ -642,42 +1095,70 @@ Components can define an `onGlobalStateChange` method for custom handling:
|
|
|
642
1095
|
```js
|
|
643
1096
|
class MyComponent extends FezBase {
|
|
644
1097
|
onGlobalStateChange(key, value) {
|
|
645
|
-
console.log(`Global state "${key}" changed to:`, value)
|
|
1098
|
+
console.log(`Global state "${key}" changed to:`, value);
|
|
646
1099
|
// Custom logic instead of automatic render
|
|
647
|
-
if (key ===
|
|
648
|
-
this.updateTheme(value)
|
|
1100
|
+
if (key === "theme") {
|
|
1101
|
+
this.updateTheme(value);
|
|
649
1102
|
}
|
|
650
1103
|
}
|
|
651
1104
|
|
|
652
1105
|
render() {
|
|
653
1106
|
// Still subscribes by reading the value
|
|
654
|
-
return `<div class="${this.globalState.theme ||
|
|
1107
|
+
return `<div class="${this.globalState.theme || "light"}">...</div>`;
|
|
655
1108
|
}
|
|
656
1109
|
}
|
|
657
1110
|
```
|
|
658
1111
|
|
|
1112
|
+
### Real Example: Language Switching
|
|
1113
|
+
|
|
1114
|
+
Control global state from outside Fez components:
|
|
1115
|
+
|
|
1116
|
+
```js
|
|
1117
|
+
// From anywhere in your app (vanilla JS, other frameworks, etc.)
|
|
1118
|
+
Fez.state.set("language", "en");
|
|
1119
|
+
|
|
1120
|
+
// All components using this.globalState.language will automatically re-render
|
|
1121
|
+
document.getElementById("lang-select").addEventListener("change", (e) => {
|
|
1122
|
+
Fez.state.set("language", e.target.value);
|
|
1123
|
+
});
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
```html
|
|
1127
|
+
<!-- Component automatically reacts to language changes -->
|
|
1128
|
+
<script>
|
|
1129
|
+
class {
|
|
1130
|
+
get greeting() {
|
|
1131
|
+
const greetings = { en: 'Hello', de: 'Hallo', hr: 'Bok' }
|
|
1132
|
+
return greetings[this.globalState.language] || greetings.en
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
</script>
|
|
1136
|
+
|
|
1137
|
+
<div>{greeting}, {props.name}!</div>
|
|
1138
|
+
```
|
|
1139
|
+
|
|
659
1140
|
### Real Example: Shared Counter State
|
|
660
1141
|
|
|
661
1142
|
```js
|
|
662
1143
|
// Multiple counter components sharing max count
|
|
663
1144
|
class Counter extends FezBase {
|
|
664
1145
|
init(props) {
|
|
665
|
-
this.state.count = parseInt(props.start || 0)
|
|
1146
|
+
this.state.count = parseInt(props.start || 0);
|
|
666
1147
|
}
|
|
667
1148
|
|
|
668
1149
|
beforeRender() {
|
|
669
1150
|
// All counters share and update the global max
|
|
670
|
-
this.globalState.maxCount ||= 0
|
|
1151
|
+
this.globalState.maxCount ||= 0;
|
|
671
1152
|
|
|
672
1153
|
// Find max across all counter instances
|
|
673
|
-
let max = 0
|
|
674
|
-
Fez.state.forEach(
|
|
1154
|
+
let max = 0;
|
|
1155
|
+
Fez.state.forEach("maxCount", (fez) => {
|
|
675
1156
|
if (fez.state?.count > max) {
|
|
676
|
-
max = fez.state.count
|
|
1157
|
+
max = fez.state.count;
|
|
677
1158
|
}
|
|
678
|
-
})
|
|
1159
|
+
});
|
|
679
1160
|
|
|
680
|
-
this.globalState.maxCount = max
|
|
1161
|
+
this.globalState.maxCount = max;
|
|
681
1162
|
}
|
|
682
1163
|
|
|
683
1164
|
render() {
|
|
@@ -685,7 +1166,51 @@ class Counter extends FezBase {
|
|
|
685
1166
|
<button onclick="fez.state.count++">+</button>
|
|
686
1167
|
<span>Count: ${this.state.count}</span>
|
|
687
1168
|
<span>(Global max: ${this.globalState.maxCount})</span>
|
|
688
|
-
|
|
1169
|
+
`;
|
|
689
1170
|
}
|
|
690
1171
|
}
|
|
691
1172
|
```
|
|
1173
|
+
|
|
1174
|
+
---
|
|
1175
|
+
|
|
1176
|
+
## Legacy Template Syntax
|
|
1177
|
+
|
|
1178
|
+
The original double-brace syntax `{{ }}` is still fully supported for backward compatibility. New projects should use the Svelte-like single-brace syntax documented above.
|
|
1179
|
+
|
|
1180
|
+
### Legacy Syntax Reference
|
|
1181
|
+
|
|
1182
|
+
```html
|
|
1183
|
+
<!-- Expressions -->
|
|
1184
|
+
{{ state.name }} {{ state.active ? 'yes' : 'no' }}
|
|
1185
|
+
|
|
1186
|
+
<!-- Conditionals -->
|
|
1187
|
+
{{if state.show}}...{{/if}} {{if state.show}}...{{else}}...{{/if}} {{unless
|
|
1188
|
+
state.hidden}}...{{/unless}}
|
|
1189
|
+
|
|
1190
|
+
<!-- Loops -->
|
|
1191
|
+
{{for item in state.items}}...{{/for}} {{each state.items as item,
|
|
1192
|
+
index}}...{{/each}}
|
|
1193
|
+
|
|
1194
|
+
<!-- Raw HTML and JSON -->
|
|
1195
|
+
{{raw state.htmlContent}} {{json state.data}}
|
|
1196
|
+
|
|
1197
|
+
<!-- Event handlers (string interpolation) -->
|
|
1198
|
+
<button onclick="fez.remove({{index}})">Remove</button>
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
The legacy syntax uses `[[ ]]` as an alternative to `{{ }}` for compatibility with Go templates and other templating engines.
|
|
1202
|
+
|
|
1203
|
+
### Migration
|
|
1204
|
+
|
|
1205
|
+
To migrate from legacy to Svelte-like syntax:
|
|
1206
|
+
|
|
1207
|
+
| Legacy | Svelte-like |
|
|
1208
|
+
| -------------------------- | ------------------------ |
|
|
1209
|
+
| `{{ expr }}` | `{expr}` |
|
|
1210
|
+
| `{{if cond}}` | `{#if cond}` |
|
|
1211
|
+
| `{{else}}` | `{:else}` |
|
|
1212
|
+
| `{{/if}}` | `{/if}` |
|
|
1213
|
+
| `{{for x in list}}` | `{#for x in list}` |
|
|
1214
|
+
| `{{each list as x}}` | `{#each list as x}` |
|
|
1215
|
+
| `{{raw html}}` | `{@html html}` |
|
|
1216
|
+
| `onclick="fez.foo({{i}})"` | `onclick={() => foo(i)}` |
|