@twobirds/microcomponents 0.9.0 → 0.10.1
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 +2 -16
- package/dist/cli/bundle.d.ts +106 -0
- package/dist/cli/bundle.d.ts.map +1 -0
- package/dist/cli/bundler.d.ts +44 -0
- package/dist/cli/bundler.d.ts.map +1 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/dev.d.ts +87 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dispatch.d.ts +6 -2
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/index.js +1807 -1
- package/dist/cli/inliner.d.ts +42 -0
- package/dist/cli/inliner.d.ts.map +1 -0
- package/dist/cli/preview.d.ts +48 -0
- package/dist/cli/preview.d.ts.map +1 -0
- package/dist/core/elements.js +1294 -75
- package/dist/core/mcGuards.js +5 -8
- package/dist/mcp/api.json +2 -17
- package/dist/mcp/concepts/project-layout.md +9 -8
- package/dist/mcp/index.js +450 -230
- package/dist/microcomponents.js +2882 -9
- package/dist/state/formValues.js +2236 -281
- package/dist/state/observables.js +1794 -342
- package/dist/utils/events.js +171 -197
- package/package.json +7 -8
- package/dist/cli/create.js +0 -653
- package/dist/cli/dispatch.js +0 -316
- package/dist/cli/scaffold.js +0 -109
- package/dist/cli/writeScaffold.js +0 -114
- package/dist/core/MC.js +0 -837
- package/dist/core/index.js +0 -72
- package/dist/state/index.js +0 -5
- package/dist/state/placeholders.js +0 -187
- package/dist/types/index.js +0 -1
- package/dist/types/mcTypes.js +0 -9
- package/dist/utils/autoload.js +0 -160
- package/dist/utils/helpers.js +0 -532
- package/dist/utils/index.js +0 -12
- package/dist/utils/resolveComponentPath.js +0 -39
- package/dist/vite/index.d.ts +0 -7
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/index.js +0 -6
- package/dist/vite/loadAssets.d.ts +0 -36
- package/dist/vite/loadAssets.d.ts.map +0 -1
- package/dist/vite/loadAssets.js +0 -221
package/dist/core/elements.js
CHANGED
|
@@ -1,11 +1,1234 @@
|
|
|
1
|
+
//#region src/utils/events.ts
|
|
2
|
+
/**
|
|
3
|
+
@module Events
|
|
4
|
+
|
|
5
|
+
This module provides event utilities for interfacing **between microcomponents and external
|
|
6
|
+
frameworks or native DOM/JS environments**. It is not intended for communication between
|
|
7
|
+
frontend microcomponents themselves — for that purpose, use the inherited methods
|
|
8
|
+
( `.on()`, `.one()`, `.off()`, `.trigger()`) that exist when you extend `DC`.
|
|
9
|
+
|
|
10
|
+
Import this module when you need to exchange information with microcomponents from outside
|
|
11
|
+
the microcomponent system (e.g., vanilla JS, React via `useRef()`, or other frameworks).
|
|
12
|
+
|
|
13
|
+
The core feature is the ability to explicitly control event flow direction when using {@link trigger}:
|
|
14
|
+
- upward propagation to ancestor components ('u')
|
|
15
|
+
- local dispatch only ('l') [default]
|
|
16
|
+
- downward propagation to descendant components ('d').
|
|
17
|
+
|
|
18
|
+
@example
|
|
19
|
+
html:
|
|
20
|
+
```html
|
|
21
|
+
<div class="grandparent">
|
|
22
|
+
<div class="parent">
|
|
23
|
+
<div class="child">
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
javascript:
|
|
30
|
+
```javascript
|
|
31
|
+
import { on, one, trigger, off } from '@twobirds/microcomponents/events';
|
|
32
|
+
|
|
33
|
+
const grandParent = document.querySelector('.grandparent');
|
|
34
|
+
const parent = document.querySelector('.parent');
|
|
35
|
+
const child = document.querySelector('.child');
|
|
36
|
+
if (!grandParent || !parent || !child) throw new Error('example markup missing');
|
|
37
|
+
|
|
38
|
+
const grandParentHandler = on(grandParent, 'dataReady', (ev) => console.log(ev.detail.data));
|
|
39
|
+
|
|
40
|
+
const parentHandler = on(parent, 'dataReady', (ev) => ev.detail.control.stop());
|
|
41
|
+
|
|
42
|
+
trigger(child, 'dataReady', { value: 42 }, 'u');
|
|
43
|
+
|
|
44
|
+
off(parent, 'dataReady', parentHandler);
|
|
45
|
+
off(grandParent, 'dataReady', grandParentHandler);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `dataReady` event bubbles upward from the innermost `div`, but `parentHandler`
|
|
49
|
+
calls `ev.detail.control.stop()`, so the `grandParent` handler is never reached.
|
|
50
|
+
Handlers are detached with `off()` once they are no longer needed.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Returns the correct event name to listen for on a DOM element.
|
|
54
|
+
*
|
|
55
|
+
* If `eventName` (case-insensitive) matches a native HTML event handler property
|
|
56
|
+
* (`'on' + eventName.toLowerCase()`) on the target, returns the lowercase native
|
|
57
|
+
* name so that standard DOM events like `'click'` are listened to correctly.
|
|
58
|
+
* Otherwise, prefixes with `_mc` for microcomponent custom events.
|
|
59
|
+
*
|
|
60
|
+
* @hidden
|
|
61
|
+
* @param target - The DOM element whose native event support is checked.
|
|
62
|
+
* @param eventName - The event name as provided by the caller (e.g., `'click'`, `'Update'`).
|
|
63
|
+
* @returns The resolved event name: lowercase native or `_mc`-prefixed custom.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const btn = document.createElement('button');
|
|
68
|
+
* console.log(trueEventName(btn, 'Click'));
|
|
69
|
+
* console.log(trueEventName(btn, 'Update'));
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* `'Click'` resolves to the native `'click'` because the button has an `onclick` handler
|
|
73
|
+
* property; `'Update'` has no native counterpart, so it becomes the `_mc`-prefixed custom
|
|
74
|
+
* event name.
|
|
75
|
+
*/
|
|
76
|
+
function trueEventName(target, eventName) {
|
|
77
|
+
const lower = eventName.toLowerCase();
|
|
78
|
+
if ("on" + lower in target) return lower;
|
|
79
|
+
return "_mc" + eventName[0].toUpperCase() + eventName.substring(1);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
Removes a previously attached event handler from the DOM node. See {@link on} and {@link one}.
|
|
83
|
+
|
|
84
|
+
Uses internal microcomponent-prefixed naming for event handlers.
|
|
85
|
+
This is primarily used to interface with external frameworks or vanilla JS environments.
|
|
86
|
+
For communication between frontend microcomponents, use the `DC.off()` method instead.
|
|
87
|
+
|
|
88
|
+
@param target - The DOM element from which to remove the listener.
|
|
89
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
90
|
+
@param handler - The listener function or object to detach.
|
|
91
|
+
|
|
92
|
+
@example
|
|
93
|
+
```ts
|
|
94
|
+
const handler = on(myElement, 'click', () => console.log('clicked'));
|
|
95
|
+
off(myElement, 'click', handler);
|
|
96
|
+
```
|
|
97
|
+
@group Events
|
|
98
|
+
*/
|
|
99
|
+
function off(target, eventName, handler) {
|
|
100
|
+
/* v8 ignore next */
|
|
101
|
+
if (!target) return;
|
|
102
|
+
if (typeof target === "string") {
|
|
103
|
+
document.querySelectorAll(target).forEach((el) => off(el, eventName, handler));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
target.removeEventListener(trueEventName(target, eventName), handler);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
Attaches a persistent event listener that will fire every time the specified event occurs.
|
|
110
|
+
|
|
111
|
+
This is primarily used to interface with external frameworks or vanilla JS environments.
|
|
112
|
+
For communication between frontend microcomponents, use the `DC.on()` method instead.
|
|
113
|
+
|
|
114
|
+
The handler receives a `CustomEvent<McDetail<D>>`: the payload sits under
|
|
115
|
+
`ev.detail.data`, the bubble-control handle under `ev.detail.control`.
|
|
116
|
+
|
|
117
|
+
@param target - The DOM element to attach the listener to.
|
|
118
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
119
|
+
@param handler - The callback function or object to invoke on each event.
|
|
120
|
+
@param once - If true, remove the listener after first invocation.
|
|
121
|
+
@returns The attached handler reference. Useful for later deletion with {@link off}.
|
|
122
|
+
|
|
123
|
+
@example
|
|
124
|
+
```ts
|
|
125
|
+
const handler = on<{ value: number }>(myElement, 'update', (ev) => {
|
|
126
|
+
console.log(ev.detail.data.value);
|
|
127
|
+
});
|
|
128
|
+
off(myElement, 'update', handler);
|
|
129
|
+
```
|
|
130
|
+
@group Events
|
|
131
|
+
*/
|
|
132
|
+
function on(target, eventName, handler, once = false) {
|
|
133
|
+
/* v8 ignore next */
|
|
134
|
+
if (!target) return;
|
|
135
|
+
if (typeof target === "string") {
|
|
136
|
+
document.querySelectorAll(target).forEach((el) => on(el, eventName, handler));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
target.addEventListener(trueEventName(target, eventName), handler, {
|
|
140
|
+
capture: false,
|
|
141
|
+
once
|
|
142
|
+
});
|
|
143
|
+
return handler;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
Attaches an event listener that will fire only once.
|
|
147
|
+
|
|
148
|
+
Internally calls {@link on} with the `once` flag set to true. The handler
|
|
149
|
+
is automatically removed after its first invocation.
|
|
150
|
+
This is primarily used to interface with external frameworks or vanilla JS environments.
|
|
151
|
+
For communication between frontend microcomponents, use the `DC.one()` method instead.
|
|
152
|
+
|
|
153
|
+
@param target - The DOM element to attach the listener to.
|
|
154
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
155
|
+
@param handler - The callback function or object to invoke once.
|
|
156
|
+
@returns The attached one-time handler reference. Useful for later deletion with {@link off}.
|
|
157
|
+
|
|
158
|
+
@example
|
|
159
|
+
```ts
|
|
160
|
+
one<{ ready: boolean }>(myElement, 'dataReady', (ev) => console.log('received:', ev.detail.data));
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The listener fires only once and detaches automatically.
|
|
164
|
+
@group Events
|
|
165
|
+
*/
|
|
166
|
+
function one(target, eventName, handler) {
|
|
167
|
+
/* v8 ignore next */
|
|
168
|
+
if (!target) return;
|
|
169
|
+
if (typeof target === "string") {
|
|
170
|
+
document.querySelectorAll(target).forEach((el) => one(el, eventName, handler));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
on(target, eventName, handler, true);
|
|
174
|
+
return handler;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* @hidden
|
|
178
|
+
* A controller object used to manage and potentially abort event bubbling.
|
|
179
|
+
*
|
|
180
|
+
* The `trigger()` function internally creates instance of this class,
|
|
181
|
+
* allowing the an eventual handler to call `ev.detail.control.stop()` to
|
|
182
|
+
* prevent further propagation in either direction (up/down).
|
|
183
|
+
*
|
|
184
|
+
* This is only used internally.
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* trigger(myElement, 'update', { value: 42 }, 'u');
|
|
188
|
+
*
|
|
189
|
+
* onUpdate(ev) {
|
|
190
|
+
* ev.detail.control.stop();
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
var BubbleController = class {
|
|
195
|
+
stopped = false;
|
|
196
|
+
constructor() {}
|
|
197
|
+
/**
|
|
198
|
+
* Signals that event bubbling should be halted.
|
|
199
|
+
*
|
|
200
|
+
* After calling this method, subsequent `trigger()` calls will abort
|
|
201
|
+
* before dispatching the event to any targets.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* trigger(myElement, 'update', { value: 42 }, 'u');
|
|
206
|
+
*
|
|
207
|
+
* onUpdate(ev) {
|
|
208
|
+
* ev.detail.control.stop();
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
stop() {
|
|
213
|
+
this.stopped = true;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Creates and dispatches a custom event on the specified DOM node.
|
|
218
|
+
*
|
|
219
|
+
* The event is namespaced with an `_mc` prefix by default, unless already prefixed.
|
|
220
|
+
* Supports bidirectional bubbling (up/down the DOM tree) based on the `bubble` parameter:
|
|
221
|
+
* - 'l': local dispatch only
|
|
222
|
+
* - 'u': bubble up towards ancestors
|
|
223
|
+
* - 'd': bubble down to descendants
|
|
224
|
+
*
|
|
225
|
+
* The dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload
|
|
226
|
+
* under `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.
|
|
227
|
+
*
|
|
228
|
+
* Prefer a direct target over bubbling: name the receiving element in `target` and keep the
|
|
229
|
+
* default `'l'`, so one `querySelectorAll` reaches it in a single step. Reserve `'u'`/`'d'`
|
|
230
|
+
* for receivers that cannot be named in advance, since they walk the tree and re-dispatch the
|
|
231
|
+
* event at every level. Pass a typed object as `data` rather than a bare string, so the
|
|
232
|
+
* handler reads named fields from `ev.detail.data` and the flow stays readable as it grows.
|
|
233
|
+
*
|
|
234
|
+
* @param target - The DOM element on which to trigger the event, or a querySelectorAll(selector) selector string.
|
|
235
|
+
* @param event - The name of the event (will be prefixed if needed).
|
|
236
|
+
* @param data - Additional data payload attached to `event.detail.data`.
|
|
237
|
+
* @param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
|
|
238
|
+
* @param controller - Optional controller for managing bubbling behavior.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* trigger(myElement, 'update', { value: 42 }, 'l');
|
|
243
|
+
* trigger(myElement, 'dataReady', { ready: true }, 'u');
|
|
244
|
+
* ```
|
|
245
|
+
*
|
|
246
|
+
* The first dispatch fires locally only; the second bubbles upward to ancestor components.
|
|
247
|
+
*/
|
|
248
|
+
function trigger(target, event, data = {}, bubble = "l", controller) {
|
|
249
|
+
/* v8 ignore next */
|
|
250
|
+
if (!target) return;
|
|
251
|
+
if (typeof target === "string") {
|
|
252
|
+
document.querySelectorAll(target).forEach((el) => trigger(el, event, data, bubble));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const eventName = /^_mc/.test(event) ? event : trueEventName(target, event), bubbleController = controller ? controller : new BubbleController(), ev = new CustomEvent(eventName, {
|
|
256
|
+
detail: {
|
|
257
|
+
data,
|
|
258
|
+
control: bubbleController
|
|
259
|
+
},
|
|
260
|
+
bubbles: false
|
|
261
|
+
});
|
|
262
|
+
if (bubbleController.stopped) return;
|
|
263
|
+
if (/l/.test(bubble)) {
|
|
264
|
+
target.dispatchEvent(ev);
|
|
265
|
+
if (bubbleController.stopped) return;
|
|
266
|
+
}
|
|
267
|
+
if (/[ud]/.test(bubble)) queueMicrotask(() => {
|
|
268
|
+
if (bubble.indexOf("u") > -1) trigger(target.parentElement, eventName, data, "lu", bubbleController);
|
|
269
|
+
if (bubble.indexOf("d") > -1) for (const child of target.children) trigger(child, eventName, data, "ld");
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/utils/helpers.ts
|
|
274
|
+
/**
|
|
275
|
+
* Converts a kebab-case string to PascalCase.
|
|
276
|
+
* Example: 'my-component-name' -> 'MyComponentName'
|
|
277
|
+
* @param str - The kebab-case string to convert.
|
|
278
|
+
* @returns The converted PascalCase string.
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts
|
|
282
|
+
* console.log(kebabToPascal('my-element'));
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
function kebabToPascal(str) {
|
|
286
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Injects a CSS string into the given scope (document or shadow root) as an adopted stylesheet.
|
|
290
|
+
* Enables css-in-js so that every custom element is self-contained and importable.
|
|
291
|
+
*
|
|
292
|
+
* @hidden
|
|
293
|
+
* @param target - The scope to inject styles into: `document` for global styles, or a `ShadowRoot` for encapsulated component internals.
|
|
294
|
+
* @param css - The CSS string to inject.
|
|
295
|
+
* @returns The created `CSSStyleSheet` instance that was adopted into the target scope.
|
|
296
|
+
*
|
|
297
|
+
* When targeting **document**: selectors like `:host`, `::slotted()`, and shadow-internal classes are **not allowed** — they have no effect outside a shadow root. Use only global selectors, element tags, or layout presets.
|
|
298
|
+
* When targeting **shadowRoot**: use `:host` for the custom element itself, standard selectors (`.inner`, `#id`) for internal structure, and `::slotted()` for projected light-DOM content.
|
|
299
|
+
*
|
|
300
|
+
* @example Inject global styles into the document:
|
|
301
|
+
* ```ts
|
|
302
|
+
* import { injectCSS } from '@twobirds/mc/utils/helpers';
|
|
303
|
+
* injectCSS(document, 'body { display: block; }');
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* @example Inject global styles for a micro component into the document:
|
|
307
|
+
* ```ts
|
|
308
|
+
* import { injectCSS } from '@twobirds/mc/utils/helpers';
|
|
309
|
+
* injectCSS(document, 'my-microcomponent { display: block; }');
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example Inject scoped styles into a shadow root:
|
|
313
|
+
* ```ts
|
|
314
|
+
* const shadow = element.attachShadow({ mode: 'open' });
|
|
315
|
+
* injectCSS(shadow, ':host { color: var(--text); } .inner { padding: 8px; }');
|
|
316
|
+
* ```
|
|
317
|
+
* Styles can also be added using a compound innerHTML statement: `shadow.innerHTML = \`<style>${style}</style>${template}\``
|
|
318
|
+
*/
|
|
319
|
+
var _injectedCSS = /* @__PURE__ */ new WeakMap();
|
|
320
|
+
function injectCSS(target, css) {
|
|
321
|
+
const injected = _injectedCSS.get(target) ?? /* @__PURE__ */ new Set();
|
|
322
|
+
if (injected.has(css)) return target.adoptedStyleSheets[0];
|
|
323
|
+
injected.add(css);
|
|
324
|
+
_injectedCSS.set(target, injected);
|
|
325
|
+
const sheet = new CSSStyleSheet();
|
|
326
|
+
sheet.replaceSync(css);
|
|
327
|
+
target.adoptedStyleSheets.push(sheet);
|
|
328
|
+
return sheet;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Processes a record of asset strings and applies them to the given target element.
|
|
332
|
+
* Supports injecting CSS into document or shadow root, and setting innerHTML for
|
|
333
|
+
* both target and shadow DOM. Automatically attaches a shadow root if any key starts
|
|
334
|
+
* with `'shadow'`.
|
|
335
|
+
*
|
|
336
|
+
* Recognized keys: `'css'`, `'targetCss'` (document-level styles), `'shadowCss'`
|
|
337
|
+
* (shadow-root styles), `'shadowHtml'` (shadow innerHTML), `'targetHtml'` (target innerHTML).
|
|
338
|
+
*
|
|
339
|
+
* @param instance - The micro component to apply assets onto.
|
|
340
|
+
* @param assets - A record mapping asset type keys to their string content.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* processAssets(this, {
|
|
345
|
+
* css: 'body { margin: 0; }',
|
|
346
|
+
* shadowCss: ':host { display: block; }',
|
|
347
|
+
* shadowHtml: '<slot></slot>'
|
|
348
|
+
* });
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
function processAssets(instance, assets) {
|
|
352
|
+
const target = instance.target;
|
|
353
|
+
let shadow = target.shadowRoot;
|
|
354
|
+
if (!shadow && Object.keys(assets).some((key) => key.indexOf("shadow") === 0)) shadow = target.attachShadow({ mode: "open" });
|
|
355
|
+
Object.keys(assets).forEach((key) => {
|
|
356
|
+
switch (key) {
|
|
357
|
+
case "css":
|
|
358
|
+
injectCSS(document, assets[key]);
|
|
359
|
+
break;
|
|
360
|
+
case "targetCss":
|
|
361
|
+
injectCSS(document, assets[key]);
|
|
362
|
+
break;
|
|
363
|
+
case "shadowCss":
|
|
364
|
+
injectCSS(shadow, assets[key]);
|
|
365
|
+
break;
|
|
366
|
+
case "shadowHtml":
|
|
367
|
+
shadow.innerHTML = assets[key];
|
|
368
|
+
bindTemplateEvents(shadow, instance);
|
|
369
|
+
break;
|
|
370
|
+
case "targetHtml":
|
|
371
|
+
target.innerHTML = assets[key];
|
|
372
|
+
bindTemplateEvents(target, instance);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Loads companion asset files (CSS, HTML) for a given component basename from the server.
|
|
379
|
+
*
|
|
380
|
+
* Each entry in `assets` encodes an optional variant and extension in camelCase, e.g. `'shadowCss'` → `{name}.shadow.css`,
|
|
381
|
+
* `'targetHtml'` → `{name}.target.html`. Plain entries like `'css'` or `'html'` resolve to
|
|
382
|
+
* `{name}.css` / `{name}.html`.
|
|
383
|
+
*
|
|
384
|
+
* In bundled mode the Vite plugin replaces this call with an inline array literal, so `.then()`
|
|
385
|
+
* fires synchronously — no async overhead.
|
|
386
|
+
*
|
|
387
|
+
* @hidden
|
|
388
|
+
* @param name - The component basename (e.g. `'shop-catalog'`).
|
|
389
|
+
* @param assets - Array of asset descriptors such as `'shadowCss'`, `'targetHtml'`, `'shadowHtml'`, `'css'`, `'html'`.
|
|
390
|
+
* @returns A promise resolving to an array of file contents in the same order as `assets`. If a file is not found, the corresponding entry is `'file not found: ' + filename`.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* export const MyComponent = loadAssets('my-component', ['shadowCss', 'shadowHtml']).then(([shadowCss, shadowHtml]) => {
|
|
395
|
+
* class _MyComponent extends DC { ... }
|
|
396
|
+
* defineCE('my-component', _MyComponent);
|
|
397
|
+
* return _MyComponent;
|
|
398
|
+
* });
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
function loadAssets(name, assets) {
|
|
402
|
+
const parseAsset = (asset) => {
|
|
403
|
+
const m = asset.match(/^([a-z]+)([A-Z][a-z]*)$/);
|
|
404
|
+
if (m) return `./${name.replace(/-/g, "/")}.${m[1]}.${m[2].toLowerCase()}`;
|
|
405
|
+
return `./${name.replace(/-/g, "/")}.${asset.toLowerCase()}`;
|
|
406
|
+
};
|
|
407
|
+
return Promise.all(assets.map(async (asset) => {
|
|
408
|
+
const filename = parseAsset(asset);
|
|
409
|
+
try {
|
|
410
|
+
const resp = await fetch(filename);
|
|
411
|
+
if (!resp.ok) return `file not found: ${filename}`;
|
|
412
|
+
if (filename.endsWith("json")) {
|
|
413
|
+
const text = await resp.text();
|
|
414
|
+
try {
|
|
415
|
+
return JSON.parse(text);
|
|
416
|
+
} catch {
|
|
417
|
+
console.error(`could not JSON.parse() asset '${asset}' from ${filename}:\n`, text);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return resp.text();
|
|
422
|
+
} catch {
|
|
423
|
+
return `file not found: ${filename}`;
|
|
424
|
+
}
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Binds declarative event attributes in a template fragment to microcomponent triggers.
|
|
429
|
+
*
|
|
430
|
+
* Scans all elements under `root` for attributes whose names correspond to native DOM
|
|
431
|
+
* events (e.g. `click`, `submit`, `change`). The attribute value encodes the target and
|
|
432
|
+
* handler method in the form `target|methodName`:
|
|
433
|
+
*
|
|
434
|
+
* - `this|myHandler` — triggers locally on the component attached to this element.
|
|
435
|
+
* - `other-comp|submit` — resolves elements via `querySelectorAll('other-comp')` and
|
|
436
|
+
* triggers the event on each matched host's `_mc`.
|
|
437
|
+
*
|
|
438
|
+
* An optional sibling `payload` attribute supplies the data argument for `trigger()`.
|
|
439
|
+
* After binding, both the event attribute and the payload attribute are removed from the DOM.
|
|
440
|
+
*
|
|
441
|
+
* @param rootElement - The container element whose descendants should be scanned for bindings.
|
|
442
|
+
* @param instance - The instance to trigger events on.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```html
|
|
446
|
+
* <button click='this|onSubmit|{"action":"go"}'>Submit</button>
|
|
447
|
+
* <input change="|cart-form|refresh">
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
function bindTemplateEvents(rootElement, instance) {
|
|
451
|
+
const allElements = [...rootElement.querySelectorAll("*")];
|
|
452
|
+
for (const el of allElements) {
|
|
453
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
454
|
+
const attrs = [...el.attributes];
|
|
455
|
+
for (const attr of attrs) {
|
|
456
|
+
const lowerName = attr.name.toLowerCase();
|
|
457
|
+
if (!("on" + lowerName in el)) continue;
|
|
458
|
+
const parts = attr.value.trim().split("|");
|
|
459
|
+
if (parts.length < 2) continue;
|
|
460
|
+
const [selector, event, payload] = parts;
|
|
461
|
+
el.removeAttribute(attr.name);
|
|
462
|
+
let payloadData;
|
|
463
|
+
if (payload) try {
|
|
464
|
+
payloadData = JSON.parse(payload);
|
|
465
|
+
} catch {
|
|
466
|
+
payloadData = payload;
|
|
467
|
+
}
|
|
468
|
+
if (selector === "this" || selector === "") on(el, lowerName, () => instance.trigger(event, payloadData));
|
|
469
|
+
else on(el, lowerName, () => trigger(selector, event, payloadData));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/core/mcGuards.ts
|
|
475
|
+
function isMcHost(element, key) {
|
|
476
|
+
return typeof HTMLElement !== "undefined" && element instanceof HTMLElement && "_mc" in element && (key === void 0 || key in element._mc);
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/core/MC.ts
|
|
480
|
+
function add(targetHTMLElement, key, element = {}) {
|
|
481
|
+
const node = targetHTMLElement;
|
|
482
|
+
if (!("_mc" in targetHTMLElement)) node._mc = new DC(targetHTMLElement);
|
|
483
|
+
const _mc = node._mc;
|
|
484
|
+
if (key.length && !_mc[key]) {
|
|
485
|
+
_mc[key] = element;
|
|
486
|
+
if (targetHTMLElement instanceof HTMLElement && !targetHTMLElement.getAttribute("data-mc")) targetHTMLElement.setAttribute("data-mc", "");
|
|
487
|
+
}
|
|
488
|
+
return node;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
Removes a named microcomponent from the internal `_mc` collection of a target DOM node.
|
|
492
|
+
|
|
493
|
+
This function deletes the specified key from the element's `_mc` hash object and, if the
|
|
494
|
+
collection becomes empty, removes the `data-mc` attribute from the DOM node.
|
|
495
|
+
|
|
496
|
+
The bare poisoned value (`el._mc.gone` without member access) is `never`,
|
|
497
|
+
which is assignable to anything — only member/method use of a removed entry is caught.
|
|
498
|
+
|
|
499
|
+
@template T - A plain or already-enhanced element type.
|
|
500
|
+
@template K - The literal registration key; its entry is poisoned in the registry type.
|
|
501
|
+
@param target - The DOM element whose microcomponent should be removed.
|
|
502
|
+
@param key - The property name/key corresponding to the microcomponent to remove.
|
|
503
|
+
*/
|
|
504
|
+
function remove(target, key) {
|
|
505
|
+
if (!key) return;
|
|
506
|
+
const _mc = target._mc;
|
|
507
|
+
if (!_mc) return;
|
|
508
|
+
if (_mc[key]) delete _mc[key];
|
|
509
|
+
if (!Object.keys(_mc).length && target instanceof HTMLElement && target.hasAttribute("data-mc")) target.removeAttribute("data-mc");
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
Runs the "Init" lifecycle event on a DOM microcomponent instance asynchronously.
|
|
513
|
+
|
|
514
|
+
This function schedules the `trigger('Init')` call in a microtask, ensuring
|
|
515
|
+
that all synchronous initialization is complete before the lifecycle hook
|
|
516
|
+
executes. Only `DC` instances have an Init lifecycle — the `McBase`
|
|
517
|
+
constructor calls this exclusively behind an `instanceof DC` check, so the
|
|
518
|
+
signature encodes that invariant instead of re-checking it at runtime.
|
|
519
|
+
|
|
520
|
+
@param element - The DOM microcomponent instance on which to trigger the Init event.
|
|
521
|
+
*/
|
|
522
|
+
function init(element) {
|
|
523
|
+
queueMicrotask(() => {
|
|
524
|
+
element.trigger("Init", null);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
Attaches event listeners to DOM elements based on method naming conventions.
|
|
529
|
+
|
|
530
|
+
Scans the prototype of the provided element and attaches listeners for methods
|
|
531
|
+
prefixed with `on` or `one`, corresponding to either native HTML events (e.g.,
|
|
532
|
+
'onclick') or microcomponent-prefixed custom events (e.g., '_mcUpdate').
|
|
533
|
+
|
|
534
|
+
@param element - The instance whose prototype should be scanned for event handlers.
|
|
535
|
+
*/
|
|
536
|
+
function autoAttachListeners(element) {
|
|
537
|
+
const sourceTarget = element.target;
|
|
538
|
+
const prototype = Object.getPrototypeOf(element);
|
|
539
|
+
const sourceElement = element;
|
|
540
|
+
const definedEventHandlerNames = Object.getOwnPropertyNames(prototype).filter((key) => /^(on[A-Z]|one[A-Z])/.test(key) && typeof prototype[key] === "function");
|
|
541
|
+
for (const eventHandlerName of definedEventHandlerNames) {
|
|
542
|
+
const once = /^one/.test(eventHandlerName);
|
|
543
|
+
const eventName = eventHandlerName.replace(/^one?/, "");
|
|
544
|
+
const listener = (ev) => sourceElement[eventHandlerName](ev);
|
|
545
|
+
on(sourceTarget, eventName, listener, once);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
Retrieves immediate child microcomponents of a DOM node.
|
|
550
|
+
|
|
551
|
+
Queries the DOM for descendant elements marked with `data-mc` that are direct
|
|
552
|
+
children of the provided element and returns their associated `_mc` instances.
|
|
553
|
+
|
|
554
|
+
@param domNode - The DOM element whose child microcomponents to find.
|
|
555
|
+
@returns An array of child microcomponent instances.
|
|
556
|
+
*/
|
|
557
|
+
function getMcChildNodes(domNode) {
|
|
558
|
+
return Array.from(domNode.querySelectorAll("[data-mc]:not(:scope [data-mc] [data-mc])")).filter((e) => e instanceof HTMLElement && "_mc" in e).map((e) => e._mc);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
Walks up the DOM from `from`, returning the `_mc` controller of the nearest
|
|
562
|
+
`data-mc` ancestor — restricted, when `key` is given, to hosts whose registry
|
|
563
|
+
actually carries that key. Hosts marked `data-mc` but not (yet) carrying an
|
|
564
|
+
`_mc` record (e.g. autoload pending) are skipped rather than returned.
|
|
565
|
+
|
|
566
|
+
@param from - The element whose ancestors to search (exclusive).
|
|
567
|
+
@param key - Optional registry key the ancestor's `_mc` must contain.
|
|
568
|
+
@returns The nearest matching controller, or null.
|
|
569
|
+
*/
|
|
570
|
+
function findMcAncestor(from, key) {
|
|
571
|
+
let el = from.parentElement?.closest("[data-mc]");
|
|
572
|
+
while (el instanceof HTMLElement) {
|
|
573
|
+
/* v8 ignore next */
|
|
574
|
+
if ("_mc" in el) {
|
|
575
|
+
const mc = el._mc;
|
|
576
|
+
if (key === void 0 || key in mc) return mc;
|
|
577
|
+
}
|
|
578
|
+
el = el.parentElement?.closest("[data-mc]");
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
The base class for all microcomponents — shared root of both {@link MC} and {@link DC}.
|
|
584
|
+
|
|
585
|
+
Holds a `WeakRef` to the target (a DOM element for `DC`, or any plain object for `MC`)
|
|
586
|
+
to avoid circular references. For `DC` subclasses it also bootstraps two automatic
|
|
587
|
+
behaviours:
|
|
588
|
+
|
|
589
|
+
1. **Event listener auto-attachment** — methods whose names start with `on` or `one`
|
|
590
|
+
(e.g. `onClick`, `oneInit`) are wired to the target element via {@link on} / {@link one}.
|
|
591
|
+
2. **"Init" lifecycle event** — dispatched asynchronously in a microtask so that all
|
|
592
|
+
synchronous constructor work finishes first.
|
|
593
|
+
|
|
594
|
+
Both mechanisms run only when the instance is a `DC` (checked with `instanceof DC`).
|
|
595
|
+
Plain `MC` instances skip them entirely, keeping server-side usage lightweight.
|
|
596
|
+
|
|
597
|
+
### Plugin extension points
|
|
598
|
+
|
|
599
|
+
The prototype of `McBase` is the attachment point for optional plugins that curry
|
|
600
|
+
additional instance methods onto every microcomponent. Two built-in plugins modify
|
|
601
|
+
this class at runtime:
|
|
602
|
+
|
|
603
|
+
- **{@link State.observePlugin}** — adds `notify()`, `subscribe()`, and `bind()` so that
|
|
604
|
+
observable properties can be controlled directly on the component instance with
|
|
605
|
+
fluent chaining. Call `observePlugin(DC)` once at module level to activate.
|
|
606
|
+
|
|
607
|
+
- **{@link State.formValuePlugin}** — adds `formValues()` for two-way binding between DOM
|
|
608
|
+
form controls and an observable property. Also pulls in all methods from
|
|
609
|
+
{@link State.observePlugin}, so you can chain `.subscribe()` and `.bind()` after
|
|
610
|
+
`.formValues()`. Call `formValuePlugin(DC)` once at module level to activate.
|
|
611
|
+
|
|
612
|
+
Both plugins are idempotent — calling them multiple times has no side effects. They
|
|
613
|
+
work by assigning methods onto `McBase.prototype` (reached via
|
|
614
|
+
`Object.getPrototypeOf(Class.prototype)`) and use TypeScript module augmentation
|
|
615
|
+
(`declare module`) so the added signatures appear in type-checking automatically.
|
|
616
|
+
|
|
617
|
+
@template TTarget - The concrete type of the wrapped target; preserved by the
|
|
618
|
+
{@link McBase.target} getter.
|
|
619
|
+
|
|
620
|
+
@example Extending with plugins at runtime
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
import { DC, observePlugin, formValuePlugin } from '@twobirds/microcomponents';
|
|
624
|
+
|
|
625
|
+
observePlugin(DC);
|
|
626
|
+
formValuePlugin(DC);
|
|
627
|
+
|
|
628
|
+
class MyForm extends DC {
|
|
629
|
+
data = { name: '' };
|
|
630
|
+
|
|
631
|
+
oneInit() {
|
|
632
|
+
this
|
|
633
|
+
.formValues(this.target, 'data')
|
|
634
|
+
.subscribe('data', (v) => console.log(v))
|
|
635
|
+
.bind('data', this.target);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`observePlugin(DC)` adds `notify()`, `subscribe()`, `bind()` to `McBase.prototype`;
|
|
641
|
+
`formValuePlugin(DC)` adds `formValues()` and re-runs `observePlugin`. `oneInit()`
|
|
642
|
+
runs once after the constructor.
|
|
643
|
+
@group Micro Components
|
|
644
|
+
*/
|
|
645
|
+
var McBase = class {
|
|
646
|
+
_target;
|
|
647
|
+
/**
|
|
648
|
+
The constructor for the base `McBase` class.
|
|
649
|
+
|
|
650
|
+
Initializes a weak reference to the target and triggers auto-attachment
|
|
651
|
+
of listeners and initialization lifecycle events.
|
|
652
|
+
|
|
653
|
+
@param target - The object or element this microcomponent represents.
|
|
654
|
+
*/
|
|
655
|
+
constructor(target) {
|
|
656
|
+
this._target = new WeakRef(target || {});
|
|
657
|
+
if (this instanceof DC && this.constructor !== DC) {
|
|
658
|
+
autoAttachListeners(this);
|
|
659
|
+
const tag = this.constructor.tag;
|
|
660
|
+
if (tag === void 0 || tag.toUpperCase() === this.target.tagName) init(this);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
Returns the current target object (DOM node or other data) referenced by this microcomponent.
|
|
665
|
+
|
|
666
|
+
Uses a `WeakRef` internally to avoid circular references, and safely dereferences it here.
|
|
667
|
+
|
|
668
|
+
The dereference is asserted non-null: the controller is stored on its target
|
|
669
|
+
(`element._mc`), so any code that can reach the controller through the element is
|
|
670
|
+
holding the target alive. Only a controller retained independently after its element
|
|
671
|
+
was garbage-collected could observe a stale reference — do not cache controllers
|
|
672
|
+
detached from their elements.
|
|
673
|
+
|
|
674
|
+
@returns The target object.
|
|
675
|
+
*/
|
|
676
|
+
get target() {
|
|
677
|
+
return this._target.deref();
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
/**
|
|
681
|
+
@class DC
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
A DOM-centric microcomponent that operates on a specific HTML element.
|
|
685
|
+
|
|
686
|
+
Extends {@link McBase} and associates itself with an actual DOM node via the
|
|
687
|
+
`_target` reference. Provides methods for event handling, DOM traversal,
|
|
688
|
+
and interaction with sibling or ancestor microcomponents.
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Static Configuration Properties
|
|
693
|
+
|
|
694
|
+
DC classes support three static properties that configure custom elements
|
|
695
|
+
before any instance is constructed:
|
|
696
|
+
|
|
697
|
+
### `static tag?: string`
|
|
698
|
+
|
|
699
|
+
Replaces the default target element with a semantic HTML element, or removes it entirely.
|
|
700
|
+
|
|
701
|
+
- **Non-empty string** (e.g., `'header'`) — the original custom element is replaced
|
|
702
|
+
with the specified built-in element. The original tag name is added as a CSS class.
|
|
703
|
+
- **Empty string `''`** — the target element is removed from the DOM entirely.
|
|
704
|
+
Useful for components that only inject global styles or act as style containers.
|
|
705
|
+
|
|
706
|
+
@example static tag — replace with semantic element
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
class ShopHeader extends DC {
|
|
710
|
+
static tag = 'header';
|
|
711
|
+
}
|
|
712
|
+
defineCE('shop-header', ShopHeader);
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Produces `<header class="shop-header">` instead of `<shop-header>`.
|
|
716
|
+
|
|
717
|
+
@example static tag — remove element entirely
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
class GlobalStyles extends DC {
|
|
721
|
+
static tag = '';
|
|
722
|
+
}
|
|
723
|
+
defineCE('global-styles', GlobalStyles);
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
The element self-destructs; only side effects (e.g., injected styles) remain.
|
|
727
|
+
|
|
728
|
+
### `static assets: string[]`
|
|
729
|
+
|
|
730
|
+
Declares companion files for auto-loading by convention. Asset names are camelCase
|
|
731
|
+
and translated into file extensions: `'targetHtml'` → `.target.html`, `'json'` → `.json`.
|
|
732
|
+
|
|
733
|
+
The path is derived from the custom element tag segments, and the basename is the
|
|
734
|
+
last segment. For `<cart-container-checkout>`, assets load from `/cart/container/checkout.*`.
|
|
735
|
+
|
|
736
|
+
When assets finish loading, an `'assets'` event fires so that `oneAssets(ev)` can
|
|
737
|
+
process them. Everything ending in `Json` arrives parsed; all other files arrive as text.
|
|
738
|
+
|
|
739
|
+
**One-time global styles**: Assets named `'css'` and `'targetCss'` inject CSS into the
|
|
740
|
+
global document scope via `injectCSS(document, ...)`. Because these are idempotent
|
|
741
|
+
global side effects, they are processed only once — after the first instance loads them,
|
|
742
|
+
the entries are removed from both `static assets` and `static _assets`. Subsequent
|
|
743
|
+
instances skip loading and re-processing them entirely.
|
|
744
|
+
|
|
745
|
+
**CSS file roles**:
|
|
746
|
+
|
|
747
|
+
- `.css` — design tokens only (CSS custom-property presets). Injected into the document
|
|
748
|
+
scope once, like `targetCss`.
|
|
749
|
+
- `.target.css` — layout rules for the light DOM. Also document-scoped and one-time.
|
|
750
|
+
- `.shadow.css` — layout rules for the shadow DOM. Injected per instance into the
|
|
751
|
+
component's own shadow root, so each instance gets its own copy.
|
|
752
|
+
|
|
753
|
+
Every declared asset — CSS, HTML, JSON — is inlined into `static _assets` at build time by
|
|
754
|
+
the load-assets plugin, so a bundled build performs no fetches at all.
|
|
755
|
+
|
|
756
|
+
In a bundled build the plain `.css` files are additionally concatenated into a single
|
|
757
|
+
`bundle.css` by the bundling script. That file is a convenience listing of the available
|
|
758
|
+
variables: copy the ones you want to change into your own stylesheet and override them
|
|
759
|
+
there. It is not required at runtime, and it cannot drift from the inlined presets — both
|
|
760
|
+
are read from the same source files.
|
|
761
|
+
|
|
762
|
+
The runtime decides whether an asset still needs fetching with `!ctor._assets[key]`, so an
|
|
763
|
+
inlined entry must never be an empty string: that would trigger a re-fetch of a file that
|
|
764
|
+
does not exist in a bundled build.
|
|
765
|
+
|
|
766
|
+
@example static assets with autoload
|
|
767
|
+
|
|
768
|
+
```ts
|
|
769
|
+
class CartCheckout extends DC {
|
|
770
|
+
static assets = ['targetHtml', 'targetCss', 'configJson'];
|
|
771
|
+
|
|
772
|
+
oneAssets(ev) {
|
|
773
|
+
const { configJson } = ev.detail.data;
|
|
774
|
+
console.log(configJson);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
defineCE('cart-checkout', CartCheckout);
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
`configJson` arrives already parsed into an object.
|
|
781
|
+
|
|
782
|
+
### `static attributes?: object`
|
|
783
|
+
|
|
784
|
+
Sets HTML attributes on every instance automatically.
|
|
785
|
+
|
|
786
|
+
@example static attributes
|
|
787
|
+
|
|
788
|
+
```ts
|
|
789
|
+
class ProductCard extends DC {
|
|
790
|
+
static attributes = { role: 'listitem' };
|
|
791
|
+
}
|
|
792
|
+
defineCE('product-card', ProductCard);
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Every `<product-card>` carries `role="listitem"`.
|
|
796
|
+
|
|
797
|
+
### Combined usage
|
|
798
|
+
|
|
799
|
+
All three can be combined in one class:
|
|
800
|
+
|
|
801
|
+
@example All static properties together
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
class ShopCatalog extends DC {
|
|
805
|
+
static tag = 'section';
|
|
806
|
+
static attributes = { role: 'list', 'aria-label': 'products' };
|
|
807
|
+
static assets = ['targetHtml', 'targetCss'];
|
|
808
|
+
}
|
|
809
|
+
defineCE('shop-catalog', ShopCatalog);
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Produces `<section role="list" aria-label="products">`.
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## Event Handler Conventions
|
|
817
|
+
|
|
818
|
+
Methods whose names start with `on` or `one` are automatically wired as event listeners
|
|
819
|
+
on the target element. The prefix determines persistence:
|
|
820
|
+
|
|
821
|
+
- **`on<EventName>`** — persistent listener (fires every time)
|
|
822
|
+
- **`one<EventName>`** — one-time listener (fires once, then detaches)
|
|
823
|
+
|
|
824
|
+
### Web Component Lifecycle Events
|
|
825
|
+
|
|
826
|
+
When a DC is registered via {@link defineCE}, the custom element dispatches these
|
|
827
|
+
internal events that can be caught with handler methods:
|
|
828
|
+
|
|
829
|
+
| Method | When it fires |
|
|
830
|
+
|--------|---------------|
|
|
831
|
+
| `onConnected()` | Element was inserted into the DOM (`connectedCallback`) |
|
|
832
|
+
| `onDisconnected()` | Element was removed from the DOM (`disconnectedCallback`) |
|
|
833
|
+
| `onAdopted()` | Element was moved to a new document (`adoptedCallback`) |
|
|
834
|
+
| `onAttributeChanged()` | An observed attribute changed (`attributeChangedCallback`) |
|
|
835
|
+
| `oneInit()` | Constructor finished; all synchronous setup complete (microtask) |
|
|
836
|
+
| `oneAssets()` | Constructor finished; all assets loaded and available; ev contains assets object |
|
|
837
|
+
|
|
838
|
+
@example Event handlers in action
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
class MyElement extends DC {
|
|
842
|
+
onConnected() {
|
|
843
|
+
console.log('entered the DOM');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
onDisconnected() {
|
|
847
|
+
console.log('left the DOM');
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
onClick() {
|
|
851
|
+
this.trigger('something', 'payload');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
oneSomething(ev) {
|
|
855
|
+
console.log('first time only:', ev.detail.data);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
defineCE('my-element', MyElement);
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
## Construction Patterns
|
|
864
|
+
|
|
865
|
+
### Template usage (declarative)
|
|
866
|
+
|
|
867
|
+
```ts
|
|
868
|
+
class MyElement extends DC {}
|
|
869
|
+
defineCE('my-element', MyElement);
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
The registered element is then used in HTML as `<my-element></my-element>`.
|
|
873
|
+
|
|
874
|
+
### Programmatic construction with data
|
|
875
|
+
|
|
876
|
+
When constructing programmatically via {@link getCE}, pass only the `data` payload —
|
|
877
|
+
the target element is created automatically by the custom element constructor.
|
|
878
|
+
|
|
879
|
+
@example Programmatic construction
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
class MyElement extends DC {
|
|
883
|
+
data = {};
|
|
884
|
+
|
|
885
|
+
constructor(target, data) {
|
|
886
|
+
super(target);
|
|
887
|
+
this.data = data;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
defineCE('my-element', MyElement);
|
|
891
|
+
|
|
892
|
+
const Ctor = getCE('my-element');
|
|
893
|
+
document.body.append(new Ctor({ name: 'John' }));
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### Data from HTML attributes
|
|
897
|
+
|
|
898
|
+
@example Reading data from an attribute
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
class MyElement extends DC {
|
|
902
|
+
data = {};
|
|
903
|
+
|
|
904
|
+
constructor(target) {
|
|
905
|
+
super(target);
|
|
906
|
+
this.data = JSON.parse(target.getAttribute('my-data'));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
defineCE('my-element', MyElement);
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
Used in HTML as `<my-element my-data='{"name":"John"}'></my-element>`.
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
## Plugin Extension Points
|
|
917
|
+
|
|
918
|
+
The prototype of `DC` (inherited from {@link McBase}) is extended at runtime by optional
|
|
919
|
+
plugins. See the {@link McBase} documentation for details on {@link State.observePlugin} and
|
|
920
|
+
{@link State.formValuePlugin}.
|
|
921
|
+
|
|
922
|
+
@template T - The concrete element type of the host element.
|
|
923
|
+
@template EM - The component's custom event map (`event name → payload type`).
|
|
924
|
+
Declaring it registers events on the type level: `trigger()` enforces the
|
|
925
|
+
payload for registered names, and `on()`/`one()` handlers receive
|
|
926
|
+
`CustomEvent<McDetail<Payload>>` with no annotation needed. Unregistered
|
|
927
|
+
names keep the open `unknown`-payload behavior.
|
|
928
|
+
|
|
929
|
+
@example Typed event map
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
class Cart extends DC<HTMLFormElement, { CartUpdate: { count: number } }> {
|
|
933
|
+
checkout() {
|
|
934
|
+
this.trigger('CartUpdate', { count: 2 }, 'u');
|
|
935
|
+
this.on('CartUpdate', (ev) => ev.detail.data.count);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
`trigger()` checks the payload against the registered `CartUpdate` shape, and the `on()`
|
|
941
|
+
handler receives a fully typed `ev`.
|
|
942
|
+
@group Micro Components
|
|
943
|
+
*/
|
|
944
|
+
var DC = class DC extends McBase {
|
|
945
|
+
/**
|
|
946
|
+
* Replaces the target element with a semantic HTML tag, or removes it entirely.
|
|
947
|
+
* Set to `'header'` to swap `<my-element>` for `<header>`, or `''` to self-destruct.
|
|
948
|
+
*/
|
|
949
|
+
static tag;
|
|
950
|
+
/**
|
|
951
|
+
* Result container for assets (e.g., `['targetHtml', 'json']`).
|
|
952
|
+
* Asset names (keys) are camelCase and translated into file extensions: `'targetHtml'` → `.target.html`.
|
|
953
|
+
*/
|
|
954
|
+
static _assets = {};
|
|
955
|
+
/**
|
|
956
|
+
* Declares companion files for auto-loading by convention (e.g., `['targetHtml', 'json']`).
|
|
957
|
+
* Asset names are camelCase and translated into file extensions: `'targetHtml'` → `.target.html`.
|
|
958
|
+
*/
|
|
959
|
+
static assets = [];
|
|
960
|
+
/**
|
|
961
|
+
* In-flight asset request, cached per class so that instances constructed while
|
|
962
|
+
* the first fetch is still pending (e.g. in a loop) share a single request
|
|
963
|
+
* instead of each triggering their own. Resolves to the freshly loaded assets.
|
|
964
|
+
*/
|
|
965
|
+
static _assetsPromise;
|
|
966
|
+
/**
|
|
967
|
+
* Sets HTML attributes on every instance automatically.
|
|
968
|
+
* Example: `{ role: 'listitem', 'aria-label': 'nav' }`.
|
|
969
|
+
*/
|
|
970
|
+
static attributes = {};
|
|
971
|
+
/**
|
|
972
|
+
* Constructs a new DC instance and applies static configuration.
|
|
973
|
+
*
|
|
974
|
+
* The constructor performs these steps in order:
|
|
975
|
+
* 1. Calls {@link McBase} to establish the target reference and auto-attach listeners.
|
|
976
|
+
* 2. If `static tag` is set and differs from the current element, replaces the target
|
|
977
|
+
* with a new built-in element (or removes it if `tag === ''`).
|
|
978
|
+
* 3. If `static assets` has entries, initiates asynchronous loading, fires an `'assets'` event when complete handled by `oneAssets(ev)`.
|
|
979
|
+
* 4. Applies any `static attributes` to the target element.
|
|
980
|
+
*
|
|
981
|
+
* @param target - The host DOM element this component operates on.
|
|
982
|
+
*/
|
|
983
|
+
constructor(target) {
|
|
984
|
+
super(target);
|
|
985
|
+
const ctor = this.constructor;
|
|
986
|
+
if (ctor === DC) return;
|
|
987
|
+
const tagName = this.target.tagName.toLowerCase();
|
|
988
|
+
if (ctor["tag"] !== void 0 && tagName !== ctor.tag && ctor.tag !== "") {
|
|
989
|
+
const element = document.createElement(ctor.tag);
|
|
990
|
+
element.classList.add(tagName);
|
|
991
|
+
ctor._originalTag = tagName;
|
|
992
|
+
DC.add(element, ctor.name, new ctor(element));
|
|
993
|
+
delete ctor._originalTag;
|
|
994
|
+
if (tagName.includes("-") && !this.target.isConnected) this.on("connected", () => this.target.replaceWith(element));
|
|
995
|
+
else this.target.replaceWith(element);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const attributes = ctor.attributes;
|
|
999
|
+
if (ctor.attributes) Object.keys(ctor.attributes).forEach((key) => {
|
|
1000
|
+
this.target.setAttribute(key, attributes[key]);
|
|
1001
|
+
});
|
|
1002
|
+
const that = this;
|
|
1003
|
+
const remainingAssets = ctor.assets.filter((key) => !ctor._assets[key]);
|
|
1004
|
+
if (Object.keys(ctor._assets).length) processAssets(this, ctor._assets);
|
|
1005
|
+
if (remainingAssets.length) {
|
|
1006
|
+
if (!ctor._assetsPromise) ctor._assetsPromise = loadAssets(ctor._originalTag || tagName, remainingAssets).then((res) => {
|
|
1007
|
+
const loadedAssets = {};
|
|
1008
|
+
remainingAssets.forEach((key, i) => {
|
|
1009
|
+
loadedAssets[key] = res[i];
|
|
1010
|
+
});
|
|
1011
|
+
ctor._assets = Object.assign({}, ctor._assets, loadedAssets);
|
|
1012
|
+
for (const onceKey of ["css", "targetCss"]) if (loadedAssets[onceKey] !== void 0) delete ctor._assets[onceKey];
|
|
1013
|
+
ctor.assets = ctor.assets.filter((key) => key !== "css" && key !== "targetCss");
|
|
1014
|
+
return loadedAssets;
|
|
1015
|
+
});
|
|
1016
|
+
ctor._assetsPromise.then((loadedAssets) => {
|
|
1017
|
+
processAssets(this, loadedAssets);
|
|
1018
|
+
that.trigger("assets", ctor._assets);
|
|
1019
|
+
});
|
|
1020
|
+
} else if ([...Object.keys(ctor._assets)].length) queueMicrotask(() => that.trigger("assets", ctor._assets));
|
|
1021
|
+
if (ctor.tag === "") {
|
|
1022
|
+
this.target.remove();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
Returns the loaded assets for this component instance.
|
|
1028
|
+
|
|
1029
|
+
Assets are loaded asynchronously when `static assets` is defined on the class.
|
|
1030
|
+
This method returns the resolved asset map keyed by filename, or `undefined`
|
|
1031
|
+
if assets have not yet been loaded.
|
|
1032
|
+
|
|
1033
|
+
@returns A record mapping asset keys to their content strings, or undefined.
|
|
1034
|
+
*/
|
|
1035
|
+
getAssets() {
|
|
1036
|
+
return this.constructor._assets || {};
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
Static helper to attach a microcomponent instance to a DOM element.
|
|
1040
|
+
|
|
1041
|
+
Wrapper around the `add()` utility function, providing a class-level API
|
|
1042
|
+
for adding child microcomponents to a target node.
|
|
1043
|
+
|
|
1044
|
+
@template T - The concrete element type of the target; preserved in the return type.
|
|
1045
|
+
@template K - The literal registration key; becomes a known entry of the returned `_mc` registry.
|
|
1046
|
+
@template E - The type of the attached element; retrievable as `result._mc[key]`.
|
|
1047
|
+
@param target - The DOM element to which to attach the microcomponent.
|
|
1048
|
+
@param key - The property name/key under which to store the instance in `_mc`.
|
|
1049
|
+
@returns The target DOM element with the attached microcomponent.
|
|
1050
|
+
*/
|
|
1051
|
+
static add(target, key, element = {}) {
|
|
1052
|
+
return add(target, key, element);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
Static helper to remove a named microcomponent from a DOM element.
|
|
1056
|
+
|
|
1057
|
+
Wrapper around the `remove()` utility function, providing a class-level API
|
|
1058
|
+
for removing child microcomponents.
|
|
1059
|
+
|
|
1060
|
+
@template T - A plain or already-enhanced element type.
|
|
1061
|
+
@template K - The literal registration key; its entry is poisoned in the registry type.
|
|
1062
|
+
@param target - The DOM element from which to remove the microcomponent.
|
|
1063
|
+
@param key - The property name/key corresponding to the microcomponent to remove.
|
|
1064
|
+
*/
|
|
1065
|
+
static remove(target, key) {
|
|
1066
|
+
remove(target, key);
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
Class-verified entry lookup: returns the component stored under `key` on the
|
|
1070
|
+
element's `_mc` record — but only if it is an instance of `Klass`.
|
|
1071
|
+
|
|
1072
|
+
This is the *sound* retrieval primitive: presence AND type identity are both
|
|
1073
|
+
proven at runtime (`instanceof`), so it needs no global `McRegistry`
|
|
1074
|
+
declaration and stays correct even when different elements use the same key
|
|
1075
|
+
name for different component types. Accepts any value (including
|
|
1076
|
+
`querySelector` results that may be null) — every failure mode returns
|
|
1077
|
+
`undefined`.
|
|
1078
|
+
|
|
1079
|
+
@template E - The component type, inferred from `Klass`.
|
|
1080
|
+
@param element - The value to look the entry up on; anything non-host yields undefined.
|
|
1081
|
+
@param key - The registry key name to look under.
|
|
1082
|
+
@param elementMcClass - The class the entry must be an instance of.
|
|
1083
|
+
@returns The verified instance, or undefined.
|
|
1084
|
+
@example
|
|
1085
|
+
```ts
|
|
1086
|
+
const cart = DC.get(document.querySelector('form'), 'cart', Cart);
|
|
1087
|
+
cart?.addItem(item);
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
`cart` is `Cart | undefined` — presence and type identity are both proven by the
|
|
1091
|
+
`instanceof` check inside `get()`, with no global declaration involved.
|
|
1092
|
+
*/
|
|
1093
|
+
static get(element, key, elementMcClass) {
|
|
1094
|
+
if (!isMcHost(element)) return void 0;
|
|
1095
|
+
const entry = element._mc[key];
|
|
1096
|
+
return entry instanceof elementMcClass ? entry : void 0;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
Detaches an event listener from the DOM node.
|
|
1100
|
+
|
|
1101
|
+
This is an instance method wrapper for `off()`, using the internal `_target` DOM reference.
|
|
1102
|
+
|
|
1103
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
1104
|
+
@param handler - The previously attached callback function or object to remove.
|
|
1105
|
+
*/
|
|
1106
|
+
off(eventName, handler) {
|
|
1107
|
+
off(this.target, eventName, handler);
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
Attaches a persistent event listener to the DOM node.
|
|
1111
|
+
|
|
1112
|
+
This is an instance method wrapper for `on()`, using the internal `_target` DOM reference.
|
|
1113
|
+
|
|
1114
|
+
The handler receives a `CustomEvent<McDetail<Payload>>`. For event names
|
|
1115
|
+
registered in the component's event map `EM` the payload is resolved from the
|
|
1116
|
+
map — an unannotated arrow handler is fully typed. For unregistered names the
|
|
1117
|
+
payload defaults to `unknown` and can be pinned via the type argument
|
|
1118
|
+
(`on<Payload>('Name', handler)`) or the handler's own annotation.
|
|
1119
|
+
|
|
1120
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
1121
|
+
@param handler - The callback function or object to invoke.
|
|
1122
|
+
@returns The attached handler reference, unchanged — usable directly with {@link off}.
|
|
1123
|
+
*/
|
|
1124
|
+
on(eventName, handler) {
|
|
1125
|
+
return on(this.target, eventName, handler);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
Attaches a one-time event listener to the DOM node.
|
|
1129
|
+
|
|
1130
|
+
This is an instance method wrapper for `one()`, using the internal `_target` DOM reference.
|
|
1131
|
+
Payload typing follows the same rules as {@link DC.on}.
|
|
1132
|
+
|
|
1133
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
1134
|
+
@param handler - The callback function or object to invoke once.
|
|
1135
|
+
@returns The attached one-time handler reference, unchanged — usable directly with {@link off}.
|
|
1136
|
+
*/
|
|
1137
|
+
one(eventName, handler) {
|
|
1138
|
+
return one(this.target, eventName, handler);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
Triggers a custom event on the DOM node and optionally bubbles it.
|
|
1142
|
+
|
|
1143
|
+
This is an instance method wrapper for `trigger()`, using the internal `_target` DOM reference.
|
|
1144
|
+
|
|
1145
|
+
For event names registered in the component's event map `EM` the payload type
|
|
1146
|
+
is enforced; unregistered names accept any payload (typed via inference or an
|
|
1147
|
+
explicit `trigger<Payload>(...)` argument).
|
|
1148
|
+
|
|
1149
|
+
@param eventName - The name of the event (will be prefixed if needed).
|
|
1150
|
+
@param data - Additional data payload attached to `event.detail.data`.
|
|
1151
|
+
@param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
|
|
1152
|
+
@param controller - Optional controller for managing bubbling behavior.
|
|
1153
|
+
*/
|
|
1154
|
+
trigger(eventName, data = {}, bubble = "l", controller) {
|
|
1155
|
+
trigger(this.target, eventName, data, bubble, controller);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
Returns an array of all ancestor microcomponents up the DOM tree.
|
|
1159
|
+
|
|
1160
|
+
Traverses upward from this element's target, collecting the `_mc` instances
|
|
1161
|
+
of ancestor elements marked with `data-mc`. The keyed form only collects
|
|
1162
|
+
hosts whose registry carries `key`, typed from the global {@link McRegistry}
|
|
1163
|
+
binding — non-optional, since presence is checked during the walk.
|
|
1164
|
+
|
|
1165
|
+
@template R - Known registry entries of the results; pass a record to assert
|
|
1166
|
+
a site-specific shape: `ancestors<{ cart: Cart }>()`. Defaults to none.
|
|
1167
|
+
@returns An array of ancestor microcomponent instances (empty if none found).
|
|
1168
|
+
*/
|
|
1169
|
+
ancestors(key) {
|
|
1170
|
+
const result = [];
|
|
1171
|
+
let current = findMcAncestor(this.target, key);
|
|
1172
|
+
while (current) {
|
|
1173
|
+
result.push(current);
|
|
1174
|
+
current = findMcAncestor(current.target, key);
|
|
1175
|
+
}
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
Returns the closest ancestor microcomponent or null if none found.
|
|
1180
|
+
|
|
1181
|
+
This method is equivalent to calling `ancestors()[0]`, but more concise.
|
|
1182
|
+
The keyed form walks up to the nearest host whose registry carries `key`,
|
|
1183
|
+
typed from the global {@link McRegistry} binding — non-optional, since
|
|
1184
|
+
presence is checked during the walk.
|
|
1185
|
+
|
|
1186
|
+
@template R - Known registry entries of the result; pass a record to assert
|
|
1187
|
+
a site-specific shape: `closest<{ cart: Cart }>()?.cart.addItem(...)`.
|
|
1188
|
+
Defaults to none.
|
|
1189
|
+
@returns The nearest matching ancestor's `_mc`, or null if not found.
|
|
1190
|
+
*/
|
|
1191
|
+
closest(key) {
|
|
1192
|
+
return findMcAncestor(this.target, key);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
Returns an array of direct child microcomponents.
|
|
1196
|
+
|
|
1197
|
+
Calls `getMcChildNodes()` internally to find immediate descendants only,
|
|
1198
|
+
excluding nested grandchildren and deeper levels. The keyed form filters to
|
|
1199
|
+
hosts whose registry carries `key`; see {@link DC.closest}.
|
|
1200
|
+
|
|
1201
|
+
@template R - Known registry entries of the results; see {@link DC.closest}.
|
|
1202
|
+
@returns An array of child microcomponent instances (empty if none found).
|
|
1203
|
+
*/
|
|
1204
|
+
children(key) {
|
|
1205
|
+
const kids = getMcChildNodes(this.target);
|
|
1206
|
+
return key === void 0 ? kids : kids.filter((mc) => key in mc);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
Returns an array of all descendant microcomponents recursively.
|
|
1210
|
+
|
|
1211
|
+
Uses a `querySelectorAll` query to collect every nested element marked with
|
|
1212
|
+
`data-mc`, returning their associated `_mc` instances. The keyed form
|
|
1213
|
+
filters to hosts whose registry carries `key`; see {@link DC.closest}.
|
|
1214
|
+
|
|
1215
|
+
@template R - Known registry entries of the results; see {@link DC.closest}.
|
|
1216
|
+
@returns An array of descendant microcomponent instances (empty if none found).
|
|
1217
|
+
*/
|
|
1218
|
+
descendants(key) {
|
|
1219
|
+
return Array.from(this.target.querySelectorAll("[data-mc]")).filter((e) => e instanceof HTMLElement && "_mc" in e).map((e) => e._mc).filter((mc) => key === void 0 || key in mc);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/core/elements.ts
|
|
1
1224
|
/**
|
|
2
1225
|
Custom element registration utilities — bridges DC microcomponents with the native
|
|
3
1226
|
`CustomElementRegistry`.
|
|
4
1227
|
|
|
5
1228
|
This module provides:
|
|
6
1229
|
- {@link defineCE} — wraps `customElements.define()` to register a custom element that
|
|
7
|
-
|
|
8
|
-
|
|
1230
|
+
hosts a DC instance in its `_mc` registry. The defining class also controls observed
|
|
1231
|
+
attributes; all inner MCs receive lifecycle events (`connected`, `disconnected`, etc.).
|
|
9
1232
|
- {@link getCE} — typed wrapper for `customElements.get()`.
|
|
10
1233
|
- {@link listCE} — returns every tag name registered via {@link defineCE}.
|
|
11
1234
|
|
|
@@ -30,11 +1253,11 @@ The registered element is then used in HTML as `<my-element></my-element>`.
|
|
|
30
1253
|
```ts
|
|
31
1254
|
const MyElementClass = getCE('my-element');
|
|
32
1255
|
if (MyElementClass) {
|
|
33
|
-
|
|
1256
|
+
document.body.append(new MyElementClass());
|
|
34
1257
|
}
|
|
35
1258
|
```
|
|
36
1259
|
|
|
37
|
-
@example List all custom element tags registered by defineCE
|
|
1260
|
+
@example List all custom element tags registered by defineCE
|
|
38
1261
|
|
|
39
1262
|
```ts
|
|
40
1263
|
defineCE('my-element', MyElement);
|
|
@@ -42,8 +1265,6 @@ defineCE('another-widget', AnotherWidget);
|
|
|
42
1265
|
console.log(listCE());
|
|
43
1266
|
```
|
|
44
1267
|
*/
|
|
45
|
-
import { kebabToPascal } from '../utils/helpers.js';
|
|
46
|
-
import { McBase, DC } from './MC.js';
|
|
47
1268
|
/**
|
|
48
1269
|
Creates a named class constructor with the specified name.
|
|
49
1270
|
@param name - The desired name for the class.
|
|
@@ -57,14 +1278,14 @@ console.log(MyNamedClass.name);
|
|
|
57
1278
|
```
|
|
58
1279
|
*/
|
|
59
1280
|
function createNamedClass(name, targetClass) {
|
|
60
|
-
|
|
1281
|
+
return Object.defineProperty(targetClass, "name", { value: name });
|
|
61
1282
|
}
|
|
62
1283
|
function Base(ElementClass) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1284
|
+
return class extends ElementClass {
|
|
1285
|
+
constructor(...args) {
|
|
1286
|
+
super(...args);
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
68
1289
|
}
|
|
69
1290
|
/**
|
|
70
1291
|
### Examples
|
|
@@ -83,9 +1304,9 @@ The registered element is then used in HTML as `<my-element></my-element>`.
|
|
|
83
1304
|
|
|
84
1305
|
```ts
|
|
85
1306
|
class MyElement extends DC {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1307
|
+
onAttributeChanged(ev){
|
|
1308
|
+
const newValue = ev.detail.data.newValue;
|
|
1309
|
+
}
|
|
89
1310
|
}
|
|
90
1311
|
defineCE('my-element', MyElement, ['observed']);
|
|
91
1312
|
```
|
|
@@ -105,66 +1326,62 @@ the element instance with DC (DomComponent).
|
|
|
105
1326
|
@param observedAttributes - An optional array of attribute names to observe for changes.
|
|
106
1327
|
@param extend - DEPRECATED! An optional built-in HTML element tag name to extend, enabling customized built-in elements.
|
|
107
1328
|
@returns The registered custom element constructor, typed to accept the `data` payload
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1329
|
+
and exposing the `_mc` entry under the PascalCase tag name — or `undefined` if the
|
|
1330
|
+
tag name was already defined. Feed it (or an equivalent `McElementConstructor`) to
|
|
1331
|
+
the global {@link McElementTagNameMap} to make `getCE()` return it typed by string.
|
|
111
1332
|
@group Elements
|
|
112
1333
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return McElementClass;
|
|
165
|
-
}
|
|
166
|
-
export function getCE(tagName) {
|
|
167
|
-
return customElements.get(tagName);
|
|
1334
|
+
function defineCE(tagName, DefinitionClass, observedAttributes = [], extend) {
|
|
1335
|
+
if (customElements.get(tagName)) {
|
|
1336
|
+
console.warn(`IGNORED: Custom element "${tagName}" is already defined!`);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (!tagName.includes("-")) throw new Error("createCustomElement: tagName must contain at least one hyphen (\"-\").");
|
|
1340
|
+
let extendClass = HTMLElement;
|
|
1341
|
+
if (extend) extendClass = document.createElement(extend).constructor;
|
|
1342
|
+
const pascalCaseName = kebabToPascal(tagName);
|
|
1343
|
+
if (!(DefinitionClass.prototype instanceof McBase)) {
|
|
1344
|
+
customElements.define(tagName, DefinitionClass);
|
|
1345
|
+
return DefinitionClass;
|
|
1346
|
+
}
|
|
1347
|
+
const McElementClass = createNamedClass(pascalCaseName, class extends Base(extendClass) {
|
|
1348
|
+
constructor(data) {
|
|
1349
|
+
super();
|
|
1350
|
+
DC.add(this, pascalCaseName, new DefinitionClass(this, data));
|
|
1351
|
+
}
|
|
1352
|
+
connectedCallback() {
|
|
1353
|
+
this.dispatchEvent(new CustomEvent("_mcConnected"));
|
|
1354
|
+
}
|
|
1355
|
+
connectedMoveCallback() {
|
|
1356
|
+
this.dispatchEvent(new CustomEvent("_mcConnectedMove"));
|
|
1357
|
+
}
|
|
1358
|
+
disconnectedCallback() {
|
|
1359
|
+
this.dispatchEvent(new CustomEvent("_mcDisconnected"));
|
|
1360
|
+
}
|
|
1361
|
+
adoptedCallback() {
|
|
1362
|
+
this.dispatchEvent(new CustomEvent("_mcAdopted"));
|
|
1363
|
+
}
|
|
1364
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1365
|
+
this.dispatchEvent(new CustomEvent("_mcAttributeChanged", { detail: {
|
|
1366
|
+
name,
|
|
1367
|
+
oldValue,
|
|
1368
|
+
newValue
|
|
1369
|
+
} }));
|
|
1370
|
+
}
|
|
1371
|
+
formResetCallback() {
|
|
1372
|
+
this.dispatchEvent(new CustomEvent("_mcFormReset"));
|
|
1373
|
+
}
|
|
1374
|
+
static get observedAttributes() {
|
|
1375
|
+
return [...observedAttributes];
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
customElements.define(tagName, McElementClass, { extends: extend });
|
|
1379
|
+
if (!defineCE.list) defineCE.list = [];
|
|
1380
|
+
defineCE.list.push(tagName);
|
|
1381
|
+
return McElementClass;
|
|
1382
|
+
}
|
|
1383
|
+
function getCE(tagName) {
|
|
1384
|
+
return customElements.get(tagName);
|
|
168
1385
|
}
|
|
169
1386
|
/**
|
|
170
1387
|
Returns an array of all tag names registered via {@link defineCE}.
|
|
@@ -177,6 +1394,8 @@ console.log(listCE());
|
|
|
177
1394
|
```
|
|
178
1395
|
@group Elements
|
|
179
1396
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
1397
|
+
function listCE() {
|
|
1398
|
+
return defineCE.list ?? [];
|
|
182
1399
|
}
|
|
1400
|
+
//#endregion
|
|
1401
|
+
export { defineCE, getCE, listCE };
|