@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/README.md +56 -17
- package/dist/fez.js +20 -17
- package/dist/fez.js.map +4 -4
- package/package.json +13 -13
- package/src/fez/connect.js +96 -61
- package/src/fez/defaults.js +36 -6
- package/src/fez/instance.js +119 -104
- package/src/fez/root.js +21 -152
- package/src/fez/utility.js +197 -0
- package/src/{log.js → fez/utils/dump.js} +99 -42
- package/src/fez.js +2 -2
- package/src/rollup.js +73 -16
- package/dist/log.js +0 -5
- package/dist/log.js.map +0 -7
- package/dist/rollup.js +0 -3
- package/dist/rollup.js.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dinoreic/fez",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
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
|
}
|
package/src/fez/connect.js
CHANGED
|
@@ -1,94 +1,122 @@
|
|
|
1
|
-
// templating
|
|
2
1
|
import createTemplate from './lib/template.js'
|
|
3
2
|
import FezBase from './instance.js'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
if (klassObj.
|
|
29
|
-
if (klassObj.
|
|
30
|
-
|
|
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
|
|
43
|
+
const mountGlobalComponent = () => document.body.appendChild(document.createElement(name))
|
|
41
44
|
|
|
42
45
|
if (document.readyState === 'loading') {
|
|
43
|
-
document.addEventListener('DOMContentLoaded',
|
|
46
|
+
document.addEventListener('DOMContentLoaded', mountGlobalComponent);
|
|
44
47
|
} else {
|
|
45
|
-
|
|
48
|
+
mountGlobalComponent()
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
klass = newKlass
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
return `<${
|
|
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
|
-
//
|
|
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
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
//
|
|
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') {
|
package/src/fez/defaults.js
CHANGED
|
@@ -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 }
|