@dinoreic/fez 0.2.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoreic/fez",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Runtime custom dom elements",
5
5
  "main": "dist/fez.js",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "bin": {
23
- "fez": "./bin/fez"
23
+ "fez": "bin/fez"
24
24
  },
25
25
  "files": [
26
26
  "bin",
@@ -29,17 +29,6 @@
29
29
  "README.md",
30
30
  "LICENSE"
31
31
  ],
32
- "scripts": {
33
- "build": "bun build.js b",
34
- "b": "bun run build",
35
- "watch": "bun build.js w",
36
- "server": "bun run lib/server.js",
37
- "dev": "bunx concurrently --kill-others \"bun run server\" \"find src demo lib | entr -c sh -c 'bun run index && bun run b'\"",
38
- "test": "bun test",
39
- "prepublishOnly": "bun run build && bun run test",
40
- "publish": "npm publish --access public",
41
- "index": "ruby ./bin/fez-index 'demo/fez/*.fez' > demo/fez/index.json"
42
- },
43
32
  "keywords": [
44
33
  "dom",
45
34
  "elements",
@@ -66,5 +55,16 @@
66
55
  "happy-dom": "^18.0.1",
67
56
  "jsdom": "^26.1.0",
68
57
  "mime": "^4.0.7"
58
+ },
59
+ "scripts": {
60
+ "build": "bun build.js b",
61
+ "b": "bun run build",
62
+ "watch": "bun build.js w",
63
+ "server": "bun run lib/server.js",
64
+ "dev": "bunx concurrently --kill-others \"bun run server\" \"find src demo lib | entr -cn sh -c 'bun run index && bun run b'\"",
65
+ "test": "bun test",
66
+ "prepublishOnly": "bun run build && bun run test",
67
+ "publish": "npm publish --access public",
68
+ "index": "ruby ./bin/fez-index 'demo/fez/*.fez' > demo/fez/index.json"
69
69
  }
70
70
  }
@@ -1,94 +1,122 @@
1
- // templating
2
1
  import createTemplate from './lib/template.js'
3
2
  import FezBase from './instance.js'
4
3
 
5
- // this function accepts custom tag name and class definition, creates and connects
6
- // Fez(name, klass)
7
- export default function(name, klass) {
4
+ /**
5
+ * Registers a new custom element with Fez framework
6
+ * @param {string} name - Custom element name (must contain a dash)
7
+ * @param {Class|Object} klass - Component class or configuration object
8
+ * @example
9
+ * Fez('my-component', class {
10
+ * HTML = '<div>Hello World</div>'
11
+ * CSS = '.my-component { color: blue; }'
12
+ * })
13
+ */
14
+ export default function connect(name, klass) {
8
15
  const Fez = globalThis.window?.Fez || globalThis.Fez;
9
16
  // Validate custom element name format (must contain a dash)
10
17
  if (!name.includes('-')) {
11
18
  console.error(`Fez: Invalid custom element name "${name}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
12
19
  }
13
20
 
14
- // to allow anonymous class and then re-attach (does not work)
15
- // Fez('ui-todo', class { ... # instead Fez('ui-todo', class extends FezBase {
21
+ // Transform simple class definitions into Fez components
16
22
  if (!klass.fezHtmlRoot) {
17
23
  const klassObj = new klass()
18
24
  const newKlass = class extends FezBase {}
19
25
 
26
+ // Copy all properties and methods from the original class
20
27
  const props = Object.getOwnPropertyNames(klassObj)
21
28
  .concat(Object.getOwnPropertyNames(klass.prototype))
22
29
  .filter(el => !['constructor', 'prototype'].includes(el))
23
30
 
24
31
  props.forEach(prop => newKlass.prototype[prop] = klassObj[prop])
25
32
 
26
- Fez.fastBindInfo ||= {fast: [], slow: []}
27
-
28
- if (klassObj.GLOBAL) { newKlass.fezGlobal = klassObj.GLOBAL }
29
- if (klassObj.CSS) { newKlass.css = klassObj.CSS }
30
- if (klassObj.HTML) { newKlass.html = klassObj.HTML }
31
- if (klassObj.NAME) { newKlass.nodeName = klassObj.NAME }
32
- if (klassObj.FAST) {
33
- newKlass.fastBind = klassObj.FAST
34
- Fez.fastBindInfo.fast.push(typeof klassObj.FAST == 'function' ? `${name} (func)` : name)
35
- } else {
36
- Fez.fastBindInfo.slow.push(name)
33
+ // Map component configuration properties
34
+ if (klassObj.GLOBAL) { newKlass.fezGlobal = klassObj.GLOBAL } // Global instance reference
35
+ if (klassObj.CSS) { newKlass.css = klassObj.CSS } // Component styles
36
+ if (klassObj.HTML) {
37
+ newKlass.html = closeCustomTags(klassObj.HTML) // Component template
37
38
  }
39
+ if (klassObj.NAME) { newKlass.nodeName = klassObj.NAME } // Custom DOM node name
38
40
 
41
+ // Auto-mount global components to body
39
42
  if (klassObj.GLOBAL) {
40
- const func = () => document.body.appendChild(document.createElement(name))
43
+ const mountGlobalComponent = () => document.body.appendChild(document.createElement(name))
41
44
 
42
45
  if (document.readyState === 'loading') {
43
- document.addEventListener('DOMContentLoaded', func);
46
+ document.addEventListener('DOMContentLoaded', mountGlobalComponent);
44
47
  } else {
45
- func()
48
+ mountGlobalComponent()
46
49
  }
47
50
  }
48
51
 
49
52
  klass = newKlass
50
53
 
51
- let info = `${name} compiled`
52
- if (klassObj.FAST) info += ' (fast bind)'
53
- Fez.log(info)
54
+ Fez.log(`${name} compiled`)
55
+ } else if (klass.html) {
56
+ // If klass already has html property, process it
57
+ klass.html = closeCustomTags(klass.html)
54
58
  }
55
59
 
60
+ // Process component template
56
61
  if (klass.html) {
57
- klass.html = closeCustomTags(klass.html)
58
-
59
- // wrap slot to enable reactive re-renders. It will use existing .fez-slot if found
62
+ // Replace <slot /> with reactive slot containers
60
63
  klass.html = klass.html.replace(/<slot\s*\/>|<slot\s*>\s*<\/slot>/g, () => {
61
- const name = klass.SLOT || 'div'
62
- return `<${name} class="fez-slot" fez-keep="default-slot"></${name}>`
64
+ const slotTag = klass.SLOT || 'div'
65
+ return `<${slotTag} class="fez-slot" fez-keep="default-slot"></${slotTag}>`
63
66
  })
64
67
 
68
+ // Compile template function
65
69
  klass.fezHtmlFunc = createTemplate(klass.html)
66
70
  }
67
71
 
68
- // we have to register global css on component init, because some other component can depend on it (it is global)
72
+ // Register component styles globally (available to all components)
69
73
  if (klass.css) {
70
74
  klass.css = Fez.globalCss(klass.css, {name: name})
71
75
  }
72
76
 
73
77
  Fez.classes[name] = klass
74
78
 
79
+ connectCustomElement(name, klass)
80
+ }
81
+
82
+ /**
83
+ * Registers the custom element with the browser
84
+ * Sets up batched rendering for optimal performance
85
+ */
86
+ function connectCustomElement(name, klass) {
87
+ const Fez = globalThis.window?.Fez || globalThis.Fez;
88
+
75
89
  if (!customElements.get(name)) {
76
90
  customElements.define(name, class extends HTMLElement {
77
91
  connectedCallback() {
78
- // if you want to force fast render (prevent page flickering), add static fastBind = true or FAST = true
79
- // we can not fast load auto for all because that creates hard to debug problems in nested custom nodes
80
- // problems with events and slots (I woke up at 2AM, now it is 5AM)
81
- // this is usually safe for first order components, as page header or any components that do not have innerHTML or use slots
82
- // Example: you can add FAST as a function - render fast nodes that have name attribute
83
- // FAST(node) { return !!node.getAttribute('name') }
84
- // to inspect fast / slow components use Fez.info() in console
85
- if (useFastRender(this, klass)) {
86
- connectNode(name, this)
87
- } else {
88
- window.requestAnimationFrame(()=>{
89
- if (this.parentNode) {
90
- connectNode(name, this)
91
- }
92
+ // Batch all renders using microtasks for consistent timing and DOM completeness
93
+ if (!Fez._pendingConnections) {
94
+ Fez._pendingConnections = []
95
+ Fez._batchScheduled = false
96
+ }
97
+
98
+ Fez._pendingConnections.push({ name, node: this })
99
+
100
+ if (!Fez._batchScheduled) {
101
+ Fez._batchScheduled = true
102
+ Promise.resolve().then(() => {
103
+ const connections = Fez._pendingConnections.slice()
104
+ // console.error(`Batch processing ${connections.length} components:`, connections.map(c => c.name))
105
+ Fez._pendingConnections = []
106
+ Fez._batchScheduled = false
107
+
108
+ // Sort by DOM order to ensure parent nodes are processed before children
109
+ connections.sort((a, b) => {
110
+ if (a.node.contains(b.node)) return -1
111
+ if (b.node.contains(a.node)) return 1
112
+ return 0
113
+ })
114
+
115
+ connections.forEach(({ name, node }) => {
116
+ if (node.isConnected && node.parentNode) {
117
+ connectNode(name, node)
118
+ }
119
+ })
92
120
  })
93
121
  }
94
122
  }
@@ -96,8 +124,10 @@ export default function(name, klass) {
96
124
  }
97
125
  }
98
126
 
99
- //
100
-
127
+ /**
128
+ * Converts self-closing custom tags to full open/close format
129
+ * Required for proper HTML parsing of custom elements
130
+ */
101
131
  function closeCustomTags(html) {
102
132
  const selfClosingTags = new Set([
103
133
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr'
@@ -108,17 +138,11 @@ function closeCustomTags(html) {
108
138
  })
109
139
  }
110
140
 
111
- function useFastRender(n, klass) {
112
- const fezFast = n.getAttribute('fez-fast')
113
- var isFast = typeof klass.fastBind === 'function' ? klass.fastBind(n) : klass.fastBind
114
-
115
- if (fezFast == 'false') {
116
- return false
117
- } else {
118
- return fezFast || isFast
119
- }
120
- }
121
141
 
142
+ /**
143
+ * Initializes a Fez component instance from a DOM node
144
+ * Replaces the custom element with the component's rendered content
145
+ */
122
146
  function connectNode(name, node) {
123
147
  const klass = Fez.classes[name]
124
148
  const parentNode = node.parentNode
@@ -143,7 +167,7 @@ function connectNode(name, node) {
143
167
  fez.props = klass.getProps(node, newNode)
144
168
  fez.class = klass
145
169
 
146
- // move child nodes, natively to preserve bound events
170
+ // Move child nodes to preserve DOM event listeners
147
171
  fez.slot(node, newNode)
148
172
 
149
173
  newNode.fez = fez
@@ -160,10 +184,17 @@ function connectNode(name, node) {
160
184
  newNode.setAttribute('id', fez.props.id)
161
185
  }
162
186
 
163
- fez.fezRegister();
164
- ;(fez.init || fez.created || fez.connect).bind(fez)(fez.props);
187
+ // Component lifecycle initialization
188
+ fez.fezRegister()
189
+
190
+ // Call initialization method (init, created, or connect)
191
+ ;(fez.init || fez.created || fez.connect).bind(fez)(fez.props)
192
+
193
+ // Initial render
165
194
  fez.render()
166
195
  fez.firstRender = true
196
+
197
+ // Trigger mount lifecycle hook
167
198
  fez.onMount(fez.props)
168
199
 
169
200
  if (fez.onSubmit) {
@@ -174,9 +205,11 @@ function connectNode(name, node) {
174
205
  }
175
206
  }
176
207
 
177
- // if onPropsChange method defined, add observer and trigger call on all attributes once component is loaded
208
+ // Set up reactive attribute watching
178
209
  if (fez.onPropsChange) {
179
210
  observer.observe(newNode, {attributes:true})
211
+
212
+ // Trigger initial prop change callbacks
180
213
  for (const [key, value] of Object.entries(fez.props)) {
181
214
  fez.onPropsChange(key, value)
182
215
  }
@@ -184,8 +217,10 @@ function connectNode(name, node) {
184
217
  }
185
218
  }
186
219
 
187
- //
188
-
220
+ /**
221
+ * Global mutation observer for reactive attribute changes
222
+ * Watches for attribute changes and triggers component updates
223
+ */
189
224
  const observer = new MutationObserver((mutationsList, _) => {
190
225
  for (const mutation of mutationsList) {
191
226
  if (mutation.type === 'attributes') {
@@ -3,8 +3,6 @@ const loadDefaults = () => {
3
3
  // include fez component by name
4
4
  //<fez-component name="some-node" :props="fez.props"></fez-component>
5
5
  Fez('fez-component', class {
6
- FAST = true
7
-
8
6
  init(props) {
9
7
  const tag = document.createElement(props.name)
10
8
  tag.props = props.props || props['data-props'] || props
@@ -21,8 +19,6 @@ const loadDefaults = () => {
21
19
  // include remote data from url
22
20
  // <fez-include src="./demo/fez/ui-slider.html"></fez-include>
23
21
  Fez('fez-include', class {
24
- FAST = true
25
-
26
22
  init(props) {
27
23
  Fez.fetch(props.src, (data)=>{
28
24
  const dom = Fez.domRoot(data)
@@ -45,7 +41,6 @@ const loadDefaults = () => {
45
41
  const hash = Fez.fnv1(this.root.outerHTML)
46
42
  const nodeName = `inline-${hash}`
47
43
  Fez(nodeName, class {
48
- FAST = true
49
44
  HTML = html
50
45
  init() {
51
46
  Object.assign(this.state, props.state || {})
@@ -81,11 +81,6 @@ export default class FezBase {
81
81
  return formObject
82
82
  }
83
83
 
84
- static fastBind() {
85
- // return true to bind without requestAnimationFrame
86
- // you can do this if you are sure you are not expecting innerHTML data
87
- return false
88
- }
89
84
 
90
85
  static nodeName = 'div'
91
86
 
@@ -106,42 +101,11 @@ export default class FezBase {
106
101
  if (this.root?.isConnected) {
107
102
  return true
108
103
  } else {
109
- this.fezRemoveSelf()
104
+ this.fezOnDestroy()
110
105
  return false
111
106
  }
112
107
  }
113
108
 
114
- // clear all node references
115
- fezRemoveSelf() {
116
- this._setIntervalCache ||= {}
117
- Object.keys(this._setIntervalCache).forEach((key)=> {
118
- clearInterval(this._setIntervalCache[key])
119
- })
120
-
121
- if (this._eventHandlers) {
122
- Object.entries(this._eventHandlers).forEach(([eventName, handler]) => {
123
- window.removeEventListener(eventName, handler);
124
- });
125
- this._eventHandlers = {};
126
- }
127
-
128
- if (this._timeouts) {
129
- Object.values(this._timeouts).forEach(timeoutId => {
130
- clearTimeout(timeoutId);
131
- });
132
- this._timeouts = {};
133
- }
134
-
135
- this.onDestroy()
136
- this.onDestroy = () => {}
137
-
138
- if (this.root) {
139
- this.root.fez = undefined
140
- }
141
-
142
- this.root = undefined
143
- }
144
-
145
109
  // get single node property
146
110
  prop(name) {
147
111
  let v = this.oldRoot[name] || this.props[name]
@@ -178,80 +142,92 @@ export default class FezBase {
178
142
  }
179
143
  }
180
144
 
181
- // Generic function to handle window events with automatic cleanup
182
- // eventName: 'resize', 'scroll', etc.
183
- // func: callback function to execute
184
- // delay: throttle delay in ms (default: 100ms)
185
- on(eventName, func, delay = 200) {
186
- this._eventHandlers = this._eventHandlers || {};
187
- this._timeouts = this._timeouts || {};
188
-
189
- if (this._eventHandlers[eventName]) {
190
- window.removeEventListener(eventName, this._eventHandlers[eventName]);
145
+ // clear all node references
146
+ // Centralized destroy logic
147
+ fezOnDestroy() {
148
+ // Execute all registered cleanup callbacks
149
+ if (this._onDestroyCallbacks) {
150
+ this._onDestroyCallbacks.forEach(callback => {
151
+ try {
152
+ callback();
153
+ } catch (e) {
154
+ console.error('Fez: Error in cleanup callback:', e);
155
+ }
156
+ });
157
+ this._onDestroyCallbacks = [];
191
158
  }
192
159
 
193
- if (this._timeouts[eventName]) {
194
- clearTimeout(this._timeouts[eventName]);
160
+ // Call user's onDestroy lifecycle hook
161
+ this.onDestroy()
162
+ this.onDestroy = () => {}
163
+
164
+ // Clean up root references
165
+ if (this.root) {
166
+ this.root.fez = undefined
195
167
  }
196
168
 
197
- let lastRun = 0;
169
+ this.root = undefined
170
+ }
198
171
 
199
- const doExecute = () => {
200
- if (!this.isConnected) {
201
- if (this._eventHandlers[eventName]) {
202
- window.removeEventListener(eventName, this._eventHandlers[eventName]);
203
- delete this._eventHandlers[eventName];
204
- }
205
- if (this._timeouts[eventName]) {
206
- clearTimeout(this._timeouts[eventName]);
207
- delete this._timeouts[eventName];
208
- }
209
- return false;
210
- }
211
- func.call(this);
212
- return true;
213
- };
172
+ // Add a cleanup callback to be executed on destroy
173
+ addOnDestroy(callback) {
174
+ this._onDestroyCallbacks = this._onDestroyCallbacks || [];
175
+ this._onDestroyCallbacks.push(callback);
176
+ }
214
177
 
215
- const handleEvent = () => {
216
- const now = Date.now();
178
+ // Generic function to handle window events with automatic cleanup
179
+ on(eventName, func, delay = 200) {
180
+ this._eventHandlers = this._eventHandlers || {};
217
181
 
218
- if (now - lastRun >= delay) {
219
- if (doExecute()) {
220
- lastRun = now;
221
- } else {
222
- return;
223
- }
224
- }
182
+ if (this._eventHandlers[eventName]) {
183
+ window.removeEventListener(eventName, this._eventHandlers[eventName]);
184
+ }
225
185
 
226
- // Clear previous timeout and set new one to ensure final event
227
- if (this._timeouts[eventName]) {
228
- clearTimeout(this._timeouts[eventName]);
186
+ const throttledFunc = Fez.throttle(() => {
187
+ if (this.isConnected) {
188
+ func.call(this);
229
189
  }
190
+ }, delay);
230
191
 
231
- this._timeouts[eventName] = setTimeout(() => {
232
- if (now > lastRun && doExecute()) {
233
- lastRun = Date.now();
234
- }
235
- delete this._timeouts[eventName];
236
- }, delay);
237
- };
192
+ this._eventHandlers[eventName] = throttledFunc;
193
+ window.addEventListener(eventName, throttledFunc);
238
194
 
239
- this._eventHandlers[eventName] = handleEvent;
240
- window.addEventListener(eventName, handleEvent);
195
+ this.addOnDestroy(() => {
196
+ window.removeEventListener(eventName, throttledFunc);
197
+ delete this._eventHandlers[eventName];
198
+ });
241
199
  }
242
200
 
243
201
  // Helper function for resize events
244
- onResize(func, delay) {
202
+ onWindowResize(func, delay) {
245
203
  this.on('resize', func, delay);
246
204
  func();
247
205
  }
248
206
 
249
207
  // Helper function for scroll events
250
- onScroll(func, delay) {
208
+ onWindowScroll(func, delay) {
251
209
  this.on('scroll', func, delay);
252
210
  func();
253
211
  }
254
212
 
213
+ // Helper function for element resize events using ResizeObserver
214
+ onElementResize(el, func, delay = 200) {
215
+ const throttledFunc = Fez.throttle(() => {
216
+ if (this.isConnected) {
217
+ func.call(this, el.getBoundingClientRect(), el);
218
+ }
219
+ }, delay);
220
+
221
+ const observer = new ResizeObserver(throttledFunc);
222
+ observer.observe(el);
223
+
224
+ func.call(this, el.getBoundingClientRect(), el);
225
+
226
+ this.addOnDestroy(() => {
227
+ observer.disconnect();
228
+ });
229
+ }
230
+
255
231
  // copy child nodes, natively to preserve bound events
256
232
  // if node name is SLOT insert adjacent and remove SLOT, else as a child nodes
257
233
  slot(source, target) {
@@ -359,7 +335,8 @@ export default class FezBase {
359
335
 
360
336
  this.beforeRender()
361
337
 
362
- const newNode = document.createElement(this.class.nodeName || 'div')
338
+ const nodeName = typeof this.class.nodeName == 'function' ? this.class.nodeName(this.root) : this.class.nodeName
339
+ const newNode = document.createElement(nodeName || 'div')
363
340
 
364
341
  let renderedTpl
365
342
  if (Array.isArray(template)) {
@@ -486,13 +463,21 @@ export default class FezBase {
486
463
  this._setIntervalCache ||= {}
487
464
  clearInterval(this._setIntervalCache[name])
488
465
 
489
- this._setIntervalCache[name] = setInterval(() => {
466
+ const intervalID = setInterval(() => {
490
467
  if (this.isConnected) {
491
468
  func()
492
469
  }
493
470
  }, tick)
494
471
 
495
- return this._setIntervalCache[name]
472
+ this._setIntervalCache[name] = intervalID
473
+
474
+ // Register cleanup callback
475
+ this.addOnDestroy(() => {
476
+ clearInterval(intervalID);
477
+ delete this._setIntervalCache[name];
478
+ });
479
+
480
+ return intervalID
496
481
  }
497
482
 
498
483
  find(selector) {