@dinoreic/fez 0.4.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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>
59
327
 
60
- // is state is changed, template is re-rendered
61
- more() {
62
- this.state.count += this.isMax() ? 0 : 1
328
+ <!-- Inside loops - index is automatically interpolated -->
329
+ {#each state.tasks as task, index}
330
+ <button onclick="{()" ="">removeTask(index)}>Remove #{index}</button>
331
+ <button onclick="{(e)" ="">editTask(index, e)}>Edit</button>
332
+ {/each}
333
+ ```
334
+
335
+ Arrow functions in event attributes are automatically transformed:
336
+
337
+ - `{() => foo()}` becomes `onclick="fez.foo()"`
338
+ - `{(e) => foo(e)}` becomes `onclick="fez.foo(event)"`
339
+ - Loop variables like `index` are evaluated at render time
340
+
341
+ ### Self-Closing Custom Elements
342
+
343
+ Custom elements can use self-closing syntax:
344
+
345
+ ```html
346
+ <ui-icon name="star" />
347
+ <!-- Automatically converted to: <ui-icon name="star"></ui-icon> -->
348
+ ```
349
+
350
+ ## Example: Counter Component
351
+
352
+ Here's a simple counter component that demonstrates Fez's core features:
353
+
354
+ ```html
355
+ <!-- Define a counter component in ex-counter.fez -->
356
+ <script>
357
+ class {
358
+ // called when Fez node is connected to DOM
359
+ init() {
360
+ this.MAX = 6
361
+ this.state.count = 0
362
+ }
363
+
364
+ isMax() {
365
+ return this.state.count >= this.MAX
366
+ }
367
+
368
+ // if state is changed, template is re-rendered
369
+ more() {
370
+ this.state.count += this.isMax() ? 0 : 1
371
+ }
63
372
  }
64
373
  </script>
65
374
 
66
375
  <style>
67
- /* 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,10 +496,17 @@ Fez('foo-bar', class {
196
496
  // set element style, set as property or method
197
497
  CSS = `scss string... `
198
498
 
199
- // define static HTML. calling `this.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
 
503
+ // Control rendering timing to prevent flicker (property or method)
504
+ // If true: renders immediately in main loop (no flicker)
505
+ // If false/undefined: renders in next animation frame (may flicker with nested non-fast elements)
506
+ // Components that don't accept slots or work without slots should set FAST = true
507
+ FAST = true
508
+ FAST = (node) => node.hasAttribute('title') // Function: e.g., ui-btn renders fast if has title attribute
509
+
203
510
  // Make it globally accessible as `window.Dialog`
204
511
  // The component is automatically appended to the document body as a singleton. See `demo/fez/ui-dialog.fez` for a complete example.
205
512
  GLOBAL = 'Dialog'
@@ -265,18 +572,33 @@ Fez('foo-bar', class {
265
572
  // set value to a node, uses value or innerHTML
266
573
  this.val(selector, value)
267
574
 
268
- // you can publish globally, and subscribe locally
269
- Fez.publish('channel', foo)
270
- this.subscribe('channel', (foo) => { ... })
575
+ // Publish/Subscribe system
576
+ // Component-level: publishes bubble up to parent components until a subscriber is found
577
+ this.publish('channel', data) // publish from component, bubbles up to parents
578
+ this.subscribe('channel', (data) => {}) // subscribe in component (auto-cleanup on destroy)
579
+
580
+ // Global-level: publish to all subscribers
581
+ Fez.publish('channel', data) // publish globally
582
+
583
+ // Global subscribe with different targeting options:
584
+ Fez.subscribe('channel', callback) // always fires
585
+ Fez.subscribe(node, 'channel', callback) // fires only if node.isConnected
586
+ Fez.subscribe('#selector', 'channel', callback) // fires only if selector found at publish time
587
+
588
+ // Unsubscribe manually (auto-cleanup for disconnected nodes)
589
+ const unsub = Fez.subscribe('channel', callback)
590
+ unsub() // manually remove subscription
271
591
 
272
592
  // gets root childNodes
273
- this.childNodes()
274
- this.childNodes(func) // 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
275
597
 
276
598
  // check if the this.root node is attached to dom
277
599
  this.isConnected
278
600
 
279
- // 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()
280
602
  this.state.foo = 123
281
603
 
282
604
  // generic window event handler with automatic cleanup
@@ -293,7 +615,7 @@ Fez('foo-bar', class {
293
615
  this.onWindowScroll(func, delay)
294
616
 
295
617
  // requestAnimationFrame wrapper with deduplication
296
- this.nextTick(func, name)
618
+ this.fezNextTick(func, name)
297
619
 
298
620
  // get unique ID for root node, set one if needed
299
621
  this.rootId()
@@ -307,9 +629,9 @@ Fez('foo-bar', class {
307
629
  // automatic form submission handling if there is FORM as parent or child node
308
630
  this.onSubmit(formData) { ... }
309
631
 
310
- // render template and attach result dom to root. uses Idiomorph for DOM morph
311
- this.render()
312
- 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
313
635
  })
314
636
 
315
637
  /* Utility methods */
@@ -329,6 +651,15 @@ Fez.globalCss(`
329
651
  ...
330
652
  `)
331
653
 
654
+ // localStorage with automatic JSON serialization (preserves types)
655
+ Fez.localStorage.set('count', 42)
656
+ Fez.localStorage.get('count') // 42 (number, not string)
657
+ Fez.localStorage.set('user', { name: 'John' })
658
+ Fez.localStorage.get('user') // { name: 'John' }
659
+ Fez.localStorage.get('missing', 'default') // 'default' (fallback value)
660
+ Fez.localStorage.remove('key')
661
+ Fez.localStorage.clear()
662
+
332
663
  // internal, get unique ID for a string, poor mans MD5 / SHA1
333
664
  Fez.fnv1('some string')
334
665
 
@@ -347,6 +678,12 @@ Fez.cssClass(text)
347
678
  // display information about registered components in console
348
679
  Fez.info()
349
680
 
681
+ // inspect Fez or Svelte element, dumps props/state/template info to console
682
+ Fez.log(nodeOrSelector)
683
+
684
+ // Dev helper: press Cmd/Ctrl + E to toggle overlays highlighting each component on the page.
685
+ // Click a label to call Fez.log for that element automatically.
686
+
350
687
  // low-level DOM morphing function
351
688
  Fez.morphdom(target, newNode, opts)
352
689
 
@@ -359,6 +696,19 @@ Fez.tag(tag, opts, html)
359
696
  // execute function until it returns true
360
697
  Fez.untilTrue(func, pingRate)
361
698
 
699
+ // Component Index (unified registry for all component data)
700
+ Fez.index['ui-btn'].class // Component class
701
+ Fez.index['ui-btn'].meta // Metadata from META = {...}
702
+ Fez.index['ui-btn'].demo // Demo HTML string
703
+ Fez.index['ui-btn'].info // Info HTML string
704
+ Fez.index['ui-btn'].source // Raw .fez source code
705
+ Fez.index.get('name') // { class, meta, demo: DOMNode, info: DOMNode, source }
706
+ Fez.index.apply('name', el) // Render demo into element and execute scripts
707
+ Fez.index.names() // ['ui-btn', 'ui-card', ...] all component names
708
+ Fez.index.withDemo() // Component names that have demos
709
+ Fez.index.all() // All components as object
710
+ Fez.index.info() // Log all component names to console
711
+
362
712
  // resolve and execute a function from string or function reference
363
713
  // useful for event handlers that can be either functions or strings
364
714
  // Fez.resolveFunction('alert("hi")', element) - creates function and calls with element as this
@@ -373,27 +723,92 @@ Fez.resolveFunction(pointer, context)
373
723
  // Load CSS: Fez.head({ css: 'path/to/styles.css' })
374
724
  // Load CSS with attributes: Fez.head({ css: 'path/to/styles.css', media: 'print' })
375
725
  // Execute inline script: Fez.head({ script: 'console.log("Hello world")' })
726
+ // Load single Fez component: Fez.head({ fez: 'path/to/component.fez' })
727
+ // Load multiple components from txt list: Fez.head({ fez: 'path/to/components.txt' })
728
+ // Import map - rewrites bare specifiers to full URLs at compile time:
729
+ // Fez.head({ importmap: { "three": "https://esm.sh/three@0.160.0", "three/addons/": "https://esm.sh/three@0.160.0/examples/jsm/" } })
376
730
  Fez.head(config, callback)
377
731
  ```
378
732
 
733
+ ## Loading Multiple Components
734
+
735
+ For loading many components at once, use a `.txt` file listing component paths:
736
+
737
+ ```bash
738
+ # components.txt - one component per line
739
+ # Lines starting with # are comments
740
+ ui-button
741
+ ui-dialog
742
+ forms/input-text
743
+ forms/input-select
744
+ ```
745
+
746
+ Load all components with a single call:
747
+
748
+ ```js
749
+ // Load all components listed in components.txt
750
+ // Paths are relative to the txt file location
751
+ Fez.head({ fez: "./demo/components.txt" }, () => {
752
+ console.log("All components loaded!");
753
+ });
754
+ ```
755
+
756
+ **Path resolution:**
757
+
758
+ - Paths without `/` prefix are relative to the txt file location
759
+ - `.fez` extension is added automatically if not present
760
+ - Paths starting with `/` are absolute from root
761
+
762
+ Example with `./demo/fez.txt`:
763
+
764
+ ```
765
+ ui-button # loads ./demo/ui-button.fez
766
+ forms/input # loads ./demo/forms/input.fez
767
+ /lib/shared-comp # loads /lib/shared-comp.fez (absolute)
768
+ ```
769
+
770
+ ## Import Maps
771
+
772
+ Use `Fez.head({importmap})` to map bare import specifiers to full URLs. This avoids duplicate library instances when multiple sub-modules import the same dependency:
773
+
774
+ ```html
775
+ <script>
776
+ Fez.head({importmap: {
777
+ "three": "https://esm.sh/three@0.160.0",
778
+ "three/addons/": "https://esm.sh/three@0.160.0/examples/jsm/"
779
+ }})
780
+
781
+ import * as THREE from 'three'
782
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
783
+
784
+ class {
785
+ // ...
786
+ }
787
+ </script>
788
+ ```
789
+
790
+ At compile time, Fez rewrites bare specifiers to full URLs (e.g. `from 'three'` becomes `from 'https://esm.sh/three@0.160.0'`). Prefix mappings like `"three/addons/"` expand paths that start with that prefix. The `Fez.head({importmap})` call is removed from the final output.
791
+
379
792
  ## Fez script loading and definition
380
793
 
381
794
  ```html
382
- <!-- Remote loading for a component via URL in fez attribute -->
383
- <!-- Component name is extracted from filename (ui-button) -->
384
- <!-- If remote HTML contains template/xmp tags with fez attributes, they are compiled -->
385
- <!-- Otherwise, the entire content is compiled as the component -->
386
- <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>
387
800
 
388
- <!-- prefix with : to calc before node mount -->
389
- <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>
390
803
 
391
- <!-- pass JSON props via data-props -->
392
- <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>
393
806
 
394
- <!-- pass JSON template via data-json-template -->
395
- <script type="text/template">{...}</script>
396
- <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>
397
812
  ```
398
813
 
399
814
  ## Component structure
@@ -403,6 +818,20 @@ All parts are optional
403
818
  ```html
404
819
  <!-- Head elements support (inline only in XML tags) -->
405
820
  <xmp tag="some-tag">
821
+ <info>
822
+ <!-- Documentation block - rendered in demo pages -->
823
+ <ul>
824
+ <li>Component description</li>
825
+ <li>Props: <code>name</code>, <code>value</code></li>
826
+ </ul>
827
+ </info>
828
+
829
+ <demo>
830
+ <!-- Example usage - rendered in demo pages -->
831
+ <my-component name="basic"></my-component>
832
+ <my-component name="advanced" value="123"></my-component>
833
+ </demo>
834
+
406
835
  <head>
407
836
  <!-- everything in head will be copied to document head-->
408
837
  <script>console.log('Added to document head, first script to execute.')</script>
@@ -419,64 +848,74 @@ All parts are optional
419
848
  </script>
420
849
 
421
850
  <style>
422
- b {
423
- color: red; /* will be global style*/
424
- }
851
+ /* All styles are locally scoped to the component */
852
+ /* Root-level styles apply to the component root node */
853
+ color: red;
854
+ padding: 10px;
425
855
 
426
- :fez {
427
- /* component styles */
428
- }
856
+ .child { font-weight: bold; }
429
857
  </style>
430
858
  <style>
431
- 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
+ }
432
865
  </style>
433
866
 
434
867
  <div> ... <!-- any other html after head, script or style is considered template-->
435
- <!-- resolve any condition -->
436
- {{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) -->
437
870
 
438
- <!-- unless directive - opposite of if -->
439
- {{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}
440
878
  <p>No items to display</p>
441
- {{/unless}}
879
+ {/unless}
442
880
 
443
- <!-- runs in node scope, you can use for loop -->
444
- {{each fez.list as name, index}} ... {{/each}}
445
- {{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}
446
884
 
447
885
  <!-- Block definitions -->
448
- {{block image}}
449
- <img src={{ props.src}} />
450
- {{/block}}
451
- {{block:image}} <!-- Use the header block -->
452
- {{block:image}} <!-- Use the header block -->
886
+ {@block image}
887
+ <img src={props.src} />
888
+ {/block}
889
+ {@block:image} <!-- Use the block -->
453
890
 
454
- {{raw data}} <!-- unescape HTML -->
455
- {{json data}} <!-- JSON dump in PRE.json tag -->
891
+ {@html data} <!-- unescaped HTML -->
892
+ {@json data} <!-- JSON dump in PRE.json tag -->
456
893
 
457
- <!-- fez-this will link DOM node to object property (inspired by Svelte) -->
458
- <!-- linkes to -> this.listRoot -->
459
- <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">
460
898
 
461
- <!-- 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 -->
462
900
  <!-- this.animate(node) -->
463
- <li fez-use="animate">
901
+ <li fez:use="animate">
464
902
 
465
- <!-- fez-bind for two-way data binding on form elements -->
466
- <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" />
467
905
 
468
906
  <!--
469
- fez-class for adding classes with optional delay.
907
+ fez:class for adding classes with optional delay.
470
908
  class will be added to SPAN element, 100ms after dom mount (to trigger animations)
471
909
  -->
472
- <span fez-class="active:100">Delayed class</span>
910
+ <span fez:class="active:100">Delayed class</span>
473
911
 
474
- <!-- preserve state by key, not affected by state changes-->>
475
- <p fez-keep="key">...</p>
912
+ <!-- preserve element across re-renders (recreates only when key changes) -->
913
+ <p fez:keep="unique-key">...</p>
476
914
 
477
- <!-- memoize DOM content by key (component-scoped) -->
478
- <!-- stores DOM on first render, restores on subsequent renders with same key -->
479
- <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>
480
919
 
481
920
  <!-- :attribute for evaluated attributes (converts to JSON) -->
482
921
  <div :data-config="state.config"></div>
@@ -521,15 +960,17 @@ Fez includes a built-in fetch wrapper with automatic JSON parsing and session-ba
521
960
 
522
961
  ```js
523
962
  // GET request with promise
524
- const data = await Fez.fetch('https://api.example.com/data')
963
+ const data = await Fez.fetch("https://api.example.com/data");
525
964
 
526
965
  // GET request with callback, does not create promise
527
- Fez.fetch('https://api.example.com/data', (data) => {
528
- console.log(data)
529
- })
966
+ Fez.fetch("https://api.example.com/data", (data) => {
967
+ console.log(data);
968
+ });
530
969
 
531
970
  // POST request
532
- const result = await Fez.fetch('POST', 'https://api.example.com/data', { key: 'value' })
971
+ const result = await Fez.fetch("POST", "https://api.example.com/data", {
972
+ key: "value",
973
+ });
533
974
  ```
534
975
 
535
976
  ### Features
@@ -545,11 +986,11 @@ const result = await Fez.fetch('POST', 'https://api.example.com/data', { key: 'v
545
986
  ```js
546
987
  // Override default error handler
547
988
  Fez.onError = (kind, error) => {
548
- if (kind === 'fetch') {
549
- console.error('Fetch failed:', error)
989
+ if (kind === "fetch") {
990
+ console.error("Fetch failed:", error);
550
991
  // Show user-friendly error message
551
992
  }
552
- }
993
+ };
553
994
  ```
554
995
 
555
996
  ## Default Components
@@ -557,40 +998,41 @@ Fez.onError = (kind, error) => {
557
998
  Fez includes several built-in components available when you include `defaults.js`:
558
999
 
559
1000
  ### fez-component
1001
+
560
1002
  Dynamically includes a Fez component by name:
1003
+
561
1004
  ```html
562
1005
  <fez-component name="some-node" :props="fez.props"></fez-component>
563
1006
  ```
564
1007
 
565
1008
  ### fez-include
1009
+
566
1010
  Loads remote HTML content via URL:
1011
+
567
1012
  ```html
568
1013
  <fez-include src="./demo/fez/ui-slider.html"></fez-include>
569
1014
  ```
570
1015
 
571
- ### fez-inline
572
- Creates inline components with reactive state:
573
- ```html
574
- <fez-inline :state="{count: 0}">
575
- <button onclick="fez.state.count += 1">+</button>
576
- {{ state.count }} * {{ state.count }} = {{ state.count * state.count }}
577
- </fez-inline>
578
- ```
1016
+ ### fez-demo
1017
+
1018
+ Renders all components with their demos. Perfect for component documentation pages:
579
1019
 
580
- ### fez-memoize
581
- Memoizes DOM content by key (global scope):
582
1020
  ```html
583
- <!-- First render: stores the content -->
584
- <fez-memoize key="unique-key">
585
- <expensive-component></expensive-component>
586
- </fez-memoize>
1021
+ <!-- Default: loads from ./demo/fez.txt -->
1022
+ <fez-demo></fez-demo>
587
1023
 
588
- <!-- Subsequent renders: restores stored content instantly -->
589
- <fez-memoize key="unique-key">
590
- <!-- Content here is ignored, stored version is used -->
591
- </fez-memoize>
1024
+ <!-- Custom component list -->
1025
+ <fez-demo src="./my-components.txt"></fez-demo>
592
1026
  ```
593
1027
 
1028
+ The component loads all components listed in the txt file and displays:
1029
+
1030
+ - Component name and live demo (left side)
1031
+ - Info/documentation block (right side)
1032
+ - Buttons to log demo HTML and component source to console
1033
+
1034
+ See `demo/raw.html` for a minimal example.
1035
+
594
1036
  ## Global State Management
595
1037
 
596
1038
  Fez includes a built-in global state manager that automatically tracks component subscriptions. It automatically tracks which components use which state variables and only updates exactly what's needed.
@@ -608,14 +1050,14 @@ Fez includes a built-in global state manager that automatically tracks component
608
1050
  class Counter extends FezBase {
609
1051
  increment() {
610
1052
  // Setting global state - all listeners will be notified
611
- this.globalState.count = (this.globalState.count || 0) + 1
1053
+ this.globalState.count = (this.globalState.count || 0) + 1;
612
1054
  }
613
1055
 
614
1056
  render() {
615
1057
  // Reading global state - automatically subscribes this component
616
1058
  return `<button onclick="fez.increment()">
617
1059
  Count: ${this.globalState.count || 0}
618
- </button>`
1060
+ </button>`;
619
1061
  }
620
1062
  }
621
1063
  ```
@@ -624,15 +1066,26 @@ class Counter extends FezBase {
624
1066
 
625
1067
  ```js
626
1068
  // Set global state from outside components
627
- Fez.state.set('count', 10)
1069
+ Fez.state.set("count", 10);
628
1070
 
629
1071
  // Get global state value
630
- 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
+ });
631
1084
 
632
1085
  // Iterate over all components listening to a key
633
- Fez.state.forEach('count', (component) => {
634
- console.log(`${component.fezName} is listening to count`)
635
- })
1086
+ Fez.state.forEach("count", (component) => {
1087
+ console.log(`${component.fezName} is listening to count`);
1088
+ });
636
1089
  ```
637
1090
 
638
1091
  ### Optional Change Handler
@@ -642,42 +1095,70 @@ Components can define an `onGlobalStateChange` method for custom handling:
642
1095
  ```js
643
1096
  class MyComponent extends FezBase {
644
1097
  onGlobalStateChange(key, value) {
645
- console.log(`Global state "${key}" changed to:`, value)
1098
+ console.log(`Global state "${key}" changed to:`, value);
646
1099
  // Custom logic instead of automatic render
647
- if (key === 'theme') {
648
- this.updateTheme(value)
1100
+ if (key === "theme") {
1101
+ this.updateTheme(value);
649
1102
  }
650
1103
  }
651
1104
 
652
1105
  render() {
653
1106
  // Still subscribes by reading the value
654
- return `<div class="${this.globalState.theme || 'light'}">...</div>`
1107
+ return `<div class="${this.globalState.theme || "light"}">...</div>`;
655
1108
  }
656
1109
  }
657
1110
  ```
658
1111
 
1112
+ ### Real Example: Language Switching
1113
+
1114
+ Control global state from outside Fez components:
1115
+
1116
+ ```js
1117
+ // From anywhere in your app (vanilla JS, other frameworks, etc.)
1118
+ Fez.state.set("language", "en");
1119
+
1120
+ // All components using this.globalState.language will automatically re-render
1121
+ document.getElementById("lang-select").addEventListener("change", (e) => {
1122
+ Fez.state.set("language", e.target.value);
1123
+ });
1124
+ ```
1125
+
1126
+ ```html
1127
+ <!-- Component automatically reacts to language changes -->
1128
+ <script>
1129
+ class {
1130
+ get greeting() {
1131
+ const greetings = { en: 'Hello', de: 'Hallo', hr: 'Bok' }
1132
+ return greetings[this.globalState.language] || greetings.en
1133
+ }
1134
+ }
1135
+ </script>
1136
+
1137
+ <div>{greeting}, {props.name}!</div>
1138
+ ```
1139
+
659
1140
  ### Real Example: Shared Counter State
660
1141
 
661
1142
  ```js
662
1143
  // Multiple counter components sharing max count
663
1144
  class Counter extends FezBase {
664
1145
  init(props) {
665
- this.state.count = parseInt(props.start || 0)
1146
+ this.state.count = parseInt(props.start || 0);
666
1147
  }
667
1148
 
668
1149
  beforeRender() {
669
1150
  // All counters share and update the global max
670
- this.globalState.maxCount ||= 0
1151
+ this.globalState.maxCount ||= 0;
671
1152
 
672
1153
  // Find max across all counter instances
673
- let max = 0
674
- Fez.state.forEach('maxCount', fez => {
1154
+ let max = 0;
1155
+ Fez.state.forEach("maxCount", (fez) => {
675
1156
  if (fez.state?.count > max) {
676
- max = fez.state.count
1157
+ max = fez.state.count;
677
1158
  }
678
- })
1159
+ });
679
1160
 
680
- this.globalState.maxCount = max
1161
+ this.globalState.maxCount = max;
681
1162
  }
682
1163
 
683
1164
  render() {
@@ -685,7 +1166,51 @@ class Counter extends FezBase {
685
1166
  <button onclick="fez.state.count++">+</button>
686
1167
  <span>Count: ${this.state.count}</span>
687
1168
  <span>(Global max: ${this.globalState.maxCount})</span>
688
- `
1169
+ `;
689
1170
  }
690
1171
  }
691
1172
  ```
1173
+
1174
+ ---
1175
+
1176
+ ## Legacy Template Syntax
1177
+
1178
+ The original double-brace syntax `{{ }}` is still fully supported for backward compatibility. New projects should use the Svelte-like single-brace syntax documented above.
1179
+
1180
+ ### Legacy Syntax Reference
1181
+
1182
+ ```html
1183
+ <!-- Expressions -->
1184
+ {{ state.name }} {{ state.active ? 'yes' : 'no' }}
1185
+
1186
+ <!-- Conditionals -->
1187
+ {{if state.show}}...{{/if}} {{if state.show}}...{{else}}...{{/if}} {{unless
1188
+ state.hidden}}...{{/unless}}
1189
+
1190
+ <!-- Loops -->
1191
+ {{for item in state.items}}...{{/for}} {{each state.items as item,
1192
+ index}}...{{/each}}
1193
+
1194
+ <!-- Raw HTML and JSON -->
1195
+ {{raw state.htmlContent}} {{json state.data}}
1196
+
1197
+ <!-- Event handlers (string interpolation) -->
1198
+ <button onclick="fez.remove({{index}})">Remove</button>
1199
+ ```
1200
+
1201
+ The legacy syntax uses `[[ ]]` as an alternative to `{{ }}` for compatibility with Go templates and other templating engines.
1202
+
1203
+ ### Migration
1204
+
1205
+ To migrate from legacy to Svelte-like syntax:
1206
+
1207
+ | Legacy | Svelte-like |
1208
+ | -------------------------- | ------------------------ |
1209
+ | `{{ expr }}` | `{expr}` |
1210
+ | `{{if cond}}` | `{#if cond}` |
1211
+ | `{{else}}` | `{:else}` |
1212
+ | `{{/if}}` | `{/if}` |
1213
+ | `{{for x in list}}` | `{#for x in list}` |
1214
+ | `{{each list as x}}` | `{#each list as x}` |
1215
+ | `{{raw html}}` | `{@html html}` |
1216
+ | `onclick="fez.foo({{i}})"` | `onclick={() => foo(i)}` |