@twobirds/microcomponents 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +2 -16
  2. package/dist/cli/bundle.d.ts +106 -0
  3. package/dist/cli/bundle.d.ts.map +1 -0
  4. package/dist/cli/bundler.d.ts +44 -0
  5. package/dist/cli/bundler.d.ts.map +1 -0
  6. package/dist/cli/create.d.ts.map +1 -1
  7. package/dist/cli/dev.d.ts +87 -0
  8. package/dist/cli/dev.d.ts.map +1 -0
  9. package/dist/cli/dispatch.d.ts +6 -2
  10. package/dist/cli/dispatch.d.ts.map +1 -1
  11. package/dist/cli/index.js +1807 -1
  12. package/dist/cli/inliner.d.ts +42 -0
  13. package/dist/cli/inliner.d.ts.map +1 -0
  14. package/dist/cli/preview.d.ts +48 -0
  15. package/dist/cli/preview.d.ts.map +1 -0
  16. package/dist/core/elements.js +1294 -75
  17. package/dist/core/mcGuards.js +5 -8
  18. package/dist/mcp/api.json +2 -17
  19. package/dist/mcp/concepts/project-layout.md +9 -8
  20. package/dist/mcp/index.js +450 -230
  21. package/dist/microcomponents.js +2882 -9
  22. package/dist/state/formValues.js +2236 -281
  23. package/dist/state/observables.js +1794 -342
  24. package/dist/utils/events.js +171 -197
  25. package/package.json +3 -7
  26. package/dist/cli/create.js +0 -653
  27. package/dist/cli/dispatch.js +0 -316
  28. package/dist/cli/scaffold.js +0 -109
  29. package/dist/cli/writeScaffold.js +0 -114
  30. package/dist/core/MC.js +0 -837
  31. package/dist/core/index.js +0 -72
  32. package/dist/state/index.js +0 -5
  33. package/dist/state/placeholders.js +0 -187
  34. package/dist/types/index.js +0 -1
  35. package/dist/types/mcTypes.js +0 -9
  36. package/dist/utils/autoload.js +0 -160
  37. package/dist/utils/helpers.js +0 -532
  38. package/dist/utils/index.js +0 -12
  39. package/dist/utils/resolveComponentPath.js +0 -39
  40. package/dist/vite/index.d.ts +0 -7
  41. package/dist/vite/index.d.ts.map +0 -1
  42. package/dist/vite/index.js +0 -6
  43. package/dist/vite/loadAssets.d.ts +0 -36
  44. package/dist/vite/loadAssets.d.ts.map +0 -1
  45. package/dist/vite/loadAssets.js +0 -221
package/dist/core/MC.js DELETED
@@ -1,837 +0,0 @@
1
- 'use strict';
2
- import { loadAssets, processAssets } from '../utils/helpers.js';
3
- import { trigger, on, one, off } from '../utils/events.js';
4
- import { isMcHost } from './mcGuards.js';
5
- export { isMcHost } from './mcGuards.js';
6
- function add(targetHTMLElement, key, element = {}) {
7
- const node = targetHTMLElement;
8
- // create the _mc property if necessary ('in' is tested on the unwidened
9
- // element — testing the McHost-cast alias would narrow it to never)
10
- if (!('_mc' in targetHTMLElement)) {
11
- node._mc = new DC(targetHTMLElement);
12
- }
13
- // the public registry type has no string index signature; the dynamic write
14
- // goes through a keyed view of the same object
15
- const _mc = node._mc;
16
- // add the element with the given property key to the _mc property
17
- if (key.length && !_mc[key]) {
18
- _mc[key] = element;
19
- // add attribute if HTMLElement
20
- if (targetHTMLElement instanceof HTMLElement && !targetHTMLElement.getAttribute('data-mc')) {
21
- targetHTMLElement.setAttribute('data-mc', '');
22
- }
23
- }
24
- return node;
25
- }
26
- /**
27
- Removes a named microcomponent from the internal `_mc` collection of a target DOM node.
28
-
29
- This function deletes the specified key from the element's `_mc` hash object and, if the
30
- collection becomes empty, removes the `data-mc` attribute from the DOM node.
31
-
32
- The bare poisoned value (`el._mc.gone` without member access) is `never`,
33
- which is assignable to anything — only member/method use of a removed entry is caught.
34
-
35
- @template T - A plain or already-enhanced element type.
36
- @template K - The literal registration key; its entry is poisoned in the registry type.
37
- @param target - The DOM element whose microcomponent should be removed.
38
- @param key - The property name/key corresponding to the microcomponent to remove.
39
- */
40
- function remove(target, key) {
41
- if (!key)
42
- return; // silent fail
43
- const node = target;
44
- // keyed view: the public registry type has no string index signature
45
- const _mc = node._mc;
46
- if (!_mc)
47
- return;
48
- // delete the property from the HTMLElement _mc collection
49
- if (_mc[key]) {
50
- delete _mc[key];
51
- }
52
- // remove the _mc attribute if the DOM collection is empty
53
- if (!Object.keys(_mc).length && target instanceof HTMLElement && target.hasAttribute('data-mc')) {
54
- target.removeAttribute('data-mc');
55
- }
56
- }
57
- /**
58
- Runs the "Init" lifecycle event on a DOM microcomponent instance asynchronously.
59
-
60
- This function schedules the `trigger('Init')` call in a microtask, ensuring
61
- that all synchronous initialization is complete before the lifecycle hook
62
- executes. Only `DC` instances have an Init lifecycle — the `McBase`
63
- constructor calls this exclusively behind an `instanceof DC` check, so the
64
- signature encodes that invariant instead of re-checking it at runtime.
65
-
66
- @param element - The DOM microcomponent instance on which to trigger the Init event.
67
- */
68
- function init(element) {
69
- queueMicrotask(() => {
70
- element.trigger('Init', null);
71
- });
72
- }
73
- /**
74
- Attaches event listeners to DOM elements based on method naming conventions.
75
-
76
- Scans the prototype of the provided element and attaches listeners for methods
77
- prefixed with `on` or `one`, corresponding to either native HTML events (e.g.,
78
- 'onclick') or microcomponent-prefixed custom events (e.g., '_mcUpdate').
79
-
80
- @param element - The instance whose prototype should be scanned for event handlers.
81
- */
82
- function autoAttachListeners(element) {
83
- const sourceTarget = element.target;
84
- const prototype = Object.getPrototypeOf(element);
85
- const sourceElement = element;
86
- const definedEventHandlerNames = Object.getOwnPropertyNames(prototype).filter((key) => /^(on[A-Z]|one[A-Z])/.test(key) && typeof prototype[key] === 'function');
87
- for (const eventHandlerName of definedEventHandlerNames) {
88
- const once = /^one/.test(eventHandlerName);
89
- const eventName = eventHandlerName.replace(/^one?/, ''); // preserves upper/lowercase for non-native events
90
- const listener = (ev) => sourceElement[eventHandlerName](ev);
91
- on(sourceTarget, eventName, listener, once);
92
- }
93
- }
94
- /**
95
- Retrieves immediate child microcomponents of a DOM node.
96
-
97
- Queries the DOM for descendant elements marked with `data-mc` that are direct
98
- children of the provided element and returns their associated `_mc` instances.
99
-
100
- @param domNode - The DOM element whose child microcomponents to find.
101
- @returns An array of child microcomponent instances.
102
- */
103
- function getMcChildNodes(domNode) {
104
- const selector = '[data-mc]:not(:scope [data-mc] [data-mc])';
105
- const childMcElements = Array.from(domNode.querySelectorAll(selector))
106
- .filter((e) => e instanceof HTMLElement && '_mc' in e)
107
- .map((e) => e._mc);
108
- return childMcElements;
109
- }
110
- /**
111
- Walks up the DOM from `from`, returning the `_mc` controller of the nearest
112
- `data-mc` ancestor — restricted, when `key` is given, to hosts whose registry
113
- actually carries that key. Hosts marked `data-mc` but not (yet) carrying an
114
- `_mc` record (e.g. autoload pending) are skipped rather than returned.
115
-
116
- @param from - The element whose ancestors to search (exclusive).
117
- @param key - Optional registry key the ancestor's `_mc` must contain.
118
- @returns The nearest matching controller, or null.
119
- */
120
- function findMcAncestor(from, key) {
121
- let el = from.parentElement?.closest('[data-mc]');
122
- while (el instanceof HTMLElement) {
123
- /* v8 ignore next */
124
- if ('_mc' in el) {
125
- const mc = el._mc;
126
- if (key === undefined || key in mc)
127
- return mc;
128
- }
129
- el = el.parentElement?.closest('[data-mc]');
130
- }
131
- return null;
132
- }
133
- /**
134
- The base class for all microcomponents — shared root of both {@link MC} and {@link DC}.
135
-
136
- Holds a `WeakRef` to the target (a DOM element for `DC`, or any plain object for `MC`)
137
- to avoid circular references. For `DC` subclasses it also bootstraps two automatic
138
- behaviours:
139
-
140
- 1. **Event listener auto-attachment** — methods whose names start with `on` or `one`
141
- (e.g. `onClick`, `oneInit`) are wired to the target element via {@link on} / {@link one}.
142
- 2. **"Init" lifecycle event** — dispatched asynchronously in a microtask so that all
143
- synchronous constructor work finishes first.
144
-
145
- Both mechanisms run only when the instance is a `DC` (checked with `instanceof DC`).
146
- Plain `MC` instances skip them entirely, keeping server-side usage lightweight.
147
-
148
- ### Plugin extension points
149
-
150
- The prototype of `McBase` is the attachment point for optional plugins that curry
151
- additional instance methods onto every microcomponent. Two built-in plugins modify
152
- this class at runtime:
153
-
154
- - **{@link State.observePlugin}** — adds `notify()`, `subscribe()`, and `bind()` so that
155
- observable properties can be controlled directly on the component instance with
156
- fluent chaining. Call `observePlugin(DC)` once at module level to activate.
157
-
158
- - **{@link State.formValuePlugin}** — adds `formValues()` for two-way binding between DOM
159
- form controls and an observable property. Also pulls in all methods from
160
- {@link State.observePlugin}, so you can chain `.subscribe()` and `.bind()` after
161
- `.formValues()`. Call `formValuePlugin(DC)` once at module level to activate.
162
-
163
- Both plugins are idempotent — calling them multiple times has no side effects. They
164
- work by assigning methods onto `McBase.prototype` (reached via
165
- `Object.getPrototypeOf(Class.prototype)`) and use TypeScript module augmentation
166
- (`declare module`) so the added signatures appear in type-checking automatically.
167
-
168
- @template TTarget - The concrete type of the wrapped target; preserved by the
169
- {@link McBase.target} getter.
170
-
171
- @example Extending with plugins at runtime
172
-
173
- ```ts
174
- import { DC, observePlugin, formValuePlugin } from '@twobirds/microcomponents';
175
-
176
- observePlugin(DC);
177
- formValuePlugin(DC);
178
-
179
- class MyForm extends DC {
180
- data = { name: '' };
181
-
182
- oneInit() {
183
- this
184
- .formValues(this.target, 'data')
185
- .subscribe('data', (v) => console.log(v))
186
- .bind('data', this.target);
187
- }
188
- }
189
- ```
190
-
191
- `observePlugin(DC)` adds `notify()`, `subscribe()`, `bind()` to `McBase.prototype`;
192
- `formValuePlugin(DC)` adds `formValues()` and re-runs `observePlugin`. `oneInit()`
193
- runs once after the constructor.
194
- @group Micro Components
195
- */
196
- class McBase {
197
- _target; // avoid circular reference
198
- /**
199
- The constructor for the base `McBase` class.
200
-
201
- Initializes a weak reference to the target and triggers auto-attachment
202
- of listeners and initialization lifecycle events.
203
-
204
- @param target - The object or element this microcomponent represents.
205
- */
206
- constructor(target) {
207
- this._target = new WeakRef(target || {});
208
- if (this instanceof DC && this.constructor !== DC) {
209
- autoAttachListeners(this);
210
- const tag = this.constructor.tag;
211
- if (tag === undefined || tag.toUpperCase() === this.target.tagName) {
212
- init(this);
213
- }
214
- }
215
- }
216
- /**
217
- Returns the current target object (DOM node or other data) referenced by this microcomponent.
218
-
219
- Uses a `WeakRef` internally to avoid circular references, and safely dereferences it here.
220
-
221
- The dereference is asserted non-null: the controller is stored on its target
222
- (`element._mc`), so any code that can reach the controller through the element is
223
- holding the target alive. Only a controller retained independently after its element
224
- was garbage-collected could observe a stale reference — do not cache controllers
225
- detached from their elements.
226
-
227
- @returns The target object.
228
- */
229
- get target() {
230
- return this._target.deref();
231
- }
232
- }
233
- /**
234
- A general-purpose base microcomponent that does not require a DOM target.
235
-
236
- Inherits from `McBase`, but is primarily used as a lightweight container or
237
- for logic not directly tied to DOM elements.
238
-
239
- @template TTarget - The concrete type of the wrapped target data; preserved by
240
- the `target` getter, so `new MC({ some: 'data' }).target.some` stays typed.
241
- @group Micro Components
242
- */
243
- class MC extends McBase {
244
- /**
245
- Constructs an instance of the general-purpose microcomponent.
246
-
247
- @param target - Target object or data.
248
- */
249
- constructor(target) {
250
- super(target);
251
- }
252
- }
253
- /**
254
- @class DC
255
- ---
256
-
257
- A DOM-centric microcomponent that operates on a specific HTML element.
258
-
259
- Extends {@link McBase} and associates itself with an actual DOM node via the
260
- `_target` reference. Provides methods for event handling, DOM traversal,
261
- and interaction with sibling or ancestor microcomponents.
262
-
263
- ---
264
-
265
- ## Static Configuration Properties
266
-
267
- DC classes support three static properties that configure custom elements
268
- before any instance is constructed:
269
-
270
- ### `static tag?: string`
271
-
272
- Replaces the default target element with a semantic HTML element, or removes it entirely.
273
-
274
- - **Non-empty string** (e.g., `'header'`) — the original custom element is replaced
275
- with the specified built-in element. The original tag name is added as a CSS class.
276
- - **Empty string `''`** — the target element is removed from the DOM entirely.
277
- Useful for components that only inject global styles or act as style containers.
278
-
279
- @example static tag — replace with semantic element
280
-
281
- ```ts
282
- class ShopHeader extends DC {
283
- static tag = 'header';
284
- }
285
- defineCE('shop-header', ShopHeader);
286
- ```
287
-
288
- Produces `<header class="shop-header">` instead of `<shop-header>`.
289
-
290
- @example static tag — remove element entirely
291
-
292
- ```ts
293
- class GlobalStyles extends DC {
294
- static tag = '';
295
- }
296
- defineCE('global-styles', GlobalStyles);
297
- ```
298
-
299
- The element self-destructs; only side effects (e.g., injected styles) remain.
300
-
301
- ### `static assets: string[]`
302
-
303
- Declares companion files for auto-loading by convention. Asset names are camelCase
304
- and translated into file extensions: `'targetHtml'` → `.target.html`, `'json'` → `.json`.
305
-
306
- The path is derived from the custom element tag segments, and the basename is the
307
- last segment. For `<cart-container-checkout>`, assets load from `/cart/container/checkout.*`.
308
-
309
- When assets finish loading, an `'assets'` event fires so that `oneAssets(ev)` can
310
- process them. Everything ending in `Json` arrives parsed; all other files arrive as text.
311
-
312
- **One-time global styles**: Assets named `'css'` and `'targetCss'` inject CSS into the
313
- global document scope via `injectCSS(document, ...)`. Because these are idempotent
314
- global side effects, they are processed only once — after the first instance loads them,
315
- the entries are removed from both `static assets` and `static _assets`. Subsequent
316
- instances skip loading and re-processing them entirely.
317
-
318
- **CSS file roles**:
319
-
320
- - `.css` — design tokens only (CSS custom-property presets). Injected into the document
321
- scope once, like `targetCss`.
322
- - `.target.css` — layout rules for the light DOM. Also document-scoped and one-time.
323
- - `.shadow.css` — layout rules for the shadow DOM. Injected per instance into the
324
- component's own shadow root, so each instance gets its own copy.
325
-
326
- Every declared asset — CSS, HTML, JSON — is inlined into `static _assets` at build time by
327
- the load-assets plugin, so a bundled build performs no fetches at all.
328
-
329
- In a bundled build the plain `.css` files are additionally concatenated into a single
330
- `bundle.css` by the bundling script. That file is a convenience listing of the available
331
- variables: copy the ones you want to change into your own stylesheet and override them
332
- there. It is not required at runtime, and it cannot drift from the inlined presets — both
333
- are read from the same source files.
334
-
335
- The runtime decides whether an asset still needs fetching with `!ctor._assets[key]`, so an
336
- inlined entry must never be an empty string: that would trigger a re-fetch of a file that
337
- does not exist in a bundled build.
338
-
339
- @example static assets with autoload
340
-
341
- ```ts
342
- class CartCheckout extends DC {
343
- static assets = ['targetHtml', 'targetCss', 'configJson'];
344
-
345
- oneAssets(ev) {
346
- const { configJson } = ev.detail.data;
347
- console.log(configJson);
348
- }
349
- }
350
- defineCE('cart-checkout', CartCheckout);
351
- ```
352
-
353
- `configJson` arrives already parsed into an object.
354
-
355
- ### `static attributes?: object`
356
-
357
- Sets HTML attributes on every instance automatically.
358
-
359
- @example static attributes
360
-
361
- ```ts
362
- class ProductCard extends DC {
363
- static attributes = { role: 'listitem' };
364
- }
365
- defineCE('product-card', ProductCard);
366
- ```
367
-
368
- Every `<product-card>` carries `role="listitem"`.
369
-
370
- ### Combined usage
371
-
372
- All three can be combined in one class:
373
-
374
- @example All static properties together
375
-
376
- ```ts
377
- class ShopCatalog extends DC {
378
- static tag = 'section';
379
- static attributes = { role: 'list', 'aria-label': 'products' };
380
- static assets = ['targetHtml', 'targetCss'];
381
- }
382
- defineCE('shop-catalog', ShopCatalog);
383
- ```
384
-
385
- Produces `<section role="list" aria-label="products">`.
386
-
387
- ---
388
-
389
- ## Event Handler Conventions
390
-
391
- Methods whose names start with `on` or `one` are automatically wired as event listeners
392
- on the target element. The prefix determines persistence:
393
-
394
- - **`on<EventName>`** — persistent listener (fires every time)
395
- - **`one<EventName>`** — one-time listener (fires once, then detaches)
396
-
397
- ### Web Component Lifecycle Events
398
-
399
- When a DC is registered via {@link defineCE}, the custom element dispatches these
400
- internal events that can be caught with handler methods:
401
-
402
- | Method | When it fires |
403
- |--------|---------------|
404
- | `onConnected()` | Element was inserted into the DOM (`connectedCallback`) |
405
- | `onDisconnected()` | Element was removed from the DOM (`disconnectedCallback`) |
406
- | `onAdopted()` | Element was moved to a new document (`adoptedCallback`) |
407
- | `onAttributeChanged()` | An observed attribute changed (`attributeChangedCallback`) |
408
- | `oneInit()` | Constructor finished; all synchronous setup complete (microtask) |
409
- | `oneAssets()` | Constructor finished; all assets loaded and available; ev contains assets object |
410
-
411
- @example Event handlers in action
412
-
413
- ```ts
414
- class MyElement extends DC {
415
- onConnected() {
416
- console.log('entered the DOM');
417
- }
418
-
419
- onDisconnected() {
420
- console.log('left the DOM');
421
- }
422
-
423
- onClick() {
424
- this.trigger('something', 'payload');
425
- }
426
-
427
- oneSomething(ev) {
428
- console.log('first time only:', ev.detail.data);
429
- }
430
- }
431
- defineCE('my-element', MyElement);
432
- ```
433
-
434
- ---
435
-
436
- ## Construction Patterns
437
-
438
- ### Template usage (declarative)
439
-
440
- ```ts
441
- class MyElement extends DC {}
442
- defineCE('my-element', MyElement);
443
- ```
444
-
445
- The registered element is then used in HTML as `<my-element></my-element>`.
446
-
447
- ### Programmatic construction with data
448
-
449
- When constructing programmatically via {@link getCE}, pass only the `data` payload —
450
- the target element is created automatically by the custom element constructor.
451
-
452
- @example Programmatic construction
453
-
454
- ```ts
455
- class MyElement extends DC {
456
- data = {};
457
-
458
- constructor(target, data) {
459
- super(target);
460
- this.data = data;
461
- }
462
- }
463
- defineCE('my-element', MyElement);
464
-
465
- const Ctor = getCE('my-element');
466
- document.body.append(new Ctor({ name: 'John' }));
467
- ```
468
-
469
- ### Data from HTML attributes
470
-
471
- @example Reading data from an attribute
472
-
473
- ```ts
474
- class MyElement extends DC {
475
- data = {};
476
-
477
- constructor(target) {
478
- super(target);
479
- this.data = JSON.parse(target.getAttribute('my-data'));
480
- }
481
- }
482
- defineCE('my-element', MyElement);
483
- ```
484
-
485
- Used in HTML as `<my-element my-data='{"name":"John"}'></my-element>`.
486
-
487
- ---
488
-
489
- ## Plugin Extension Points
490
-
491
- The prototype of `DC` (inherited from {@link McBase}) is extended at runtime by optional
492
- plugins. See the {@link McBase} documentation for details on {@link State.observePlugin} and
493
- {@link State.formValuePlugin}.
494
-
495
- @template T - The concrete element type of the host element.
496
- @template EM - The component's custom event map (`event name → payload type`).
497
- Declaring it registers events on the type level: `trigger()` enforces the
498
- payload for registered names, and `on()`/`one()` handlers receive
499
- `CustomEvent<McDetail<Payload>>` with no annotation needed. Unregistered
500
- names keep the open `unknown`-payload behavior.
501
-
502
- @example Typed event map
503
-
504
- ```ts
505
- class Cart extends DC<HTMLFormElement, { CartUpdate: { count: number } }> {
506
- checkout() {
507
- this.trigger('CartUpdate', { count: 2 }, 'u');
508
- this.on('CartUpdate', (ev) => ev.detail.data.count);
509
- }
510
- }
511
- ```
512
-
513
- `trigger()` checks the payload against the registered `CartUpdate` shape, and the `on()`
514
- handler receives a fully typed `ev`.
515
- @group Micro Components
516
- */
517
- class DC extends McBase {
518
- /**
519
- * Replaces the target element with a semantic HTML tag, or removes it entirely.
520
- * Set to `'header'` to swap `<my-element>` for `<header>`, or `''` to self-destruct.
521
- */
522
- static tag;
523
- /**
524
- * Result container for assets (e.g., `['targetHtml', 'json']`).
525
- * Asset names (keys) are camelCase and translated into file extensions: `'targetHtml'` → `.target.html`.
526
- */
527
- static _assets = {};
528
- /**
529
- * Declares companion files for auto-loading by convention (e.g., `['targetHtml', 'json']`).
530
- * Asset names are camelCase and translated into file extensions: `'targetHtml'` → `.target.html`.
531
- */
532
- static assets = [];
533
- /**
534
- * In-flight asset request, cached per class so that instances constructed while
535
- * the first fetch is still pending (e.g. in a loop) share a single request
536
- * instead of each triggering their own. Resolves to the freshly loaded assets.
537
- */
538
- static _assetsPromise;
539
- /**
540
- * Sets HTML attributes on every instance automatically.
541
- * Example: `{ role: 'listitem', 'aria-label': 'nav' }`.
542
- */
543
- static attributes = {};
544
- /**
545
- * Constructs a new DC instance and applies static configuration.
546
- *
547
- * The constructor performs these steps in order:
548
- * 1. Calls {@link McBase} to establish the target reference and auto-attach listeners.
549
- * 2. If `static tag` is set and differs from the current element, replaces the target
550
- * with a new built-in element (or removes it if `tag === ''`).
551
- * 3. If `static assets` has entries, initiates asynchronous loading, fires an `'assets'` event when complete handled by `oneAssets(ev)`.
552
- * 4. Applies any `static attributes` to the target element.
553
- *
554
- * @param target - The host DOM element this component operates on.
555
- */
556
- constructor(target) {
557
- super(target);
558
- const ctor = this.constructor;
559
- // do not run on '_mc' property
560
- if (ctor === DC)
561
- return;
562
- const tagName = this.target.tagName.toLowerCase();
563
- // work on tag:
564
- if (ctor['tag'] !== undefined && tagName !== ctor.tag && ctor.tag !== '') {
565
- const element = document.createElement(ctor.tag);
566
- element.classList.add(tagName);
567
- ctor._originalTag = tagName;
568
- DC.add(element, ctor.name, new ctor(element));
569
- delete ctor._originalTag;
570
- if (tagName.includes('-') && !this.target.isConnected) {
571
- this.on('connected', () => this.target.replaceWith(element));
572
- }
573
- else {
574
- this.target.replaceWith(element);
575
- }
576
- return;
577
- }
578
- // work on attributes
579
- const attributes = ctor.attributes;
580
- if (ctor.attributes) {
581
- Object.keys(ctor.attributes).forEach((key) => {
582
- this.target.setAttribute(key, attributes[key]);
583
- });
584
- }
585
- /*
586
- work on assets
587
- */
588
- // oxlint-disable-next-line typescript/no-this-alias
589
- const that = this;
590
- // assets not yet present in the class-level cache still need fetching
591
- const remainingAssets = ctor.assets.filter((key) => !ctor._assets[key]);
592
- // apply any assets already cached (preloaded by the build plugin, or loaded
593
- // by an earlier instance) to THIS instance's target
594
- if (Object.keys(ctor._assets).length) {
595
- processAssets(this, ctor._assets);
596
- }
597
- if (remainingAssets.length) {
598
- // fetch once per class: the first instance starts the request and every
599
- // instance created while it is still in flight (e.g. in a loop) reuses the
600
- // same promise instead of firing a duplicate fetch
601
- if (!ctor._assetsPromise) {
602
- const elementName = ctor._originalTag || tagName;
603
- ctor._assetsPromise = loadAssets(elementName, remainingAssets).then((res) => {
604
- const loadedAssets = {};
605
- remainingAssets.forEach((key, i) => {
606
- loadedAssets[key] = res[i];
607
- });
608
- ctor._assets = Object.assign({}, ctor._assets, loadedAssets);
609
- // one-time global styles: remove 'css' and 'targetCss' from both assets and _assets
610
- for (const onceKey of ['css', 'targetCss']) {
611
- if (loadedAssets[onceKey] !== undefined) {
612
- delete ctor._assets[onceKey];
613
- }
614
- }
615
- ctor.assets = ctor.assets.filter((key) => key !== 'css' && key !== 'targetCss');
616
- return loadedAssets;
617
- });
618
- }
619
- // each instance applies the freshly loaded assets to its own target,
620
- // then fires the assets event with the merged cache
621
- ctor._assetsPromise.then((loadedAssets) => {
622
- processAssets(this, loadedAssets);
623
- that.trigger('assets', ctor._assets);
624
- });
625
- }
626
- else if ([...Object.keys(ctor._assets)].length) {
627
- queueMicrotask(() => that.trigger('assets', ctor._assets));
628
- }
629
- // remove empty tag
630
- if (ctor.tag === '') {
631
- this.target.remove();
632
- return;
633
- }
634
- }
635
- /**
636
- Returns the loaded assets for this component instance.
637
-
638
- Assets are loaded asynchronously when `static assets` is defined on the class.
639
- This method returns the resolved asset map keyed by filename, or `undefined`
640
- if assets have not yet been loaded.
641
-
642
- @returns A record mapping asset keys to their content strings, or undefined.
643
- */
644
- getAssets() {
645
- return this.constructor._assets || {};
646
- }
647
- /**
648
- Static helper to attach a microcomponent instance to a DOM element.
649
-
650
- Wrapper around the `add()` utility function, providing a class-level API
651
- for adding child microcomponents to a target node.
652
-
653
- @template T - The concrete element type of the target; preserved in the return type.
654
- @template K - The literal registration key; becomes a known entry of the returned `_mc` registry.
655
- @template E - The type of the attached element; retrievable as `result._mc[key]`.
656
- @param target - The DOM element to which to attach the microcomponent.
657
- @param key - The property name/key under which to store the instance in `_mc`.
658
- @returns The target DOM element with the attached microcomponent.
659
- */
660
- static add(target, key, element = {}) {
661
- return add(target, key, element);
662
- }
663
- /**
664
- Static helper to remove a named microcomponent from a DOM element.
665
-
666
- Wrapper around the `remove()` utility function, providing a class-level API
667
- for removing child microcomponents.
668
-
669
- @template T - A plain or already-enhanced element type.
670
- @template K - The literal registration key; its entry is poisoned in the registry type.
671
- @param target - The DOM element from which to remove the microcomponent.
672
- @param key - The property name/key corresponding to the microcomponent to remove.
673
- */
674
- static remove(target, key) {
675
- remove(target, key);
676
- }
677
- /**
678
- Class-verified entry lookup: returns the component stored under `key` on the
679
- element's `_mc` record — but only if it is an instance of `Klass`.
680
-
681
- This is the *sound* retrieval primitive: presence AND type identity are both
682
- proven at runtime (`instanceof`), so it needs no global `McRegistry`
683
- declaration and stays correct even when different elements use the same key
684
- name for different component types. Accepts any value (including
685
- `querySelector` results that may be null) — every failure mode returns
686
- `undefined`.
687
-
688
- @template E - The component type, inferred from `Klass`.
689
- @param element - The value to look the entry up on; anything non-host yields undefined.
690
- @param key - The registry key name to look under.
691
- @param elementMcClass - The class the entry must be an instance of.
692
- @returns The verified instance, or undefined.
693
- @example
694
- ```ts
695
- const cart = DC.get(document.querySelector('form'), 'cart', Cart);
696
- cart?.addItem(item);
697
- ```
698
-
699
- `cart` is `Cart | undefined` — presence and type identity are both proven by the
700
- `instanceof` check inside `get()`, with no global declaration involved.
701
- */
702
- static get(element, key, elementMcClass) {
703
- if (!isMcHost(element))
704
- return undefined;
705
- const entry = element._mc[key];
706
- return entry instanceof elementMcClass ? entry : undefined;
707
- }
708
- /**
709
- Detaches an event listener from the DOM node.
710
-
711
- This is an instance method wrapper for `off()`, using the internal `_target` DOM reference.
712
-
713
- @param eventName - The name of the event (e.g., 'click', 'update').
714
- @param handler - The previously attached callback function or object to remove.
715
- */
716
- off(eventName, handler) {
717
- off(this.target, eventName, handler);
718
- }
719
- /**
720
- Attaches a persistent event listener to the DOM node.
721
-
722
- This is an instance method wrapper for `on()`, using the internal `_target` DOM reference.
723
-
724
- The handler receives a `CustomEvent<McDetail<Payload>>`. For event names
725
- registered in the component's event map `EM` the payload is resolved from the
726
- map — an unannotated arrow handler is fully typed. For unregistered names the
727
- payload defaults to `unknown` and can be pinned via the type argument
728
- (`on<Payload>('Name', handler)`) or the handler's own annotation.
729
-
730
- @param eventName - The name of the event (e.g., 'click', 'update').
731
- @param handler - The callback function or object to invoke.
732
- @returns The attached handler reference, unchanged — usable directly with {@link off}.
733
- */
734
- on(eventName, handler) {
735
- return on(this.target, eventName, handler);
736
- }
737
- /**
738
- Attaches a one-time event listener to the DOM node.
739
-
740
- This is an instance method wrapper for `one()`, using the internal `_target` DOM reference.
741
- Payload typing follows the same rules as {@link DC.on}.
742
-
743
- @param eventName - The name of the event (e.g., 'click', 'update').
744
- @param handler - The callback function or object to invoke once.
745
- @returns The attached one-time handler reference, unchanged — usable directly with {@link off}.
746
- */
747
- one(eventName, handler) {
748
- return one(this.target, eventName, handler);
749
- }
750
- /**
751
- Triggers a custom event on the DOM node and optionally bubbles it.
752
-
753
- This is an instance method wrapper for `trigger()`, using the internal `_target` DOM reference.
754
-
755
- For event names registered in the component's event map `EM` the payload type
756
- is enforced; unregistered names accept any payload (typed via inference or an
757
- explicit `trigger<Payload>(...)` argument).
758
-
759
- @param eventName - The name of the event (will be prefixed if needed).
760
- @param data - Additional data payload attached to `event.detail.data`.
761
- @param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
762
- @param controller - Optional controller for managing bubbling behavior.
763
- */
764
- trigger(eventName, data = {}, bubble = 'l', controller) {
765
- trigger(this.target, eventName, data, bubble, controller);
766
- }
767
- /**
768
- Returns an array of all ancestor microcomponents up the DOM tree.
769
-
770
- Traverses upward from this element's target, collecting the `_mc` instances
771
- of ancestor elements marked with `data-mc`. The keyed form only collects
772
- hosts whose registry carries `key`, typed from the global {@link McRegistry}
773
- binding — non-optional, since presence is checked during the walk.
774
-
775
- @template R - Known registry entries of the results; pass a record to assert
776
- a site-specific shape: `ancestors<{ cart: Cart }>()`. Defaults to none.
777
- @returns An array of ancestor microcomponent instances (empty if none found).
778
- */
779
- ancestors(key) {
780
- const result = [];
781
- let current = findMcAncestor(this.target, key);
782
- while (current) {
783
- result.push(current);
784
- current = findMcAncestor(current.target, key);
785
- }
786
- return result;
787
- }
788
- /**
789
- Returns the closest ancestor microcomponent or null if none found.
790
-
791
- This method is equivalent to calling `ancestors()[0]`, but more concise.
792
- The keyed form walks up to the nearest host whose registry carries `key`,
793
- typed from the global {@link McRegistry} binding — non-optional, since
794
- presence is checked during the walk.
795
-
796
- @template R - Known registry entries of the result; pass a record to assert
797
- a site-specific shape: `closest<{ cart: Cart }>()?.cart.addItem(...)`.
798
- Defaults to none.
799
- @returns The nearest matching ancestor's `_mc`, or null if not found.
800
- */
801
- closest(key) {
802
- return findMcAncestor(this.target, key);
803
- }
804
- /**
805
- Returns an array of direct child microcomponents.
806
-
807
- Calls `getMcChildNodes()` internally to find immediate descendants only,
808
- excluding nested grandchildren and deeper levels. The keyed form filters to
809
- hosts whose registry carries `key`; see {@link DC.closest}.
810
-
811
- @template R - Known registry entries of the results; see {@link DC.closest}.
812
- @returns An array of child microcomponent instances (empty if none found).
813
- */
814
- children(key) {
815
- const kids = getMcChildNodes(this.target);
816
- return key === undefined ? kids : kids.filter((mc) => key in mc);
817
- }
818
- /**
819
- Returns an array of all descendant microcomponents recursively.
820
-
821
- Uses a `querySelectorAll` query to collect every nested element marked with
822
- `data-mc`, returning their associated `_mc` instances. The keyed form
823
- filters to hosts whose registry carries `key`; see {@link DC.closest}.
824
-
825
- @template R - Known registry entries of the results; see {@link DC.closest}.
826
- @returns An array of descendant microcomponent instances (empty if none found).
827
- */
828
- descendants(key) {
829
- const hosts = Array.from(this.target.querySelectorAll('[data-mc]'));
830
- const mcArray = hosts
831
- .filter((e) => e instanceof HTMLElement && '_mc' in e)
832
- .map((e) => e._mc)
833
- .filter((mc) => key === undefined || key in mc);
834
- return mcArray;
835
- }
836
- }
837
- export { MC, DC, McBase };