@dinoreic/fez 0.4.1 → 0.5.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.
@@ -1,215 +1,318 @@
1
- import createTemplate from './lib/template.js'
2
- import FezBase from './instance.js'
3
-
4
1
  /**
5
- * Global mutation observer for reactive attribute changes
6
- * Watches for attribute changes and triggers component updates
2
+ * Fez Component Registration & Connection
3
+ *
4
+ * This file handles:
5
+ * - Registering components with customElements
6
+ * - Transforming plain classes to FezBase subclasses
7
+ * - Instantiating components when they appear in DOM
8
+ *
9
+ * Flow:
10
+ * 1. connect(name, class) - registers custom element
11
+ * 2. connectedCallback() - when element appears in DOM
12
+ * 3. connectNode() - creates instance, renders, calls lifecycle
7
13
  */
8
- const observer = new MutationObserver((mutationsList, _) => {
9
- for (const mutation of mutationsList) {
10
- if (mutation.type === 'attributes') {
11
- const fez = mutation.target.fez
12
- const name = mutation.attributeName
13
- const value = mutation.target.getAttribute(name)
14
14
 
15
+ import createTemplate from "./lib/template.js";
16
+ import FezBase from "./instance.js";
17
+
18
+ // =============================================================================
19
+ // CONSTANTS
20
+ // =============================================================================
21
+
22
+ const SELF_CLOSING_TAGS = new Set([
23
+ "area",
24
+ "base",
25
+ "br",
26
+ "col",
27
+ "embed",
28
+ "hr",
29
+ "img",
30
+ "input",
31
+ "link",
32
+ "meta",
33
+ "source",
34
+ "track",
35
+ "wbr",
36
+ ]);
37
+
38
+ // Attribute observer for reactive props
39
+ const attrObserver = new MutationObserver((mutations) => {
40
+ for (const mutation of mutations) {
41
+ if (mutation.type === "attributes") {
42
+ const fez = mutation.target.fez;
15
43
  if (fez) {
16
- fez.props[name] = value
17
- fez.onPropsChange(name, value)
18
- // console.log(`The [${name}] attribute was modified to [${value}].`);
44
+ const name = mutation.attributeName;
45
+ const value = mutation.target.getAttribute(name);
46
+ fez.props[name] = value;
47
+ fez.onPropsChange(name, value);
19
48
  }
20
49
  }
21
50
  }
22
51
  });
23
52
 
53
+ // =============================================================================
54
+ // MAIN CONNECT FUNCTION
55
+ // =============================================================================
56
+
24
57
  /**
25
- * Registers a new custom element with Fez framework
26
- * @param {string} name - Custom element name (must contain a dash)
27
- * @param {Class|Object} klass - Component class or configuration object
58
+ * Register a Fez component
59
+ *
60
+ * @param {string} name - Custom element name (must contain dash)
61
+ * @param {Class} klass - Component class
62
+ *
28
63
  * @example
29
- * Fez('my-component', class {
30
- * HTML = '<div>Hello World</div>'
31
- * CSS = '.my-component { color: blue; }'
64
+ * Fez('ui-button', class {
65
+ * HTML = '<button><slot /></button>'
66
+ * CSS = 'button { color: blue; }'
67
+ * init() { console.log('created') }
32
68
  * })
33
69
  */
34
70
  export default function connect(name, klass) {
35
71
  const Fez = globalThis.window?.Fez || globalThis.Fez;
36
- // Validate custom element name format (must contain a dash)
37
- if (!name.includes('-')) {
38
- console.error(`Fez: Invalid custom element name "${name}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
72
+
73
+ // Validate name
74
+ if (!name.includes("-")) {
75
+ console.error(`Fez: Invalid name "${name}". Must contain a dash.`);
76
+ return;
39
77
  }
40
78
 
41
- // Transform simple class definitions into Fez components
42
- if (!klass.fezHtmlRoot) {
43
- const klassObj = new klass()
44
- const newKlass = class extends FezBase {}
79
+ // Transform to FezBase subclass
80
+ klass = ensureFezBase(Fez, name, klass);
81
+
82
+ // Process HTML template
83
+ if (klass.html) {
84
+ klass.html = klass.html
85
+ .replace(
86
+ /<slot(\s[^>]*)?>/,
87
+ `<div class="fez-slot" fez-keep="default-slot"$1>`,
88
+ )
89
+ .replace("</slot>", `</div>`);
45
90
 
46
- // Copy all properties and methods from the original class
47
- const props = Object.getOwnPropertyNames(klassObj)
48
- .concat(Object.getOwnPropertyNames(klass.prototype))
49
- .filter(el => !['constructor', 'prototype'].includes(el))
91
+ klass.fezHtmlFunc = createTemplate(klass.html, { name });
92
+ }
50
93
 
51
- props.forEach(prop => newKlass.prototype[prop] = klassObj[prop])
94
+ // Register CSS
95
+ if (klass.css) {
96
+ klass.css = Fez.globalCss(klass.css, { name });
97
+ }
52
98
 
53
- // Map component configuration properties
54
- if (klassObj.FAST) { newKlass.FAST = klassObj.FAST } // Global instance reference
55
- if (klassObj.GLOBAL) { newKlass.GLOBAL = klassObj.GLOBAL } // Global instance reference
56
- if (klassObj.CSS) { newKlass.css = klassObj.CSS } // Component styles
57
- if (klassObj.HTML) {
58
- newKlass.html = closeCustomTags(klassObj.HTML) // Component template
59
- }
60
- if (klassObj.NAME) { newKlass.nodeName = klassObj.NAME } // Custom DOM node name
99
+ // Store class in index
100
+ Fez.index.ensure(name).class = klass;
61
101
 
62
- // Auto-mount global components to body
63
- if (klassObj.GLOBAL) {
64
- document.body.appendChild(document.createElement(name))
65
- }
102
+ // Register custom element
103
+ if (!customElements.get(name)) {
104
+ customElements.define(
105
+ name,
106
+ class extends HTMLElement {
107
+ connectedCallback() {
108
+ if (shouldRenderFast(this, klass)) {
109
+ connectNode(name, this);
110
+ } else {
111
+ requestAnimationFrame(() => connectNode(name, this));
112
+ }
113
+ }
114
+ },
115
+ );
116
+ }
117
+ }
66
118
 
67
- klass = newKlass
119
+ // =============================================================================
120
+ // CLASS TRANSFORMATION
121
+ // =============================================================================
68
122
 
69
- Fez.log(`${name} compiled`)
70
- } else if (klass.html) {
71
- // If klass already has html property, process it
72
- klass.html = closeCustomTags(klass.html)
123
+ /**
124
+ * Transform plain class to FezBase subclass
125
+ * Maps uppercase config props (HTML, CSS, etc.)
126
+ */
127
+ function ensureFezBase(Fez, name, klass) {
128
+ // Already a FezBase subclass
129
+ if (klass.prototype instanceof FezBase) {
130
+ if (klass.html) klass.html = closeCustomTags(klass.html);
131
+ return klass;
73
132
  }
74
133
 
75
- // Process component template
76
- if (klass.html) {
77
- let slotTag = klass.SLOT || 'div'
134
+ // Create FezBase subclass
135
+ const instance = new klass();
136
+ const newKlass = class extends FezBase {};
78
137
 
79
- klass.html = klass.html
80
- .replace('<slot', `<${slotTag} class="fez-slot" fez-keep="default-slot"`)
81
- .replace('</slot>', `</${slotTag}>`)
138
+ // Copy properties and methods
139
+ const props = [
140
+ ...Object.getOwnPropertyNames(instance),
141
+ ...Object.getOwnPropertyNames(klass.prototype),
142
+ ].filter((p) => p !== "constructor" && p !== "prototype");
82
143
 
83
- // Compile template function
84
- klass.fezHtmlFunc = createTemplate(klass.html)
144
+ for (const prop of props) {
145
+ newKlass.prototype[prop] = instance[prop];
85
146
  }
86
147
 
87
- // Register component styles globally (available to all components)
88
- if (klass.css) {
89
- klass.css = Fez.globalCss(klass.css, {name: name})
148
+ // Map config properties
149
+ const configMap = {
150
+ FAST: "FAST",
151
+ GLOBAL: "GLOBAL",
152
+ NAME: "nodeName",
153
+ };
154
+ for (const [from, to] of Object.entries(configMap)) {
155
+ if (instance[from]) newKlass[to] = instance[from];
90
156
  }
91
157
 
92
- Fez.classes[name] = klass
93
-
94
- if (!customElements.get(name)) {
95
- customElements.define(name, class extends HTMLElement {
96
- connectedCallback() {
97
- // Fez.onReady(()=>{connectNode(name, this)})
98
- // connectNode(name, this)
99
- if (useFastRender(this, klass)) {
100
- connectNode(name, this)
101
- } else {
102
- requestAnimationFrame(()=>{
103
- connectNode(name, this)
104
- })
105
- }
106
- }
107
- })
158
+ // Handle CSS (can be string or function)
159
+ if (instance.CSS) {
160
+ newKlass.css =
161
+ typeof instance.CSS === "function" ? instance.CSS() : instance.CSS;
108
162
  }
109
- }
110
163
 
111
- function useFastRender(node, klass) {
112
- const fezFast = node.getAttribute('fez-fast')
113
- var isFast = typeof klass.FAST === 'function' ? klass.FAST(node) : klass.FAST
114
- if (fezFast || isFast || node.childNodes[0] || node.nextSibling) {
115
- return true
164
+ // Handle HTML (can be string or function)
165
+ if (instance.HTML) {
166
+ const html =
167
+ typeof instance.HTML === "function" ? instance.HTML() : instance.HTML;
168
+ newKlass.html = closeCustomTags(html);
116
169
  }
117
- else if (fezFast == 'false') {
118
- return false
170
+
171
+ // Handle META (generic metadata object)
172
+ if (instance.META) {
173
+ newKlass.META = instance.META;
174
+ Fez.index.ensure(name).meta = instance.META;
119
175
  }
120
- else {
121
- return false
176
+
177
+ // Auto-mount global components
178
+ if (instance.GLOBAL) {
179
+ Fez.onReady(() => document.body.appendChild(document.createElement(name)));
122
180
  }
181
+
182
+ Fez.consoleLog(`${name} compiled`);
183
+ return newKlass;
123
184
  }
124
185
 
125
186
  /**
126
- * Converts self-closing custom tags to full open/close format
127
- * Required for proper HTML parsing of custom elements
187
+ * Convert self-closing custom tags to full format
188
+ * <my-comp /> -> <my-comp></my-comp>
189
+ * Uses (?:[^>]|=>) to skip => (arrow functions) inside attributes
128
190
  */
129
191
  function closeCustomTags(html) {
130
- const selfClosingTags = new Set([
131
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr'
132
- ])
192
+ return html.replace(
193
+ /<([a-z][a-z-]*)\b((?:=>|[^>])*)>/g,
194
+ (match, tag, attrs) => {
195
+ if (!attrs.trimEnd().endsWith("/")) return match;
196
+ if (SELF_CLOSING_TAGS.has(tag)) return match;
197
+ return `<${tag}${attrs.replace(/\s*\/$/, "")}></${tag}>`;
198
+ },
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Determine if component should render synchronously
204
+ */
205
+ function shouldRenderFast(node, klass) {
206
+ const attr = node.getAttribute("fez-fast");
207
+ if (attr === "false") return false;
133
208
 
134
- return html.replace(/<([a-z-]+)\b([^>]*)\/>/g, (match, tagName, attributes) => {
135
- return selfClosingTags.has(tagName) ? match : `<${tagName}${attributes}></${tagName}>`
136
- })
209
+ const klassFast =
210
+ typeof klass.FAST === "function" ? klass.FAST(node) : klass.FAST;
211
+ return !!(attr || klassFast || node.childNodes[0] || node.nextSibling);
137
212
  }
138
213
 
214
+ // =============================================================================
215
+ // NODE CONNECTION (Instantiation)
216
+ // =============================================================================
217
+
139
218
  /**
140
- * Initializes a Fez component instance from a DOM node
141
- * Replaces the custom element with the component's rendered content
219
+ * Initialize component instance from DOM node
142
220
  */
143
221
  function connectNode(name, node) {
144
- const klass = Fez.classes[name]
145
- const parentNode = node.parentNode
222
+ if (!node.isConnected) return;
223
+ if (node.classList?.contains("fez")) return;
146
224
 
147
- if (node.isConnected) {
148
- const nodeName = typeof klass.nodeName == 'function' ? klass.nodeName(node) : klass.nodeName
149
- const newNode = document.createElement(nodeName || 'div')
225
+ const klass = Fez.index[name]?.class;
226
+ const nodeName =
227
+ typeof klass.nodeName === "function"
228
+ ? klass.nodeName(node)
229
+ : klass.nodeName;
230
+ const newNode = document.createElement(nodeName || "div");
150
231
 
151
- newNode.classList.add('fez')
152
- newNode.classList.add(`fez-${name}`)
232
+ newNode.classList.add("fez", `fez-${name}`);
153
233
 
154
- parentNode.replaceChild(newNode, node);
234
+ if (!node.parentNode) {
235
+ console.warn(`Fez: ${name} has no parent, skipping`);
236
+ return;
237
+ }
155
238
 
156
- const fez = new klass()
239
+ // Replace custom element with component node
240
+ node.parentNode.replaceChild(newNode, node);
157
241
 
158
- fez.UID = ++Fez.instanceCount
159
- Fez.instances.set(fez.UID, fez)
242
+ // Create instance
243
+ const fez = new klass();
244
+ fez.UID = ++Fez.instanceCount;
245
+ Fez.instances.set(fez.UID, fez);
160
246
 
161
- fez.oldRoot = node
162
- fez.fezName = name
163
- fez.root = newNode
164
- fez.props = klass.getProps(node, newNode)
165
- fez.class = klass
247
+ fez.oldRoot = node;
248
+ fez.fezName = name;
249
+ fez.root = newNode;
250
+ fez.props = klass.getProps(node, newNode);
251
+ fez.class = klass;
166
252
 
167
- // Move child nodes to preserve DOM event listeners
168
- fez.slot(node, newNode)
253
+ // Move children (slot content)
254
+ fez.fezSlot(node, newNode);
169
255
 
170
- newNode.fez = fez
256
+ newNode.fez = fez;
171
257
 
172
- if (klass.GLOBAL && klass.GLOBAL != true) {
173
- window[klass.GLOBAL] = fez
174
- }
258
+ // Global component reference
259
+ if (klass.GLOBAL && klass.GLOBAL !== true) {
260
+ window[klass.GLOBAL] ||= fez;
261
+ }
175
262
 
176
- if (window.$) {
177
- fez.$root = $(newNode)
178
- }
263
+ // jQuery compatibility
264
+ if (window.$) fez.$root = $(newNode);
179
265
 
180
- if (fez.props.id) {
181
- newNode.setAttribute('id', fez.props.id)
182
- }
266
+ // Copy ID
267
+ if (fez.props.id) newNode.setAttribute("id", fez.props.id);
268
+
269
+ // Copy fez-keep for DOM differ preservation
270
+ const fezKeep = node.getAttribute("fez-keep");
271
+ if (fezKeep) newNode.setAttribute("fez-keep", fezKeep);
272
+
273
+ // === LIFECYCLE ===
274
+
275
+ // Setup reactive state
276
+ fez.fezRegister();
277
+
278
+ // Capture children before rendering replaces them
279
+ if (fez.root.childNodes.length) {
280
+ fez._fezSlotNodes = Array.from(fez.root.childNodes);
281
+ fez._fezChildNodes = fez._fezSlotNodes.filter((n) => n.nodeType === 1);
282
+ }
283
+
284
+ // Prevent state changes during init/mount from scheduling extra renders
285
+ fez._isInitializing = true;
183
286
 
184
- // Component lifecycle initialization
185
- fez.fezRegister()
287
+ // Init (supports multiple naming conventions)
288
+ const initMethod = fez.onInit || fez.init || fez.created || fez.connect;
289
+ initMethod.call(fez, fez.props);
186
290
 
187
- // Call initialization method (init, created, or connect)
188
- ;(fez.init || fez.created || fez.connect).bind(fez)(fez.props)
291
+ // Render
292
+ fez.fezRender();
189
293
 
190
- // Initial render
191
- fez.render()
192
- fez.firstRender = true
294
+ // Done initializing - state changes in onMount will now trigger renders
295
+ fez._isInitializing = false;
193
296
 
194
- // Trigger mount lifecycle hook
195
- fez.onMount(fez.props)
297
+ // Mount
298
+ fez.onMount(fez.props);
196
299
 
197
- if (fez.onSubmit) {
198
- const form = fez.root.nodeName == 'FORM' ? fez.root : fez.find('form')
300
+ // Form submit handling
301
+ if (fez.onSubmit) {
302
+ const form = fez.root.nodeName === "FORM" ? fez.root : fez.find("form");
303
+ if (form) {
199
304
  form.onsubmit = (e) => {
200
- e.preventDefault()
201
- fez.onSubmit(fez.formData())
202
- }
305
+ e.preventDefault();
306
+ fez.onSubmit(fez.formData());
307
+ };
203
308
  }
309
+ }
204
310
 
205
- // Set up reactive attribute watching
206
- if (fez.onPropsChange) {
207
- observer.observe(newNode, {attributes:true})
208
-
209
- // Trigger initial prop change callbacks
210
- for (const [key, value] of Object.entries(fez.props)) {
211
- fez.onPropsChange(key, value)
212
- }
311
+ // Watch for attribute changes
312
+ if (fez.onPropsChange) {
313
+ attrObserver.observe(newNode, { attributes: true });
314
+ for (const [key, value] of Object.entries(fez.props)) {
315
+ fez.onPropsChange(key, value);
213
316
  }
214
317
  }
215
318
  }