@dinoreic/fez 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Attribution License
2
+
3
+ Copyright (c) 2025 Dino Reic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ 1. The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ 2. Attribution Requirement: Any product, application, or service that uses this
16
+ Software must include the following attribution in a visible location
17
+ (such as documentation, about page, or README):
18
+
19
+ "Built with Fez by Dino Reic (https://github.com/dux/fez)"
20
+
21
+ 3. The attribution must be retained in all derivative works and may not be
22
+ removed or obscured.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,638 @@
1
+ <img src="demo/fez.png" align="right" width="110" />
2
+
3
+ # FEZ - Custom DOM Elements
4
+
5
+ Check the Demo site https://dux.github.io/fez/
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.
8
+
9
+ It uses
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.
15
+
16
+ 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
+
18
+ ## How to install
19
+
20
+ `<script src="https://dux.github.io/fez/dist/fez.js"></script>`
21
+
22
+ ## Little more details
23
+
24
+ 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.
25
+
26
+ Although fastest, Modifying DOM state directly in React / Vue / etc. is considered an anti-pattern. For `Fez` this is just fine if you want to do it. `Fez` basically modifies DOM, you just have a few helpers to help you do it.
27
+
28
+ It replaces modern JS frameworks by using native Autonomous Custom Elements to create new HTML tags. This has been supported for years in [all major browsers](https://caniuse.com/custom-elementsv1).
29
+
30
+ 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
+
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 jQuery, 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).
33
+
34
+ It great in combination with another widely used JS libs, as jQuery, Zepto, underscore of loDash.
35
+
36
+ ## How it works
37
+
38
+ * define your custom component - `Fez('ui-foo', class UiFoo extends FezBase)`
39
+ * add HTML - `<ui-foo bar="baz" id="node1"></ui-foo>`
40
+ * lib will call `node1.fez.init()` when node is added to DOM and connect your component to dom.
41
+ * use `Fez` helper methods, or do all by yourself, all good.
42
+
43
+ That is all.
44
+
45
+ ## Example: Counter Component
46
+
47
+ Here's a simple counter component that demonstrates Fez's core features:
48
+
49
+ ```html
50
+ <!-- Define a counter component in ex-counter.fez.html -->
51
+ <script>
52
+ init() {
53
+ // called when Fez node is connected to DOM
54
+ this.MAX = 6
55
+ this.state.count = 0
56
+ }
57
+
58
+ onStateChange(key, value, oldValue) {
59
+ // called whenever this.state changes
60
+ console.log(`State ${key} changed from ${oldValue} to ${value}`)
61
+ if (key === 'count' && value >= this.MAX) {
62
+ console.log('Counter reached maximum!')
63
+ }
64
+ }
65
+
66
+ isMax() {
67
+ // is state is changed, template is re-rendered
68
+ return this.state.count >= this.MAX
69
+ }
70
+
71
+ more() {
72
+ this.state.count += this.isMax() ? 0 : 1
73
+ }
74
+ </script>
75
+
76
+ <style>
77
+ /* compiles from scss to css and injects class in head */
78
+ /* body style */
79
+ background-color: #f7f7f7;
80
+
81
+ /* scoped to this component */
82
+ :fez {
83
+ zoom: 2;
84
+ margin: 10px 0;
85
+
86
+ button {
87
+ position: relative;
88
+ top: -3px;
89
+ }
90
+
91
+ span {
92
+ padding: 0 5px;
93
+ }
94
+ }
95
+ </style>
96
+
97
+ <button onclick="fez.state.count -= 1" disabled={{ state.count == 1 }}>-</button>
98
+
99
+ <span>
100
+ {{ state.count }}
101
+ </span>
102
+
103
+ <button onclick="fez.more()" disabled={{ isMax() }}>+</button>
104
+ {{if state.count > 0}}
105
+ <span>&mdash;</span>
106
+ {{if state.count == MAX }}
107
+ MAX
108
+ {{else}}
109
+ {{#if state.count % 2 }}
110
+ odd
111
+ {{else}}
112
+ even
113
+ {{/if}}
114
+ {{/if}}
115
+ {{/if}}
116
+ ```
117
+
118
+ To use this component in your HTML:
119
+
120
+ ```html
121
+ <!-- Load Fez library -->
122
+ <script src="https://dux.github.io/fez/dist/fez.js"></script>
123
+
124
+ <!-- Load component via template tag -->
125
+ <template fez="/fez-libs/ex-counter.fez.html"></template>
126
+
127
+ <!-- Use the component -->
128
+ <ex-counter></ex-counter>
129
+ ```
130
+
131
+ This example showcases:
132
+ - **Reactive state**: Changes to `this.state` automatically update the DOM
133
+ - **Template syntax**: `{{ }}` for expressions, `@` as shorthand for `this.`
134
+ - **Event handling**: Direct DOM event handlers with access to component methods
135
+ - **Conditional rendering**: `{{#if}}`, `{{:else}}` blocks for dynamic UI
136
+ - **Scoped styling**: SCSS support with styles automatically scoped to component
137
+ - **Component lifecycle**: `init()` method called when component mounts
138
+
139
+ ## What can it do and why is it great?
140
+
141
+ ### Core Features
142
+
143
+ * **Native Custom Elements** - Creates and defines Custom HTML tags using the native browser interface for maximum performance
144
+ * **Server-Side Friendly** - Works seamlessly with server-generated HTML, any routing library, and progressive enhancement strategies
145
+ * **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
146
+ * **Single-File Components** - Define CSS, HTML, and JavaScript in one file, no build step required
147
+ * **No Framework Magic** - Plain vanilla JS classes with clear, documented methods. No hooks, runes, or complex abstractions
148
+ * **Runtime SCSS** - Style components using SCSS syntax via [Goober](https://goober.js.org/), compiled at runtime
149
+ * **Smart Memory Management** - Automatic garbage collection cleans up disconnected nodes every 5 seconds
150
+
151
+ ### Advanced Templating & Styling
152
+
153
+ * **Powerful Template Engine** - Multiple syntaxes (`{{ }}` and `[[ ]]`), control flow (`#if`, `#unless`, `#for`, `#each`), and block templates
154
+ * **Reactive State Management** - Built-in reactive `state` object automatically triggers re-renders on property changes
155
+ * **DOM Morphing** - Uses [Idiomorph](https://github.com/bigskysoftware/idiomorph) for intelligent DOM updates that preserve element state and animations
156
+ * **Style Macros** - Define custom CSS shortcuts like `Fez.styleMacro('mobile', '@media (max-width: 768px)')` and use as `:mobile { ... }`
157
+ * **Scoped & Global Styles** - Components can define both scoped CSS (`:fez { ... }`) and global styles in the same component
158
+
159
+ ### Developer Experience
160
+
161
+ * **Built-in Utilities** - Helpful methods like `formData()`, `setInterval()` (auto-cleanup), `onResize()`, and `nextTick()`
162
+ * **Two-Way Data Binding** - Use `fez-bind` directive for automatic form synchronization
163
+ * **Advanced Slot System** - Full `<slot />` support with event listener preservation
164
+ * **Publish/Subscribe** - Built-in pub/sub system for component communication
165
+ * **Global State Management** - Automatic subscription-based global state with `this.globalState` proxy
166
+ * **Dynamic Component Loading** - Load components from URLs with `<template fez="path/to/component.html">`
167
+ * **Auto HTML Correction** - Fixes invalid self-closing tags (`<fez-icon name="gear" />` → `<fez-icon name="gear"></fez-icon>`)
168
+
169
+ ### Performance & Integration
170
+
171
+ * **Fast/Slow Render Modes** - Optimize initial render with `FAST = true` to prevent flickering
172
+ * **Request Animation Frame** - Smart RAF integration for smooth updates
173
+ * **Built-in Fetch with Caching** - `Fez.fetch()` includes automatic response caching and JSON/FormData handling
174
+ * **Global Component Access** - Register components globally with `GLOBAL = 'ComponentName'` for easy access
175
+ * **Rich Lifecycle Hooks** - `init`, `onMount`, `beforeRender`, `afterRender`, `onDestroy`, `onPropsChange`, `onStateChange`, `onGlobalStateChange`
176
+ * **Development Mode** - Enable detailed logging with `Fez.DEV = true`
177
+
178
+ ### Why It's Great
179
+
180
+ * **Zero Build Step** - Just include the script and start coding
181
+ * **20KB Minified** - Tiny footprint with powerful features
182
+ * **Framework Agnostic** - Use alongside React, Vue, or any other framework
183
+ * **Progressive Enhancement** - Perfect for modernizing legacy applications one component at a time
184
+ * **Native Performance** - Leverages browser's native Custom Elements API
185
+ * **Intuitive API** - If you know vanilla JavaScript, you already know Fez
186
+
187
+ ## Full available interface
188
+
189
+ ### Fez Static Methods
190
+
191
+ ```js
192
+ Fez('#foo') // find fez node with id="foo"
193
+ Fez('ui-tabs', this) // find first parent node ui-tabs
194
+
195
+ // add global scss
196
+ Fez.globalCss(`
197
+ .some-class {
198
+ color: red;
199
+ &.foo { ... }
200
+ .foo { ... }
201
+ }
202
+ ...
203
+ `)
204
+
205
+ // internal, get unique ID for a string, poor mans MD5 / SHA1
206
+ Fez.fnv1('some string')
207
+
208
+ // get generated css class name, from scss source string
209
+ Fez.css(text)
210
+
211
+ // get generated css class name without global attachment
212
+ Fez.cssClass(text)
213
+
214
+ // display information about fast/slow bind components in console
215
+ Fez.info()
216
+
217
+ // low-level DOM morphing function
218
+ Fez.morphdom(target, newNode, opts)
219
+
220
+ // HTML escaping utility
221
+ Fez.htmlEscape(text)
222
+
223
+ // create HTML tags with encoded props
224
+ Fez.tag(tag, opts, html)
225
+
226
+ // execute function until it returns true
227
+ Fez.untilTrue(func, pingRate)
228
+
229
+ // add scripts/styles to document head
230
+ // Load JavaScript from URL: Fez.head({ js: 'path/to/script.js' })
231
+ // Load JavaScript with attributes: Fez.head({ js: 'path/to/script.js', type: 'module', async: true })
232
+ // Load JavaScript with callback: Fez.head({ js: 'path/to/script.js' }, () => console.log('loaded'))
233
+ // Load JavaScript module and auto-import to window: Fez.head({ js: 'path/to/module.js', module: 'MyModule' })
234
+ // Load CSS: Fez.head({ css: 'path/to/styles.css' })
235
+ // Load CSS with attributes: Fez.head({ css: 'path/to/styles.css', media: 'print' })
236
+ // Execute inline script: Fez.head({ script: 'console.log("Hello world")' })
237
+ Fez.head(config, callback)
238
+
239
+ // define custom style macro
240
+ // Fez.styleMacro('mobile', '@media (max-width: 768px)')
241
+ // :mobile { ... } -> @media (max-width: 768px) { ... }
242
+ Fez.styleMacro(name, value)
243
+
244
+ // define custom DOM node name -> <foo-bar>...
245
+ Fez('foo-bar', class {
246
+ // set element node name, set as property or method, defaults to DIV
247
+ // why? because Fez renames custom dom nodes to regular HTML nodes
248
+ NAME = 'span'
249
+ NAME(node) { ... }
250
+ // alternative: static nodeName = 'span'
251
+
252
+ // set element style, set as property or method
253
+ CSS = `scss string... `
254
+
255
+ // unless node has no innerHTML on initialization, bind will be set to slow (fastBind = false)
256
+ // if you are using components that to not use innerHTML and slots, enable fast bind (fastBind = true)
257
+ // component will be rendered as parsed, and not on next tick (reduces flickering)
258
+ // <fez-icon name="gear" />
259
+ FAST = true
260
+ FAST(node) { ... }
261
+ // alternative: static fastBind() { return true }
262
+
263
+ // define static HTML. calling `this.render()` (no arguments) will refresh current node.
264
+ // if you pair it with `reactiveStore()`, to auto update on props change, you will have Svelte or Vue style reactive behaviour.
265
+ HTML = `...`
266
+
267
+ // Make it globally accessible as `window.Dialog`
268
+ // The component is automatically appended to the document body as a singleton. See `demo/fez/ui-dialog.fez` for a complete example.
269
+ GLOBAL = 'Dialog'
270
+ GLOBAL = true // just append node to document, do not create window reference
271
+
272
+ // use connect or created
273
+ init(props) {
274
+ // copy original attributes from attr hash to root node
275
+ this.copy('href', 'onclick', 'style')
276
+
277
+ // set style property to root node. look at a clock example
278
+ // shortcut to this.root.style.setProperty(key, value)
279
+ this.setStyle('--color', 'red')
280
+
281
+ // clasic interval, that runs only while node is attached
282
+ this.setInterval(func, tick) { ... }
283
+
284
+ // get closest form data, as object
285
+ this.formData()
286
+
287
+ // mounted DOM node root
288
+ this.root
289
+
290
+ // mounted DOM node root wrapped in $, only if jQuery is available
291
+ this.$root
292
+
293
+ // node attributes, converted to properties
294
+ this.props
295
+
296
+ // gets single node attribute or property
297
+ this.prop('onclick')
298
+
299
+ // shortcut for this.root.querySelector(selector)
300
+ this.find(selector)
301
+
302
+ // gets value for FORM fields or node innerHTML
303
+ this.val(selector)
304
+ // set value to a node, uses value or innerHTML
305
+ this.val(selector, value)
306
+
307
+ // you can publish globally, and subscribe locally
308
+ Fez.publish('channel', foo)
309
+ this.subscribe('channel', (foo) => { ... })
310
+
311
+ // gets root childNodes
312
+ this.childNodes()
313
+ this.childNodes(func) // pass function to loop forEach on selection, removed nodes from DOM
314
+
315
+ // check if the this.root node is attached to dom
316
+ this.isConnected()
317
+
318
+ // on every "this.state" props change, auto update view.
319
+ this.state = this.reactiveStore()
320
+ // this.state has reactiveStore() attached by default. any change will trigger this.render()
321
+ this.state.foo = 123
322
+
323
+ // window resize event with cleanup
324
+ this.onResize(func, delay)
325
+
326
+ // requestAnimationFrame wrapper with deduplication
327
+ this.nextTick(func, name)
328
+
329
+ // get/set unique ID for root node
330
+ this.rootId()
331
+
332
+ // get/set attributes on root node
333
+ this.attr(name, value)
334
+
335
+ // hide the custom element wrapper and move children to parent
336
+ this.fezHide()
337
+
338
+ // execute after connect and initial component render
339
+ this.onMount() { ... } // or this.connected() { ... }
340
+
341
+ // execute before or after every render
342
+ this.beforeRender() { ... }
343
+ this.afterRender() { ... }
344
+
345
+ // if you want to monitor new or changed node attributes
346
+ // monitors all original node attributes
347
+ // <ui-icon name="home" color="red" />
348
+ this.onPropsChange(attrName, attrValue) { ... }
349
+
350
+ // called when local component state changes
351
+ this.onStateChange(key, value, oldValue) { ... }
352
+
353
+ // called when global state changes (if component reads that key)
354
+ this.onGlobalStateChange(key, value) { ... }
355
+
356
+ // called when component is destroyed
357
+ this.onDestroy() { ... }
358
+
359
+ // automatic form submission handling if defined
360
+ this.onSubmit(formData) { ... }
361
+
362
+ // render template and attach result dom to root. uses Idiomorph for DOM morph
363
+ this.render()
364
+
365
+ // you can render to another root too
366
+ this.render(this.find('.body'), someHtmlTemplate)
367
+ }
368
+ })
369
+ ```
370
+
371
+ ## Fez script loading and definition
372
+
373
+ ```html
374
+ <!-- Remote loading for a component via URL in fez attribute -->
375
+ <!-- Component name is extracted from filename (ui-button) -->
376
+ <!-- If remote HTML contains template/xmp tags with fez attributes, they are compiled -->
377
+ <!-- Otherwise, the entire content is compiled as the component -->
378
+ <script fez="path/to/ui-button.fez.html"></script>
379
+
380
+ <!-- prefix with : to calc before node mount -->
381
+ <foo-bar :size="document.getElementById('icon-range').value"></foo-bar>
382
+
383
+ <!-- pass JSON props via data-props -->
384
+ <foo-bar data-props='{"name": "John", "age": 30}'></foo-bar>
385
+
386
+ <!-- pass JSON template via data-json-template -->
387
+ <script type="text/template">{...}</script>
388
+ <foo-bar data-json-template="true"></foo-bar>
389
+
390
+ <!-- override slow bind behavior -->
391
+ <foo-bar fez-fast="true"></foo-bar>
392
+ ```
393
+
394
+ ## Component structure
395
+
396
+ All parts are optional
397
+
398
+ ```html
399
+ <!-- Head elements support (inline only in XML tags) -->
400
+ <xmp tag="some-tag">
401
+ <head>
402
+ <!-- everything in head will be copied to document head-->
403
+ <script>console.log('Added to document head, first script to execute.')</script>
404
+ </head>
405
+
406
+ <script>
407
+ class {
408
+ init(props) { ... } // when fez node is initialized, before template render
409
+ onMount(props) { ... } // called after first template render
410
+ }
411
+ </script>
412
+ <script> // class can be omitted if only functions are passed
413
+ init(props) { ... }
414
+ </script>
415
+
416
+ <style>
417
+ b {
418
+ color: red; /* will be global style*/
419
+ }
420
+
421
+ :fez {
422
+ /* component styles */
423
+ }
424
+ </style>
425
+ <style>
426
+ color: red; /* if "body {" or ":fez {" is not found, style is considered local component style */
427
+ </style>
428
+
429
+ <div> ... <!-- any other html after head, script or style is considered template-->
430
+ <!-- resolve any condition -->
431
+ {{if foo}} ... {{/if}}
432
+
433
+ <!-- unless directive - opposite of if -->
434
+ {{unless fez.list.length}}
435
+ <p>No items to display</p>
436
+ {{/unless}}
437
+
438
+ <!-- runs in node scope, you can use for loop -->
439
+ {{each fez.list as name, index}} ... {{/each}}
440
+ {{for name, index in fez.list}} ... {{/for}}
441
+
442
+ <!-- Block definitions -->
443
+ {{block image}}
444
+ <img src={{ props.src}} />
445
+ {{/block}}
446
+ {{block:image}}<!-- Use the header block -->
447
+ {{block:image}}<!-- Use the header block -->
448
+
449
+ <!-- fez-this will link DOM node to object property (inspired by Svelte) -->
450
+ <!-- this.listRoot -->
451
+ <ul fez-this="listRoot">
452
+
453
+ <!-- when node is added to dom fez-use will call object function by name, and pass current node -->
454
+ <!-- this.animate(node) -->
455
+ <li fez-use="animate">
456
+
457
+ <!-- fez-bind for two-way data binding on form elements -->
458
+ <input type="text" fez-bind="state.username" />
459
+ <input onkeyup="fez.list[{{ index }}].name = fez.value" value="{{ name }}" />
460
+
461
+ <!--
462
+ fez-class for adding classes with optional delay.
463
+ class will be added to SPAN element, 100ms after dom mount (to trigger animations)
464
+ -->
465
+ <span fez-class="active:100">Delayed class</span>
466
+
467
+ <!-- :attribute for evaluated attributes (converts to JSON) -->
468
+ <div :data-config="state.config"></div>
469
+ </div>
470
+ </xmp>
471
+ ```
472
+
473
+ ### how to call custom FEZ node from the outside, anywhere in HTML
474
+
475
+ Inside `init()`, you have pointer to `this`. Pass it anywhere you need, even store in window.
476
+
477
+ Example: Dialog controller
478
+
479
+ ```html
480
+ <ui-dialog id="main-dialog"></ui-dialog>
481
+ ```
482
+
483
+ ```js
484
+ Fez('ui-dialog', class {
485
+ init() {
486
+ // makes dialog globally available
487
+ window.Dialog = this
488
+ }
489
+
490
+ close() {
491
+ ...
492
+ }
493
+ })
494
+
495
+ // close dialog window, from anywhere
496
+ Dialog.close()
497
+
498
+ // you can load via Fez + node selector
499
+ Fez('#main-dialog').close()
500
+ ```
501
+
502
+ ## Fez.fetch API
503
+
504
+ Fez includes a built-in fetch wrapper with automatic JSON parsing and session-based caching:
505
+
506
+ ### Basic Usage
507
+
508
+ ```js
509
+ // GET request with promise
510
+ const data = await Fez.fetch('https://api.example.com/data')
511
+
512
+ // GET request with callback, does not create promise
513
+ Fez.fetch('https://api.example.com/data', (data) => {
514
+ console.log(data)
515
+ })
516
+
517
+ // POST request
518
+ const result = await Fez.fetch('POST', 'https://api.example.com/data', { key: 'value' })
519
+ ```
520
+
521
+ ### Features
522
+
523
+ - **Automatic JSON parsing**: Response is automatically parsed if Content-Type is application/json
524
+ - **Session caching**: All requests are cached in memory until page refresh
525
+ - **Flexible parameter order**: Method can be omitted (defaults to GET), callback can be last parameter
526
+ - **Error handling**: When using callbacks, errors are passed to `Fez.onError` with kind 'fetch'
527
+ - **Logging**: Enable with `Fez.LOG = true` to see cache hits and live fetches
528
+
529
+ ### Custom Error Handler
530
+
531
+ ```js
532
+ // Override default error handler
533
+ Fez.onError = (kind, error) => {
534
+ if (kind === 'fetch') {
535
+ console.error('Fetch failed:', error)
536
+ // Show user-friendly error message
537
+ }
538
+ }
539
+ ```
540
+
541
+ ## Global State Management
542
+
543
+ 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.
544
+
545
+ ### How it Works
546
+
547
+ - Components access global state via `this.globalState` proxy
548
+ - Reading a value by key automatically subscribes the component to changes to that key.
549
+ - Setting a value notifies all subscribed components to that key.
550
+ - Components are automatically cleaned up when disconnected
551
+
552
+ ### Basic Usage
553
+
554
+ ```js
555
+ class Counter extends FezBase {
556
+ increment() {
557
+ // Setting global state - all listeners will be notified
558
+ this.globalState.count = (this.globalState.count || 0) + 1
559
+ }
560
+
561
+ render() {
562
+ // Reading global state - automatically subscribes this component
563
+ return `<button onclick="fez.increment()">
564
+ Count: ${this.globalState.count || 0}
565
+ </button>`
566
+ }
567
+ }
568
+ ```
569
+
570
+ ### External Access
571
+
572
+ ```js
573
+ // Set global state from outside components
574
+ Fez.state.set('count', 10)
575
+
576
+ // Get global state value
577
+ const count = Fez.state.get('count')
578
+
579
+ // Iterate over all components listening to a key
580
+ Fez.state.forEach('count', (component) => {
581
+ console.log(`${component.fezName} is listening to count`)
582
+ })
583
+ ```
584
+
585
+ ### Optional Change Handler
586
+
587
+ Components can define an `onGlobalStateChange` method for custom handling:
588
+
589
+ ```js
590
+ class MyComponent extends FezBase {
591
+ onGlobalStateChange(key, value) {
592
+ console.log(`Global state "${key}" changed to:`, value)
593
+ // Custom logic instead of automatic render
594
+ if (key === 'theme') {
595
+ this.updateTheme(value)
596
+ }
597
+ }
598
+
599
+ render() {
600
+ // Still subscribes by reading the value
601
+ return `<div class="${this.globalState.theme || 'light'}">...</div>`
602
+ }
603
+ }
604
+ ```
605
+
606
+ ### Real Example: Shared Counter State
607
+
608
+ ```js
609
+ // Multiple counter components sharing max count
610
+ class Counter extends FezBase {
611
+ init(props) {
612
+ this.state.count = parseInt(props.start || 0)
613
+ }
614
+
615
+ beforeRender() {
616
+ // All counters share and update the global max
617
+ this.globalState.maxCount ||= 0
618
+
619
+ // Find max across all counter instances
620
+ let max = 0
621
+ Fez.state.forEach('maxCount', fez => {
622
+ if (fez.state?.count > max) {
623
+ max = fez.state.count
624
+ }
625
+ })
626
+
627
+ this.globalState.maxCount = max
628
+ }
629
+
630
+ render() {
631
+ return `
632
+ <button onclick="fez.state.count++">+</button>
633
+ <span>Count: ${this.state.count}</span>
634
+ <span>(Global max: ${this.globalState.maxCount})</span>
635
+ `
636
+ }
637
+ }
638
+ ```