@dinoreic/fez 0.2.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoreic/fez",
3
- "version": "0.2.2",
3
+ "version": "0.3.2",
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 || {})
@@ -58,6 +53,41 @@ const loadDefaults = () => {
58
53
  }
59
54
  }
60
55
  })
56
+
57
+ // Memory store for memoization
58
+ const memoStore = new Map()
59
+
60
+ // memoize component content by key
61
+ // <fez-memoize key="unique-key">content to memoize</fez-memoize>
62
+ Fez('fez-memoize', class {
63
+ init(props) {
64
+ if (!props.key) {
65
+ Fez.error('fez-memoize: key prop is required')
66
+ return
67
+ }
68
+
69
+ if (memoStore.has(props.key)) {
70
+ // Restore from memory in init
71
+ const storedNode = memoStore.get(props.key)
72
+ Fez.log(`Memoize - key: "${props.key}" - restore`)
73
+ this.root.innerHTML = ''
74
+ this.root.appendChild(storedNode.cloneNode(true))
75
+ }
76
+ }
77
+
78
+ onMount(props) {
79
+ // Only store if not already in memory
80
+ if (!memoStore.has(props.key)) {
81
+ requestAnimationFrame(() => {
82
+ // Store current DOM content
83
+ const contentNode = document.createElement('div')
84
+ contentNode.innerHTML = this.root.innerHTML
85
+ Fez.log(`Memoize - key: "${props.key}" - set`)
86
+ memoStore.set(props.key, contentNode)
87
+ })
88
+ }
89
+ }
90
+ })
61
91
  }
62
92
 
63
93
  // Only load defaults if Fez is available
@@ -66,4 +96,4 @@ if (typeof Fez !== 'undefined' && Fez) {
66
96
  }
67
97
 
68
98
  // Export for use in tests
69
- export { loadDefaults }
99
+ export { loadDefaults }