@dinoreic/fez 0.4.1 → 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 +707 -209
- 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 +249 -146
- package/src/fez/defaults.js +272 -92
- 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 +279 -209
- 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/svelte-cde-adapter.coffee +10 -2
- 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>
|
|
327
|
+
|
|
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
|
+
}
|
|
59
367
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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,8 +496,8 @@ 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
|
|
|
203
503
|
// Control rendering timing to prevent flicker (property or method)
|
|
@@ -274,30 +574,31 @@ Fez('foo-bar', class {
|
|
|
274
574
|
|
|
275
575
|
// Publish/Subscribe system
|
|
276
576
|
// Component-level: publishes bubble up to parent components until a subscriber is found
|
|
277
|
-
this.publish('channel', data)
|
|
278
|
-
this.subscribe('channel', (data) => {}) // subscribe in component
|
|
279
|
-
|
|
280
|
-
// Global-level: publish to all subscribers
|
|
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
|
|
281
581
|
Fez.publish('channel', data) // publish globally
|
|
282
|
-
|
|
283
|
-
// Global subscribe
|
|
284
|
-
//
|
|
285
|
-
Fez.subscribe(node, 'channel', callback)
|
|
286
|
-
Fez.subscribe('#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// Manual unsubscribe (automatic cleanup happens when node disconnects)
|
|
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)
|
|
290
589
|
const unsub = Fez.subscribe('channel', callback)
|
|
291
590
|
unsub() // manually remove subscription
|
|
292
591
|
|
|
293
592
|
// gets root childNodes
|
|
294
|
-
this.childNodes()
|
|
295
|
-
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
|
|
296
597
|
|
|
297
598
|
// check if the this.root node is attached to dom
|
|
298
599
|
this.isConnected
|
|
299
600
|
|
|
300
|
-
// this.state has
|
|
601
|
+
// this.state has fezReactiveStore() attached by default. any change will trigger this.fezRender()
|
|
301
602
|
this.state.foo = 123
|
|
302
603
|
|
|
303
604
|
// generic window event handler with automatic cleanup
|
|
@@ -314,7 +615,7 @@ Fez('foo-bar', class {
|
|
|
314
615
|
this.onWindowScroll(func, delay)
|
|
315
616
|
|
|
316
617
|
// requestAnimationFrame wrapper with deduplication
|
|
317
|
-
this.
|
|
618
|
+
this.fezNextTick(func, name)
|
|
318
619
|
|
|
319
620
|
// get unique ID for root node, set one if needed
|
|
320
621
|
this.rootId()
|
|
@@ -328,9 +629,9 @@ Fez('foo-bar', class {
|
|
|
328
629
|
// automatic form submission handling if there is FORM as parent or child node
|
|
329
630
|
this.onSubmit(formData) { ... }
|
|
330
631
|
|
|
331
|
-
// render template and attach result dom to root. uses
|
|
332
|
-
this.
|
|
333
|
-
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
|
|
334
635
|
})
|
|
335
636
|
|
|
336
637
|
/* Utility methods */
|
|
@@ -350,6 +651,15 @@ Fez.globalCss(`
|
|
|
350
651
|
...
|
|
351
652
|
`)
|
|
352
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
|
+
|
|
353
663
|
// internal, get unique ID for a string, poor mans MD5 / SHA1
|
|
354
664
|
Fez.fnv1('some string')
|
|
355
665
|
|
|
@@ -369,10 +679,10 @@ Fez.cssClass(text)
|
|
|
369
679
|
Fez.info()
|
|
370
680
|
|
|
371
681
|
// inspect Fez or Svelte element, dumps props/state/template info to console
|
|
372
|
-
Fez.
|
|
682
|
+
Fez.log(nodeOrSelector)
|
|
373
683
|
|
|
374
684
|
// Dev helper: press Cmd/Ctrl + E to toggle overlays highlighting each component on the page.
|
|
375
|
-
// Click a label to call Fez.
|
|
685
|
+
// Click a label to call Fez.log for that element automatically.
|
|
376
686
|
|
|
377
687
|
// low-level DOM morphing function
|
|
378
688
|
Fez.morphdom(target, newNode, opts)
|
|
@@ -386,6 +696,19 @@ Fez.tag(tag, opts, html)
|
|
|
386
696
|
// execute function until it returns true
|
|
387
697
|
Fez.untilTrue(func, pingRate)
|
|
388
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
|
+
|
|
389
712
|
// resolve and execute a function from string or function reference
|
|
390
713
|
// useful for event handlers that can be either functions or strings
|
|
391
714
|
// Fez.resolveFunction('alert("hi")', element) - creates function and calls with element as this
|
|
@@ -400,27 +723,92 @@ Fez.resolveFunction(pointer, context)
|
|
|
400
723
|
// Load CSS: Fez.head({ css: 'path/to/styles.css' })
|
|
401
724
|
// Load CSS with attributes: Fez.head({ css: 'path/to/styles.css', media: 'print' })
|
|
402
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/" } })
|
|
403
730
|
Fez.head(config, callback)
|
|
404
731
|
```
|
|
405
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
|
+
|
|
406
792
|
## Fez script loading and definition
|
|
407
793
|
|
|
408
794
|
```html
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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>
|
|
414
800
|
|
|
415
|
-
|
|
416
|
-
|
|
801
|
+
<!-- prefix with : to calc before node mount -->
|
|
802
|
+
<foo-bar :size="document.getElementById('icon-range').value"></foo-bar>
|
|
417
803
|
|
|
418
|
-
|
|
419
|
-
|
|
804
|
+
<!-- pass JSON props via data-props -->
|
|
805
|
+
<foo-bar data-props='{"name": "John", "age": 30}'></foo-bar>
|
|
420
806
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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>
|
|
424
812
|
```
|
|
425
813
|
|
|
426
814
|
## Component structure
|
|
@@ -430,6 +818,20 @@ All parts are optional
|
|
|
430
818
|
```html
|
|
431
819
|
<!-- Head elements support (inline only in XML tags) -->
|
|
432
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
|
+
|
|
433
835
|
<head>
|
|
434
836
|
<!-- everything in head will be copied to document head-->
|
|
435
837
|
<script>console.log('Added to document head, first script to execute.')</script>
|
|
@@ -446,64 +848,74 @@ All parts are optional
|
|
|
446
848
|
</script>
|
|
447
849
|
|
|
448
850
|
<style>
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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;
|
|
452
855
|
|
|
453
|
-
:
|
|
454
|
-
/* component styles */
|
|
455
|
-
}
|
|
856
|
+
.child { font-weight: bold; }
|
|
456
857
|
</style>
|
|
457
858
|
<style>
|
|
458
|
-
|
|
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
|
+
}
|
|
459
865
|
</style>
|
|
460
866
|
|
|
461
867
|
<div> ... <!-- any other html after head, script or style is considered template-->
|
|
462
|
-
<!--
|
|
463
|
-
|
|
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) -->
|
|
464
870
|
|
|
465
|
-
<!--
|
|
466
|
-
{
|
|
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}
|
|
467
878
|
<p>No items to display</p>
|
|
468
|
-
{
|
|
879
|
+
{/unless}
|
|
469
880
|
|
|
470
|
-
<!--
|
|
471
|
-
{
|
|
472
|
-
{
|
|
881
|
+
<!-- Loops -->
|
|
882
|
+
{#each state.list as name, index}...{/each}
|
|
883
|
+
{#for name, index in state.list}...{/for}
|
|
473
884
|
|
|
474
885
|
<!-- Block definitions -->
|
|
475
|
-
{
|
|
476
|
-
<img src={
|
|
477
|
-
{
|
|
478
|
-
{
|
|
479
|
-
{{block:image}} <!-- Use the header block -->
|
|
886
|
+
{@block image}
|
|
887
|
+
<img src={props.src} />
|
|
888
|
+
{/block}
|
|
889
|
+
{@block:image} <!-- Use the block -->
|
|
480
890
|
|
|
481
|
-
{
|
|
482
|
-
{
|
|
891
|
+
{@html data} <!-- unescaped HTML -->
|
|
892
|
+
{@json data} <!-- JSON dump in PRE.json tag -->
|
|
483
893
|
|
|
484
|
-
<!-- fez
|
|
485
|
-
<!--
|
|
486
|
-
|
|
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">
|
|
487
898
|
|
|
488
|
-
<!-- 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 -->
|
|
489
900
|
<!-- this.animate(node) -->
|
|
490
|
-
<li fez
|
|
901
|
+
<li fez:use="animate">
|
|
491
902
|
|
|
492
|
-
<!-- fez
|
|
493
|
-
<input type="text" fez
|
|
903
|
+
<!-- fez:bind for two-way data binding on form elements -->
|
|
904
|
+
<input type="text" fez:bind="state.username" />
|
|
494
905
|
|
|
495
906
|
<!--
|
|
496
|
-
fez
|
|
907
|
+
fez:class for adding classes with optional delay.
|
|
497
908
|
class will be added to SPAN element, 100ms after dom mount (to trigger animations)
|
|
498
909
|
-->
|
|
499
|
-
<span fez
|
|
910
|
+
<span fez:class="active:100">Delayed class</span>
|
|
500
911
|
|
|
501
|
-
<!-- preserve
|
|
502
|
-
<p fez
|
|
912
|
+
<!-- preserve element across re-renders (recreates only when key changes) -->
|
|
913
|
+
<p fez:keep="unique-key">...</p>
|
|
503
914
|
|
|
504
|
-
<!--
|
|
505
|
-
|
|
506
|
-
|
|
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>
|
|
507
919
|
|
|
508
920
|
<!-- :attribute for evaluated attributes (converts to JSON) -->
|
|
509
921
|
<div :data-config="state.config"></div>
|
|
@@ -548,15 +960,17 @@ Fez includes a built-in fetch wrapper with automatic JSON parsing and session-ba
|
|
|
548
960
|
|
|
549
961
|
```js
|
|
550
962
|
// GET request with promise
|
|
551
|
-
const data = await Fez.fetch(
|
|
963
|
+
const data = await Fez.fetch("https://api.example.com/data");
|
|
552
964
|
|
|
553
965
|
// GET request with callback, does not create promise
|
|
554
|
-
Fez.fetch(
|
|
555
|
-
console.log(data)
|
|
556
|
-
})
|
|
966
|
+
Fez.fetch("https://api.example.com/data", (data) => {
|
|
967
|
+
console.log(data);
|
|
968
|
+
});
|
|
557
969
|
|
|
558
970
|
// POST request
|
|
559
|
-
const result = await Fez.fetch(
|
|
971
|
+
const result = await Fez.fetch("POST", "https://api.example.com/data", {
|
|
972
|
+
key: "value",
|
|
973
|
+
});
|
|
560
974
|
```
|
|
561
975
|
|
|
562
976
|
### Features
|
|
@@ -572,11 +986,11 @@ const result = await Fez.fetch('POST', 'https://api.example.com/data', { key: 'v
|
|
|
572
986
|
```js
|
|
573
987
|
// Override default error handler
|
|
574
988
|
Fez.onError = (kind, error) => {
|
|
575
|
-
if (kind ===
|
|
576
|
-
console.error(
|
|
989
|
+
if (kind === "fetch") {
|
|
990
|
+
console.error("Fetch failed:", error);
|
|
577
991
|
// Show user-friendly error message
|
|
578
992
|
}
|
|
579
|
-
}
|
|
993
|
+
};
|
|
580
994
|
```
|
|
581
995
|
|
|
582
996
|
## Default Components
|
|
@@ -584,40 +998,41 @@ Fez.onError = (kind, error) => {
|
|
|
584
998
|
Fez includes several built-in components available when you include `defaults.js`:
|
|
585
999
|
|
|
586
1000
|
### fez-component
|
|
1001
|
+
|
|
587
1002
|
Dynamically includes a Fez component by name:
|
|
1003
|
+
|
|
588
1004
|
```html
|
|
589
1005
|
<fez-component name="some-node" :props="fez.props"></fez-component>
|
|
590
1006
|
```
|
|
591
1007
|
|
|
592
1008
|
### fez-include
|
|
1009
|
+
|
|
593
1010
|
Loads remote HTML content via URL:
|
|
1011
|
+
|
|
594
1012
|
```html
|
|
595
1013
|
<fez-include src="./demo/fez/ui-slider.html"></fez-include>
|
|
596
1014
|
```
|
|
597
1015
|
|
|
598
|
-
### fez-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
<fez-inline :state="{count: 0}">
|
|
602
|
-
<button onclick="fez.state.count += 1">+</button>
|
|
603
|
-
{{ state.count }} * {{ state.count }} = {{ state.count * state.count }}
|
|
604
|
-
</fez-inline>
|
|
605
|
-
```
|
|
1016
|
+
### fez-demo
|
|
1017
|
+
|
|
1018
|
+
Renders all components with their demos. Perfect for component documentation pages:
|
|
606
1019
|
|
|
607
|
-
### fez-memoize
|
|
608
|
-
Memoizes DOM content by key (global scope):
|
|
609
1020
|
```html
|
|
610
|
-
<!--
|
|
611
|
-
<fez-
|
|
612
|
-
<expensive-component></expensive-component>
|
|
613
|
-
</fez-memoize>
|
|
1021
|
+
<!-- Default: loads from ./demo/fez.txt -->
|
|
1022
|
+
<fez-demo></fez-demo>
|
|
614
1023
|
|
|
615
|
-
<!--
|
|
616
|
-
<fez-
|
|
617
|
-
<!-- Content here is ignored, stored version is used -->
|
|
618
|
-
</fez-memoize>
|
|
1024
|
+
<!-- Custom component list -->
|
|
1025
|
+
<fez-demo src="./my-components.txt"></fez-demo>
|
|
619
1026
|
```
|
|
620
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
|
+
|
|
621
1036
|
## Global State Management
|
|
622
1037
|
|
|
623
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.
|
|
@@ -635,14 +1050,14 @@ Fez includes a built-in global state manager that automatically tracks component
|
|
|
635
1050
|
class Counter extends FezBase {
|
|
636
1051
|
increment() {
|
|
637
1052
|
// Setting global state - all listeners will be notified
|
|
638
|
-
this.globalState.count = (this.globalState.count || 0) + 1
|
|
1053
|
+
this.globalState.count = (this.globalState.count || 0) + 1;
|
|
639
1054
|
}
|
|
640
1055
|
|
|
641
1056
|
render() {
|
|
642
1057
|
// Reading global state - automatically subscribes this component
|
|
643
1058
|
return `<button onclick="fez.increment()">
|
|
644
1059
|
Count: ${this.globalState.count || 0}
|
|
645
|
-
</button
|
|
1060
|
+
</button>`;
|
|
646
1061
|
}
|
|
647
1062
|
}
|
|
648
1063
|
```
|
|
@@ -651,15 +1066,26 @@ class Counter extends FezBase {
|
|
|
651
1066
|
|
|
652
1067
|
```js
|
|
653
1068
|
// Set global state from outside components
|
|
654
|
-
Fez.state.set(
|
|
1069
|
+
Fez.state.set("count", 10);
|
|
655
1070
|
|
|
656
1071
|
// Get global state value
|
|
657
|
-
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
|
+
});
|
|
658
1084
|
|
|
659
1085
|
// Iterate over all components listening to a key
|
|
660
|
-
Fez.state.forEach(
|
|
661
|
-
console.log(`${component.fezName} is listening to count`)
|
|
662
|
-
})
|
|
1086
|
+
Fez.state.forEach("count", (component) => {
|
|
1087
|
+
console.log(`${component.fezName} is listening to count`);
|
|
1088
|
+
});
|
|
663
1089
|
```
|
|
664
1090
|
|
|
665
1091
|
### Optional Change Handler
|
|
@@ -669,42 +1095,70 @@ Components can define an `onGlobalStateChange` method for custom handling:
|
|
|
669
1095
|
```js
|
|
670
1096
|
class MyComponent extends FezBase {
|
|
671
1097
|
onGlobalStateChange(key, value) {
|
|
672
|
-
console.log(`Global state "${key}" changed to:`, value)
|
|
1098
|
+
console.log(`Global state "${key}" changed to:`, value);
|
|
673
1099
|
// Custom logic instead of automatic render
|
|
674
|
-
if (key ===
|
|
675
|
-
this.updateTheme(value)
|
|
1100
|
+
if (key === "theme") {
|
|
1101
|
+
this.updateTheme(value);
|
|
676
1102
|
}
|
|
677
1103
|
}
|
|
678
1104
|
|
|
679
1105
|
render() {
|
|
680
1106
|
// Still subscribes by reading the value
|
|
681
|
-
return `<div class="${this.globalState.theme ||
|
|
1107
|
+
return `<div class="${this.globalState.theme || "light"}">...</div>`;
|
|
682
1108
|
}
|
|
683
1109
|
}
|
|
684
1110
|
```
|
|
685
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
|
+
|
|
686
1140
|
### Real Example: Shared Counter State
|
|
687
1141
|
|
|
688
1142
|
```js
|
|
689
1143
|
// Multiple counter components sharing max count
|
|
690
1144
|
class Counter extends FezBase {
|
|
691
1145
|
init(props) {
|
|
692
|
-
this.state.count = parseInt(props.start || 0)
|
|
1146
|
+
this.state.count = parseInt(props.start || 0);
|
|
693
1147
|
}
|
|
694
1148
|
|
|
695
1149
|
beforeRender() {
|
|
696
1150
|
// All counters share and update the global max
|
|
697
|
-
this.globalState.maxCount ||= 0
|
|
1151
|
+
this.globalState.maxCount ||= 0;
|
|
698
1152
|
|
|
699
1153
|
// Find max across all counter instances
|
|
700
|
-
let max = 0
|
|
701
|
-
Fez.state.forEach(
|
|
1154
|
+
let max = 0;
|
|
1155
|
+
Fez.state.forEach("maxCount", (fez) => {
|
|
702
1156
|
if (fez.state?.count > max) {
|
|
703
|
-
max = fez.state.count
|
|
1157
|
+
max = fez.state.count;
|
|
704
1158
|
}
|
|
705
|
-
})
|
|
1159
|
+
});
|
|
706
1160
|
|
|
707
|
-
this.globalState.maxCount = max
|
|
1161
|
+
this.globalState.maxCount = max;
|
|
708
1162
|
}
|
|
709
1163
|
|
|
710
1164
|
render() {
|
|
@@ -712,7 +1166,51 @@ class Counter extends FezBase {
|
|
|
712
1166
|
<button onclick="fez.state.count++">+</button>
|
|
713
1167
|
<span>Count: ${this.state.count}</span>
|
|
714
1168
|
<span>(Global max: ${this.globalState.maxCount})</span>
|
|
715
|
-
|
|
1169
|
+
`;
|
|
716
1170
|
}
|
|
717
1171
|
}
|
|
718
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)}` |
|