@dinoreic/fez 0.2.0 → 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/dist/log.js ADDED
@@ -0,0 +1,5 @@
1
+ (()=>{var u=o=>{let a=o.split(/(<\/?[^>]+>)/g).map(i=>i.trim()).filter(i=>i),n=0,t=[];for(let i=0;i<a.length;i++){let e=a[i],s=a[i+1],p=a[i+2];if(e.startsWith("<"))if(!e.startsWith("</")&&!e.endsWith("/>")&&s&&!s.startsWith("<")&&p&&p.startsWith("</")){let r=Math.max(0,n);t.push(" ".repeat(r)+e+s+p),i+=2}else if(e.startsWith("</")){n--;let r=Math.max(0,n);t.push(" ".repeat(r)+e)}else if(e.endsWith("/>")||e.includes(" />")){let r=Math.max(0,n);t.push(" ".repeat(r)+e)}else{let r=Math.max(0,n);t.push(" ".repeat(r)+e),n++}else if(e){let r=Math.max(0,n);t.push(" ".repeat(r)+e)}}return t.join(`
2
+ `)},c=(()=>{let o=[],a=[],n=0;return t=>{if(!document.body){window.requestAnimationFrame(()=>c(t));return}t instanceof Node&&(t=u(t.outerHTML));let i=typeof t;t===void 0&&(t="undefined"),t===null&&(t="null"),Array.isArray(t)?i="array":typeof t=="object"&&t!==null&&(i="object"),typeof t!="string"&&(t=JSON.stringify(t,(r,l)=>typeof l=="function"?String(l):l,2).replaceAll("<","&lt;")),t=t.trim(),o.push(t+`
3
+
4
+ type: ${i}`),a.push(i);let e=document.getElementById("dump-dialog");e||(e=document.body.appendChild(document.createElement("div")),e.id="dump-dialog",e.style.cssText="position:fixed;top:30px;left:30px;right:50px;bottom:50px;background:#fff;border:1px solid#333;box-shadow:0 0 10px rgba(0,0,0,0.5);padding:20px;overflow:auto;z-index:9999;font:13px/1.4 monospace;white-space:pre");let s=parseInt(localStorage.getItem("_LOG_INDEX"));!isNaN(s)&&s>=0&&s<o.length?n=s:n=o.length-1;let p=()=>{let r=o.map((l,d)=>{let f="#f0f0f0";return d!==n&&(a[d]==="object"?f="#d6e3ef":a[d]==="array"&&(f="#d8d5ef")),`<button style="padding:4px 8px;margin:0;cursor:pointer;background:${d===n?"#333":f};color:${d===n?"#fff":"#000"}" data-index="${d}">${d+1}</button>`}).join("");e.innerHTML='<div style="display:flex;flex-direction:column;height:100%"><div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px"><div style="display:flex;flex-wrap:wrap;gap:4px;flex:1;margin-right:10px">'+r+'</div><button style="padding:4px 8px;cursor:pointer;flex-shrink:0">&times;</button></div><xmp style="flex:1;overflow:auto;margin:0;padding:0;color:#000;background:#fff;font-size:14px;line-height:22px">'+o[n]+"</xmp></div>",e.querySelector('button[style*="flex-shrink:0"]').onclick=()=>e.remove(),e.querySelectorAll("button[data-index]").forEach(l=>{l.onclick=()=>{n=parseInt(l.dataset.index),localStorage.setItem("_LOG_INDEX",n),p()}})};p()}})();typeof window<"u"&&(window.LOG=c,window.LOG_PP=u);var x=c;})();
5
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/log.js"],
4
+ "sourcesContent": ["// pretty print HTML\nconst LOG_PP = (html) => {\n const parts = html\n .split(/(<\\/?[^>]+>)/g)\n .map(p => p.trim())\n .filter(p => p);\n\n let indent = 0;\n const lines = [];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const nextPart = parts[i + 1];\n const nextNextPart = parts[i + 2];\n\n // Check if it's a tag\n if (part.startsWith('<')) {\n // Check if this is an opening tag followed by text and then its closing tag\n if (!part.startsWith('</') && !part.endsWith('/>') && nextPart && !nextPart.startsWith('<') && nextNextPart && nextNextPart.startsWith('</')) {\n // Combine them on one line\n const actualIndent = Math.max(0, indent);\n lines.push(' '.repeat(actualIndent) + part + nextPart + nextNextPart);\n i += 2; // Skip the next two parts\n }\n // Closing tag\n else if (part.startsWith('</')) {\n indent--;\n const actualIndent = Math.max(0, indent);\n lines.push(' '.repeat(actualIndent) + part);\n }\n // Self-closing tag\n else if (part.endsWith('/>') || part.includes(' />')) {\n const actualIndent = Math.max(0, indent);\n lines.push(' '.repeat(actualIndent) + part);\n }\n // Opening tag\n else {\n const actualIndent = Math.max(0, indent);\n lines.push(' '.repeat(actualIndent) + part);\n indent++;\n }\n }\n // Text node\n else if (part) {\n const actualIndent = Math.max(0, indent);\n lines.push(' '.repeat(actualIndent) + part);\n }\n }\n\n return lines.join('\\n');\n}\n\nconst LOG = (() => {\n const logs = [];\n const logTypes = []; // Track the original type of each log\n let currentIndex = 0;\n\n return o => {\n if (!document.body) {\n window.requestAnimationFrame( () => LOG(o) )\n return\n }\n\n if (o instanceof Node) {\n o = LOG_PP(o.outerHTML)\n }\n\n // Store the original type\n let originalType = typeof o;\n\n if (o === undefined) { o = 'undefined' }\n if (o === null) { o = 'null' }\n\n if (Array.isArray(o)) {\n originalType = 'array';\n } else if (typeof o === 'object' && o !== null) {\n originalType = 'object';\n }\n\n if (typeof o != 'string') {\n o = JSON.stringify(o, (key, value) => {\n if (typeof value === 'function') {\n return String(value);\n }\n return value;\n }, 2).replaceAll('<', '&lt;')\n }\n\n o = o.trim()\n\n logs.push(o + `\\n\\ntype: ${originalType}`);\n logTypes.push(originalType);\n\n let d = document.getElementById('dump-dialog');\n if (!d) {\n d = document.body.appendChild(document.createElement('div'));\n d.id = 'dump-dialog';\n d.style.cssText =\n 'position:fixed;top:30px;left:30px;right:50px;bottom:50px;' +\n 'background:#fff;border:1px solid#333;box-shadow:0 0 10px rgba(0,0,0,0.5);' +\n 'padding:20px;overflow:auto;z-index:9999;font:13px/1.4 monospace;white-space:pre';\n }\n\n // Check if we have a saved index and it's still valid\n const savedIndex = parseInt(localStorage.getItem('_LOG_INDEX'));\n if (!isNaN(savedIndex) && savedIndex >= 0 && savedIndex < logs.length) {\n currentIndex = savedIndex;\n } else {\n currentIndex = logs.length - 1;\n }\n\n const renderContent = () => {\n const buttons = logs.map((_, i) => {\n let bgColor = '#f0f0f0'; // default\n if (i !== currentIndex) {\n if (logTypes[i] === 'object') {\n bgColor = '#d6e3ef'; // super light blue\n } else if (logTypes[i] === 'array') {\n bgColor = '#d8d5ef'; // super light indigo\n }\n }\n return `<button style=\"padding:4px 8px;margin:0;cursor:pointer;background:${i === currentIndex ? '#333' : bgColor};color:${i === currentIndex ? '#fff' : '#000'}\" data-index=\"${i}\">${i + 1}</button>`\n }).join('');\n\n d.innerHTML =\n '<div style=\"display:flex;flex-direction:column;height:100%\">' +\n '<div style=\"display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px\">' +\n '<div style=\"display:flex;flex-wrap:wrap;gap:4px;flex:1;margin-right:10px\">' + buttons + '</div>' +\n '<button style=\"padding:4px 8px;cursor:pointer;flex-shrink:0\">&times;</button>' +\n '</div>' +\n '<xmp style=\"flex:1;overflow:auto;margin:0;padding:0;color:#000;background:#fff;font-size:14px;line-height:22px\">' + logs[currentIndex] + '</xmp>' +\n '</div>';\n\n d.querySelector('button[style*=\"flex-shrink:0\"]').onclick = () => d.remove();\n\n d.querySelectorAll('button[data-index]').forEach(btn => {\n btn.onclick = () => {\n currentIndex = parseInt(btn.dataset.index);\n localStorage.setItem('_LOG_INDEX', currentIndex);\n renderContent();\n };\n });\n };\n\n renderContent();\n };\n})();\n\nif (typeof window !== 'undefined') {\n window.LOG = LOG\n window.LOG_PP = LOG_PP\n}\n\nexport default LOG\n"],
5
+ "mappings": "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",
6
+ "names": ["LOG_PP", "html", "parts", "p", "indent", "lines", "part", "nextPart", "nextNextPart", "actualIndent", "LOG", "logs", "logTypes", "currentIndex", "o", "originalType", "key", "value", "d", "savedIndex", "renderContent", "buttons", "_", "i", "bgColor", "btn", "log_default"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoreic/fez",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Runtime custom dom elements",
5
5
  "main": "dist/fez.js",
6
6
  "type": "module",
@@ -13,10 +13,14 @@
13
13
  "import": "./src/rollup.js",
14
14
  "require": "./src/rollup.js"
15
15
  },
16
+ "./log": {
17
+ "import": "./src/log.js",
18
+ "require": "./src/log.js"
19
+ },
16
20
  "./package.json": "./package.json"
17
21
  },
18
22
  "bin": {
19
- "fez": "./bin/fez"
23
+ "fez": "bin/fez"
20
24
  },
21
25
  "files": [
22
26
  "bin",
@@ -25,17 +29,6 @@
25
29
  "README.md",
26
30
  "LICENSE"
27
31
  ],
28
- "scripts": {
29
- "build": "bun build.js b",
30
- "b": "bun build.js b",
31
- "watch": "bun build.js w",
32
- "server": "bun run lib/server.js",
33
- "dev": "bunx concurrently --kill-others \"bun run server\" \"find src demo lib | entr -c sh -c 'bun run index && bun run b'\"",
34
- "test": "bun test",
35
- "prepublishOnly": "bun run build && bun run test",
36
- "publish": "npm publish --access public",
37
- "index": "ruby ./bin/fez-index 'demo/fez/*.fez' > demo/fez/index.json"
38
- },
39
32
  "keywords": [
40
33
  "dom",
41
34
  "elements",
@@ -62,5 +55,16 @@
62
55
  "happy-dom": "^18.0.1",
63
56
  "jsdom": "^26.1.0",
64
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"
65
69
  }
66
70
  }
@@ -36,8 +36,7 @@ const compileToClass = (html) => {
36
36
  }
37
37
 
38
38
  if (result.head) {
39
- const container = document.createElement('div')
40
- container.innerHTML = result.head
39
+ const container = Fez.domRoot(result.head)
41
40
 
42
41
  // Process all children of the container
43
42
  Array.from(container.children).forEach(node => {
@@ -89,66 +88,86 @@ const compileToClass = (html) => {
89
88
  return klass
90
89
  }
91
90
 
92
- // <template fez="ui-form">
93
- // <script>
94
- // ...
95
- // Fez.compile() # compile all
96
- // Fez.compile(templateNode) # compile template node
97
- // Fez.compile('ui-form', templateNode.innerHTML) # compile string
98
- export default function (tagName, html) {
99
- if (tagName instanceof Node) {
100
- const node = tagName
91
+ // Handle single argument cases - compile all, compile node, or compile from URL
92
+ function compile_bulk(data) {
93
+ if (data instanceof Node) {
94
+ const node = data
101
95
  node.remove()
102
96
 
103
97
  const fezName = node.getAttribute('fez')
104
98
 
105
99
  // Check if fezName contains dot or slash (indicates URL)
106
100
  if (fezName && (fezName.includes('.') || fezName.includes('/'))) {
107
- const url = fezName
108
-
109
- Fez.log(`Loading from ${url}`)
110
-
111
- // Load HTML content via AJAX from URL
112
- Fez.fetch(url)
113
- .then(htmlContent => {
114
- // Check if remote HTML has template/xmp tags with fez attribute
115
- const parser = new DOMParser()
116
- const doc = parser.parseFromString(htmlContent, 'text/html')
117
- const fezElements = doc.querySelectorAll('template[fez], xmp[fez]')
118
-
119
- if (fezElements.length > 0) {
120
- // Compile each found fez element
121
- fezElements.forEach(el => {
122
- const name = el.getAttribute('fez')
123
- if (name && !name.includes('-') && !name.includes('.') && !name.includes('/')) {
124
- console.error(`Fez: Invalid custom element name "${name}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
125
- }
126
- const content = el.innerHTML
127
- Fez.compile(name, content)
128
- })
129
- } else {
130
- // No fez elements found, use extracted name from URL
131
- const name = url.split('/').pop().split('.')[0]
132
- Fez.compile(name, htmlContent)
133
- }
134
- })
135
- .catch(error => {
136
- console.error(`FEZ template load error for "${fezName}": ${error.message}`)
137
- })
101
+ compile_from_url(fezName)
138
102
  return
139
103
  } else {
140
104
  // Validate fezName format for non-URL names
141
105
  if (fezName && !fezName.includes('-')) {
142
106
  console.error(`Fez: Invalid custom element name "${fezName}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
143
107
  }
144
- html = node.innerHTML
145
- tagName = fezName
108
+ // Compile the node directly
109
+ return compile(fezName, node.innerHTML)
146
110
  }
147
111
  }
148
- else if (typeof html != 'string') {
149
- document.body.querySelectorAll('template[fez], xmp[fez]').forEach((n) => Fez.compile(n))
112
+ else {
113
+ let root = data ? Fez.domRoot(data) : document.body
114
+
115
+ root.querySelectorAll('template[fez], xmp[fez]').forEach((n) => {
116
+ compile_bulk(n)
117
+ })
118
+
150
119
  return
151
120
  }
121
+ }
122
+
123
+ function compile_from_url(url) {
124
+ Fez.log(`Loading from ${url}`)
125
+
126
+ // Load HTML content via AJAX from URL
127
+ Fez.fetch(url)
128
+ .then(htmlContent => {
129
+ // Check if remote HTML has template/xmp tags with fez attribute
130
+ const parser = new DOMParser()
131
+ const doc = parser.parseFromString(htmlContent, 'text/html')
132
+ const fezElements = doc.querySelectorAll('template[fez], xmp[fez]')
133
+
134
+ if (fezElements.length > 0) {
135
+ // Compile each found fez element
136
+ fezElements.forEach(el => {
137
+ const name = el.getAttribute('fez')
138
+ if (name && !name.includes('-') && !name.includes('.') && !name.includes('/')) {
139
+ console.error(`Fez: Invalid custom element name "${name}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
140
+ }
141
+ const content = el.innerHTML
142
+ compile(name, content)
143
+ })
144
+ } else {
145
+ // No fez elements found, use extracted name from URL
146
+ const name = url.split('/').pop().split('.')[0]
147
+ compile(name, htmlContent)
148
+ }
149
+ })
150
+ .catch(error => {
151
+ console.error(`FEZ template load error for "${url}": ${error.message}`)
152
+ })
153
+ }
154
+
155
+ // <template fez="ui-form">
156
+ // <script>
157
+ // ...
158
+ // Fez.compile() # compile all
159
+ // Fez.compile(templateNode) # compile template node or string with template or xmp tags
160
+ // Fez.compile('ui-form', templateNode.innerHTML) # compile string
161
+ function compile(tagName, html) {
162
+ // Handle single argument cases
163
+ if (arguments.length === 1) {
164
+ return compile_bulk(tagName)
165
+ }
166
+
167
+ // If html contains </xmp>, send to compile_bulk for processing
168
+ if (html && html.includes('</xmp>')) {
169
+ return compile_bulk(html)
170
+ }
152
171
 
153
172
  // Validate element name if it's not a URL
154
173
  if (tagName && !tagName.includes('-') && !tagName.includes('.') && !tagName.includes('/')) {
@@ -168,7 +187,8 @@ export default function (tagName, html) {
168
187
  styleContainer.id = 'fez-hidden-styles'
169
188
  document.head.appendChild(styleContainer)
170
189
  }
171
- styleContainer.textContent += `${tagName} { display: none; }\n`
190
+ const allTags = [...Object.keys(Fez.classes), tagName].sort().join(', ')
191
+ styleContainer.textContent = `${allTags} { display: none; }\n`
172
192
  }
173
193
 
174
194
  // we cant try/catch javascript modules (they use imports)
@@ -190,3 +210,6 @@ export default function (tagName, html) {
190
210
  }
191
211
  }
192
212
  }
213
+
214
+ export { compile_from_url }
215
+ export default compile
@@ -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.slotNodeName || 'div'
62
- return `<${name} class="fez-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,9 +184,18 @@ 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()
195
+ fez.firstRender = true
196
+
197
+ // Trigger mount lifecycle hook
198
+ fez.onMount(fez.props)
166
199
 
167
200
  if (fez.onSubmit) {
168
201
  const form = fez.root.nodeName == 'FORM' ? fez.root : fez.find('form')
@@ -172,11 +205,11 @@ function connectNode(name, node) {
172
205
  }
173
206
  }
174
207
 
175
- fez.onMount(fez.props)
176
-
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') {
@@ -0,0 +1,64 @@
1
+ // Wrap defaults in a function to avoid immediate execution
2
+ const loadDefaults = () => {
3
+ // include fez component by name
4
+ //<fez-component name="some-node" :props="fez.props"></fez-component>
5
+ Fez('fez-component', class {
6
+ init(props) {
7
+ const tag = document.createElement(props.name)
8
+ tag.props = props.props || props['data-props'] || props
9
+
10
+ while (this.root.firstChild) {
11
+ this.root.parentNode.insertBefore(this.root.lastChild, tag.nextSibling);
12
+ }
13
+
14
+ this.root.innerHTML = ''
15
+ this.root.appendChild(tag)
16
+ }
17
+ })
18
+
19
+ // include remote data from url
20
+ // <fez-include src="./demo/fez/ui-slider.html"></fez-include>
21
+ Fez('fez-include', class {
22
+ init(props) {
23
+ Fez.fetch(props.src, (data)=>{
24
+ const dom = Fez.domRoot(data)
25
+ Fez.head(dom) // include scripts and load fez components
26
+ this.root.innerHTML = dom.innerHTML
27
+ })
28
+ }
29
+ })
30
+
31
+ // include remote data from url
32
+ // <fez-inline :state="{count: 0}">
33
+ // <button onclick="fez.state.count += 1">&plus;</button>
34
+ // {{ state.count }} * {{ state.count }} = {{ state.count * state.count }}
35
+ // </fez-inline>
36
+ Fez('fez-inline', class {
37
+ init(props) {
38
+ const html = this.root.innerHTML
39
+
40
+ if (this.root.innerHTML.includes('<')) {
41
+ const hash = Fez.fnv1(this.root.outerHTML)
42
+ const nodeName = `inline-${hash}`
43
+ Fez(nodeName, class {
44
+ HTML = html
45
+ init() {
46
+ Object.assign(this.state, props.state || {})
47
+ }
48
+ })
49
+
50
+ const el = document.createElement(nodeName)
51
+ this.root.after(this.root.lastChild, el);
52
+ this.root.remove()
53
+ }
54
+ }
55
+ })
56
+ }
57
+
58
+ // Only load defaults if Fez is available
59
+ if (typeof Fez !== 'undefined' && Fez) {
60
+ loadDefaults()
61
+ }
62
+
63
+ // Export for use in tests
64
+ export { loadDefaults }