@dinoreic/fez 0.4.0 → 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,211 +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
158
+ // Handle CSS (can be string or function)
159
+ if (instance.CSS) {
160
+ newKlass.css =
161
+ typeof instance.CSS === "function" ? instance.CSS() : instance.CSS;
162
+ }
93
163
 
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
- })
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);
169
+ }
170
+
171
+ // Handle META (generic metadata object)
172
+ if (instance.META) {
173
+ newKlass.META = instance.META;
174
+ Fez.index.ensure(name).meta = instance.META;
108
175
  }
109
- }
110
176
 
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 == 'false') {
115
- return false
116
- } else {
117
- return fezFast || isFast
177
+ // Auto-mount global components
178
+ if (instance.GLOBAL) {
179
+ Fez.onReady(() => document.body.appendChild(document.createElement(name)));
118
180
  }
181
+
182
+ Fez.consoleLog(`${name} compiled`);
183
+ return newKlass;
119
184
  }
120
185
 
121
186
  /**
122
- * Converts self-closing custom tags to full open/close format
123
- * 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
124
190
  */
125
191
  function closeCustomTags(html) {
126
- const selfClosingTags = new Set([
127
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr'
128
- ])
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;
129
208
 
130
- return html.replace(/<([a-z-]+)\b([^>]*)\/>/g, (match, tagName, attributes) => {
131
- return selfClosingTags.has(tagName) ? match : `<${tagName}${attributes}></${tagName}>`
132
- })
209
+ const klassFast =
210
+ typeof klass.FAST === "function" ? klass.FAST(node) : klass.FAST;
211
+ return !!(attr || klassFast || node.childNodes[0] || node.nextSibling);
133
212
  }
134
213
 
214
+ // =============================================================================
215
+ // NODE CONNECTION (Instantiation)
216
+ // =============================================================================
217
+
135
218
  /**
136
- * Initializes a Fez component instance from a DOM node
137
- * Replaces the custom element with the component's rendered content
219
+ * Initialize component instance from DOM node
138
220
  */
139
221
  function connectNode(name, node) {
140
- const klass = Fez.classes[name]
141
- const parentNode = node.parentNode
222
+ if (!node.isConnected) return;
223
+ if (node.classList?.contains("fez")) return;
142
224
 
143
- if (node.isConnected) {
144
- const nodeName = typeof klass.nodeName == 'function' ? klass.nodeName(node) : klass.nodeName
145
- 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");
146
231
 
147
- newNode.classList.add('fez')
148
- newNode.classList.add(`fez-${name}`)
232
+ newNode.classList.add("fez", `fez-${name}`);
149
233
 
150
- parentNode.replaceChild(newNode, node);
234
+ if (!node.parentNode) {
235
+ console.warn(`Fez: ${name} has no parent, skipping`);
236
+ return;
237
+ }
151
238
 
152
- const fez = new klass()
239
+ // Replace custom element with component node
240
+ node.parentNode.replaceChild(newNode, node);
153
241
 
154
- fez.UID = ++Fez.instanceCount
155
- 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);
156
246
 
157
- fez.oldRoot = node
158
- fez.fezName = name
159
- fez.root = newNode
160
- fez.props = klass.getProps(node, newNode)
161
- 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;
162
252
 
163
- // Move child nodes to preserve DOM event listeners
164
- fez.slot(node, newNode)
253
+ // Move children (slot content)
254
+ fez.fezSlot(node, newNode);
165
255
 
166
- newNode.fez = fez
256
+ newNode.fez = fez;
167
257
 
168
- if (klass.GLOBAL && klass.GLOBAL != true) {
169
- window[klass.GLOBAL] = fez
170
- }
258
+ // Global component reference
259
+ if (klass.GLOBAL && klass.GLOBAL !== true) {
260
+ window[klass.GLOBAL] ||= fez;
261
+ }
171
262
 
172
- if (window.$) {
173
- fez.$root = $(newNode)
174
- }
263
+ // jQuery compatibility
264
+ if (window.$) fez.$root = $(newNode);
175
265
 
176
- if (fez.props.id) {
177
- newNode.setAttribute('id', fez.props.id)
178
- }
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;
179
286
 
180
- // Component lifecycle initialization
181
- 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);
182
290
 
183
- // Call initialization method (init, created, or connect)
184
- ;(fez.init || fez.created || fez.connect).bind(fez)(fez.props)
291
+ // Render
292
+ fez.fezRender();
185
293
 
186
- // Initial render
187
- fez.render()
188
- fez.firstRender = true
294
+ // Done initializing - state changes in onMount will now trigger renders
295
+ fez._isInitializing = false;
189
296
 
190
- // Trigger mount lifecycle hook
191
- fez.onMount(fez.props)
297
+ // Mount
298
+ fez.onMount(fez.props);
192
299
 
193
- if (fez.onSubmit) {
194
- 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) {
195
304
  form.onsubmit = (e) => {
196
- e.preventDefault()
197
- fez.onSubmit(fez.formData())
198
- }
305
+ e.preventDefault();
306
+ fez.onSubmit(fez.formData());
307
+ };
199
308
  }
309
+ }
200
310
 
201
- // Set up reactive attribute watching
202
- if (fez.onPropsChange) {
203
- observer.observe(newNode, {attributes:true})
204
-
205
- // Trigger initial prop change callbacks
206
- for (const [key, value] of Object.entries(fez.props)) {
207
- fez.onPropsChange(key, value)
208
- }
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);
209
316
  }
210
317
  }
211
318
  }