@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 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 (20kb minified) 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.
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
- * [Goober](https://goober.js.org/) to enable runtime SCSS (similar to styled components)
12
- * [Idiomorph](https://github.com/bigskysoftware/idiomorph) to morph DOM from one state to another (as React or Stimulus/Turbo does it)
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 [morphing](https://github.com/bigskysoftware/idiomorph).
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
- * define your custom component - `Fez('ui-foo', class UiFoo extends FezBase)`
37
- * add HTML - `<ui-foo bar="baz" id="node1"></ui-foo>`
38
- * lib will call `node1.fez.init()` when node is added to DOM and connect your component to dom.
39
- * use `Fez` helper methods, or do all by yourself, all good.
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
- ## Example: Counter Component
92
+ ## Template Syntax (Svelte-like)
44
93
 
45
- Here's a simple counter component that demonstrates Fez's core features:
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
- <!-- Define a counter component in ex-counter.fez.html -->
49
- <script>
50
- // called when Fez node is connected to DOM
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
- this.MAX = 6
53
- this.state.count = 0
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
- isMax() {
57
- return this.state.count >= this.MAX
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
- // is state is changed, template is re-rendered
61
- more() {
62
- this.state.count += this.isMax() ? 0 : 1
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
- /* compiles from scss to css and injects class in head */
68
- /* body style */
69
- background-color: #f7f7f7;
70
-
71
- /* scoped to this component */
72
- :fez {
73
- zoom: 2;
74
- margin: 10px 0;
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
- span {
82
- padding: 0 5px;
83
- }
386
+ span {
387
+ padding: 0 5px;
84
388
  }
85
389
  </style>
86
390
 
87
- <button onclick="fez.state.count -= 1" disabled={{ state.count == 1 }}>-</button>
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="fez.more()" disabled={{ isMax() }}>+</button>
94
- {{if state.count > 0}}
95
- <span>&mdash;</span>
96
- {{if state.count == MAX }}
97
- MAX
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>&mdash;</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.html"></template>
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**: `{{ }}` for expressions, `@` as shorthand for `this.`
124
- - **Event handling**: Direct DOM event handlers with access to component methods
125
- - **Conditional rendering**: `{{#if}}`, `{{:else}}` blocks for dynamic UI
126
- - **Scoped styling**: SCSS support with styles automatically scoped to component
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
- * **Native Custom Elements** - Creates and defines Custom HTML tags using the native browser interface for maximum performance
134
- * **Server-Side Friendly** - Works seamlessly with server-generated HTML, any routing library, and progressive enhancement strategies
135
- * **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
136
- * **Single-File Components** - Define CSS, HTML, and JavaScript in one file, no build step required
137
- * **No Framework Magic** - Plain vanilla JS classes with clear, documented methods. No hooks, runes, or complex abstractions
138
- * **Runtime SCSS** - Style components using SCSS syntax via [Goober](https://goober.js.org/), compiled at runtime
139
- * **Smart Memory Management** - Automatic garbage collection cleans up disconnected nodes every 5 seconds
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
- * **Powerful Template Engine** - Multiple syntaxes (`{{ }}` and `[[ ]]`), control flow (`#if`, `#unless`, `#for`, `#each`), and block templates
144
- * **Reactive State Management** - Built-in reactive `state` object automatically triggers re-renders on property changes
145
- * **DOM Morphing** - Uses [Idiomorph](https://github.com/bigskysoftware/idiomorph) for intelligent DOM updates that preserve element state and animations
146
- * **Preserve DOM Elements** - Use `fez-keep="unique-key"` attribute to preserve DOM elements across re-renders (useful for animations, form inputs, or stateful elements)
147
- * **DOM Memoization** - Use `fez-memoize="key"` attribute to memoize and restore DOM content by key (component-scoped) or `<fez-memoize key="unique-key">` component for global memoization
148
- * **Style Macros** - Define custom CSS shortcuts like `Fez.cssMixin('mobile', '@media (max-width: 768px)')` and use as `:mobile { ... }`
149
- * **Scoped & Global Styles** - Components can define both scoped CSS (`:fez { ... }`) and global styles in the same component
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
- * **Built-in Utilities** - Helpful methods like `formData()`, `setInterval()` (auto-cleanup), `onWindowResize()`, and `nextTick()`
154
- * **Two-Way Data Binding** - Use `fez-bind` directive for automatic form synchronization
155
- * **Advanced Slot System** - Full `<slot />` support with event listener preservation
156
- * **Publish/Subscribe** - Built-in pub/sub system for component communication
157
- * **Global State Management** - Automatic subscription-based global state with `this.globalState` proxy
158
- * **Dynamic Component Loading** - Load components from URLs with `<template fez="path/to/component.html">`
159
- * **Auto HTML Correction** - Fixes invalid self-closing tags (`<fez-icon name="gear" />` → `<fez-icon name="gear"></fez-icon>`)
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
- * **Optimized Rendering** - Batched microtask rendering for flicker-free component initialization
164
- * **Smart DOM Updates** - Efficient DOM manipulation with minimal reflows
165
- * **Built-in Fetch with Caching** - `Fez.fetch()` includes automatic response caching and JSON/FormData handling
166
- * **Global Component Access** - Register components globally with `GLOBAL = 'ComponentName'` for easy access
167
- * **Rich Lifecycle Hooks** - `init`, `onMount`, `beforeRender`, `afterRender`, `onDestroy`, `onPropsChange`, `onStateChange`, `onGlobalStateChange`
168
- * **Development Mode** - Enable detailed logging with `Fez.DEV = true`
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
- * **Zero Build Step** - Just include the script and start coding
173
- * **20KB Minified** - Tiny footprint with powerful features
174
- * **Framework Agnostic** - Use alongside React, Vue, or any other framework
175
- * **Progressive Enhancement** - Perfect for modernizing legacy applications one component at a time
176
- * **Native Performance** - Leverages browser's native Custom Elements API
177
- * **Intuitive API** - If you know vanilla JavaScript, you already know Fez
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.render()` (no arguments) will refresh current node.
200
- // if you pair it with `reactiveStore()`, to auto update on props change, you will have Svelte or Vue style reactive behaviour.
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) // publish from component, bubbles up to parents
278
- this.subscribe('channel', (data) => {}) // subscribe in component
279
-
280
- // Global-level: publish to all subscribers (components and global listeners)
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: runs only if node is connected to DOM
284
- // Automatically removes subscription when node is disconnected
285
- Fez.subscribe(node, 'channel', callback) // subscribe specific node
286
- Fez.subscribe('#myId', 'channel', callback) // subscribe by selector
287
- Fez.subscribe('channel', callback) // subscribe to document.body
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) // pass function to loop forEach on selection, mask nodes out of position
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 reactiveStore() attached by default. any change will trigger this.render()
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.nextTick(func, name)
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 Idiomorph for DOM morph
332
- this.render()
333
- this.render(this.find('.body'), someHtmlTemplate) // you can render to another root too
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.dump(nodeOrSelector)
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.dump for that element automatically.
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
- <!-- Remote loading for a component via URL in fez attribute -->
410
- <!-- Component name is extracted from filename (ui-button) -->
411
- <!-- If remote HTML contains template/xmp tags with fez attributes, they are compiled -->
412
- <!-- Otherwise, the entire content is compiled as the component -->
413
- <script fez="path/to/ui-button.fez.html"></script>
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
- <!-- prefix with : to calc before node mount -->
416
- <foo-bar :size="document.getElementById('icon-range').value"></foo-bar>
801
+ <!-- prefix with : to calc before node mount -->
802
+ <foo-bar :size="document.getElementById('icon-range').value"></foo-bar>
417
803
 
418
- <!-- pass JSON props via data-props -->
419
- <foo-bar data-props='{"name": "John", "age": 30}'></foo-bar>
804
+ <!-- pass JSON props via data-props -->
805
+ <foo-bar data-props='{"name": "John", "age": 30}'></foo-bar>
420
806
 
421
- <!-- pass JSON template via data-json-template -->
422
- <script type="text/template">{...}</script>
423
- <foo-bar data-json-template="true"></foo-bar>
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
- b {
450
- color: red; /* will be global style*/
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
- :fez {
454
- /* component styles */
455
- }
856
+ .child { font-weight: bold; }
456
857
  </style>
457
858
  <style>
458
- color: red; /* if "body {" or ":fez {" is not found, style is considered local component style */
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
- <!-- resolve any condition -->
463
- {{if foo}} ... {{/if}}
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
- <!-- unless directive - opposite of if -->
466
- {{unless fez.list.length}}
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
- {{/unless}}
879
+ {/unless}
469
880
 
470
- <!-- runs in node scope, you can use for loop -->
471
- {{each fez.list as name, index}} ... {{/each}}
472
- {{for name, index in fez.list}} ... {{/for}}
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
- {{block image}}
476
- <img src={{ props.src}} />
477
- {{/block}}
478
- {{block:image}} <!-- Use the header block -->
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
- {{raw data}} <!-- unescape HTML -->
482
- {{json data}} <!-- JSON dump in PRE.json tag -->
891
+ {@html data} <!-- unescaped HTML -->
892
+ {@json data} <!-- JSON dump in PRE.json tag -->
483
893
 
484
- <!-- fez-this will link DOM node to object property (inspired by Svelte) -->
485
- <!-- linkes to -> this.listRoot -->
486
- <ul fez-this="listRoot">
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-use will call object function by name, and pass current node -->
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-use="animate">
901
+ <li fez:use="animate">
491
902
 
492
- <!-- fez-bind for two-way data binding on form elements -->
493
- <input type="text" fez-bind="state.username" />
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-class for adding classes with optional delay.
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-class="active:100">Delayed class</span>
910
+ <span fez:class="active:100">Delayed class</span>
500
911
 
501
- <!-- preserve state by key, not affected by state changes-->>
502
- <p fez-keep="key">...</p>
912
+ <!-- preserve element across re-renders (recreates only when key changes) -->
913
+ <p fez:keep="unique-key">...</p>
503
914
 
504
- <!-- memoize DOM content by key (component-scoped) -->
505
- <!-- stores DOM on first render, restores on subsequent renders with same key -->
506
- <div fez-memoize="unique-key">expensive content</div>
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('https://api.example.com/data')
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('https://api.example.com/data', (data) => {
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('POST', 'https://api.example.com/data', { key: 'value' })
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 === 'fetch') {
576
- console.error('Fetch failed:', 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-inline
599
- Creates inline components with reactive state:
600
- ```html
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
- <!-- First render: stores the content -->
611
- <fez-memoize key="unique-key">
612
- <expensive-component></expensive-component>
613
- </fez-memoize>
1021
+ <!-- Default: loads from ./demo/fez.txt -->
1022
+ <fez-demo></fez-demo>
614
1023
 
615
- <!-- Subsequent renders: restores stored content instantly -->
616
- <fez-memoize key="unique-key">
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('count', 10)
1069
+ Fez.state.set("count", 10);
655
1070
 
656
1071
  // Get global state value
657
- const count = Fez.state.get('count')
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('count', (component) => {
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 === 'theme') {
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 || 'light'}">...</div>`
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('maxCount', fez => {
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)}` |