@twobirds/microcomponents 0.1.21 → 0.2.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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -92
  3. package/dist/MC.min.js +1 -345
  4. package/dist/autoload.min.js +1 -0
  5. package/dist/core/MC.d.ts +401 -0
  6. package/dist/core/MC.d.ts.map +1 -0
  7. package/dist/core/MC.js +471 -0
  8. package/dist/core/autoload.d.ts +54 -0
  9. package/dist/core/autoload.d.ts.map +1 -0
  10. package/dist/core/autoload.js +158 -0
  11. package/dist/core/elements.d.ts +232 -0
  12. package/dist/core/elements.d.ts.map +1 -0
  13. package/dist/core/elements.js +123 -0
  14. package/dist/core/events.d.ts +231 -0
  15. package/dist/core/events.d.ts.map +1 -0
  16. package/dist/core/events.js +228 -0
  17. package/dist/core/mcGuards.d.ts +49 -0
  18. package/dist/core/mcGuards.d.ts.map +1 -0
  19. package/dist/core/mcGuards.js +19 -0
  20. package/dist/elements.min.js +1 -1
  21. package/dist/events.min.js +1 -0
  22. package/dist/formValues.min.js +1 -1
  23. package/dist/mcGuards.min.js +1 -0
  24. package/dist/microcomponents.d.ts +8 -5
  25. package/dist/microcomponents.d.ts.map +1 -1
  26. package/dist/microcomponents.js +8 -5
  27. package/dist/microcomponents.min.js +1 -1
  28. package/dist/observables.min.js +1 -1
  29. package/dist/state/formValues.d.ts +70 -0
  30. package/dist/state/formValues.d.ts.map +1 -0
  31. package/dist/state/formValues.js +334 -0
  32. package/dist/state/observables.d.ts +276 -0
  33. package/dist/state/observables.d.ts.map +1 -0
  34. package/dist/state/observables.js +382 -0
  35. package/dist/state/placeholders.d.ts +62 -0
  36. package/dist/state/placeholders.d.ts.map +1 -0
  37. package/dist/state/placeholders.js +181 -0
  38. package/dist/types/mcTypes.d.ts +201 -0
  39. package/dist/types/mcTypes.d.ts.map +1 -0
  40. package/dist/types/mcTypes.js +9 -0
  41. package/dist/utils/helpers.d.ts +195 -0
  42. package/dist/utils/helpers.d.ts.map +1 -0
  43. package/dist/utils/helpers.js +340 -0
  44. package/package.json +96 -62
  45. package/dist/MC.d.ts +0 -31
  46. package/dist/MC.d.ts.map +0 -1
  47. package/dist/MC.js +0 -249
  48. package/dist/MC.ts +0 -613
  49. package/dist/MicroComponents.svg +0 -15
  50. package/dist/elements.d.ts +0 -5
  51. package/dist/elements.d.ts.map +0 -1
  52. package/dist/elements.js +0 -49
  53. package/dist/elements.ts +0 -114
  54. package/dist/events.d.ts +0 -11
  55. package/dist/events.d.ts.map +0 -1
  56. package/dist/events.js +0 -55
  57. package/dist/events.ts +0 -158
  58. package/dist/formValues.d.ts +0 -3
  59. package/dist/formValues.d.ts.map +0 -1
  60. package/dist/formValues.js +0 -156
  61. package/dist/formValues.ts +0 -371
  62. package/dist/helpers.d.ts +0 -15
  63. package/dist/helpers.d.ts.map +0 -1
  64. package/dist/helpers.js +0 -113
  65. package/dist/helpers.ts +0 -219
  66. package/dist/main.mjs +0 -1
  67. package/dist/microcomponents.ts +0 -6
  68. package/dist/observables.d.ts +0 -11
  69. package/dist/observables.d.ts.map +0 -1
  70. package/dist/observables.js +0 -228
  71. package/dist/observables.ts +0 -449
@@ -0,0 +1,471 @@
1
+ /**
2
+ @module
3
+ ## Overview
4
+
5
+ MicroComponents (MC) is a lightweight pattern for creating self-contained, event-driven, and composable UI components. It provides two core base classes: {@link MC} and {@link DC}, which differ primarily in their target attachment strategy and runtime environment requirements.
6
+
7
+ In general, your code extends from either of these classes depending on whether you need DOM interaction or pure logic encapsulation.
8
+
9
+ ---
10
+
11
+ ## MC vs DC: Server vs Client
12
+
13
+ The key distinction between the two base classes is their execution context:
14
+
15
+ - **MC (MicroComponent)** – A general-purpose component that works in **any JavaScript environment**, including server-side runtimes (Node.js, Deno, etc.). It does not require a DOM and can be used for business logic, state management, or as a lightweight container. The `target` parameter is optional.
16
+
17
+ - **DC (DOM Component)** – A DOM-centric component that operates wherever a real ( browser ) or simulated ( jsdom, etc. ) DOM exists. It attaches itself to an {@link HTMLElement} and provides event handling, DOM traversal, and lifecycle management. The `target` parameter is required and must be an HTMLElement.
18
+
19
+ **HINT:** DCs can be attached to **any** DOM node ( HTMLElement ). That is you can connect them to <div>, <span> and the like. With a trick you can even define custom elements that convert themselves to e.g. a plain <div>.
20
+
21
+ ### When to use which?
22
+
23
+ | Scenario | Recommended Class |
24
+ |----------|-------------------|
25
+ | Server-side rendering or logic | MC |
26
+ | Pure data processing without DOM | MC |
27
+ | Interactive UI elements in the browser | DC |
28
+ | Event-driven components attached to HTML nodes | DC |
29
+
30
+ ---
31
+
32
+ ## The `target` Property: Backreference Concept
33
+
34
+ Both MC and DC store a reference to their **host object** via the internal `_target` property (exposed as the `target` getter). This is a *backreference* — it points back to the object that contains or hosts this component instance.
35
+
36
+ - For **DC instances**, `this.target` refers to the {@link HTMLElement} in the DOM (or testing DOM such as jsdom) to which the component is attached.
37
+ - For **MC instances**, `this.target` can be any arbitrary object, allowing flexible composition patterns.
38
+
39
+ The reference is held via a `WeakRef` ([MDN WeakRef](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef)) internally to avoid circular references and prevent memory leaks.
40
+
41
+ ---
42
+
43
+ ## DOM Integration: The `_mc` Property and `data-mc` Attribute
44
+
45
+ When a DC is attached to an element via {@linkcode DC.add}, two things happen:
46
+
47
+ 1. The element gains a `_mc` property (of type {@link McHost}) that holds references to all attached microcomponents, keyed by their unique identifiers.
48
+ 2. The element receives a `data-mc` attribute, signaling that it hosts at least one microcomponent. This attribute is used internally for DOM queries and selectors.
49
+
50
+ This `_mc` property is the **only** interference with the native DOM structure, ensuring compatibility with other frameworks or libraries.
51
+
52
+ ---
53
+
54
+ ## Lifecycle & Automatic Event Handler Attachment
55
+
56
+ DC instances automatically trigger an `"Init"` lifecycle event asynchronously after construction. Additionally, methods prefixed with `on` or `one` (e.g., `onClick`, `oneUpdate`) are auto-attached as event listeners on the target element, supporting both native HTML events and custom microcomponent events.
57
+
58
+ ---
59
+
60
+
61
+ @example Understanding target in DC
62
+
63
+ javascript:
64
+ ```javascript
65
+ import { DC } from '@twobirds/microcomponents';
66
+
67
+ class MyButton extends DC {
68
+ constructor(target: HTMLElement) {
69
+ super(target);
70
+ }
71
+
72
+ logTarget() {
73
+ // this.target is the <button> element itself
74
+ console.log(this.target.tagName); // "BUTTON"
75
+ }
76
+ }
77
+
78
+ const btn = document.querySelector('button');
79
+ if (btn) {
80
+ const host = DC.add(btn, 'myButton', new MyButton(btn));
81
+ host._mc.myButton.logTarget();
82
+ }
83
+ ```
84
+
85
+ @example Understanding target in MC
86
+
87
+
88
+ ```javascript
89
+ import { MC } from '@twobirds/microcomponents';
90
+
91
+ class Counter extends MC {
92
+ private count = 0;
93
+
94
+ constructor(target?: object) {
95
+ super(target);
96
+ }
97
+
98
+ increment() {
99
+ this.count++;
100
+ }
101
+
102
+ getCount() {
103
+ return this.count;
104
+ }
105
+ }
106
+
107
+ // Works without any DOM — usable on server or client
108
+ const counter = new Counter();
109
+ counter.increment();
110
+ console.log(counter.getCount()); // 1
111
+ ```
112
+
113
+ @example Usage
114
+
115
+ Below is a complete example demonstrating how to create, attach, and interact with a custom microcomponent:
116
+
117
+ html before:
118
+ ```html
119
+ <div id="container"></div>
120
+ ```
121
+
122
+ javascript:
123
+ ```javascript
124
+ import { DC } from '@twobirds/microcomponents';
125
+
126
+ // 1. Create a custom micro component class extending DC
127
+ class HelloWorld extends DC {
128
+ constructor(target: HTMLElement) {
129
+ super(target);
130
+ }
131
+
132
+ greet() {
133
+ console.log('Hello World');
134
+ }
135
+ }
136
+
137
+ // 2. Get an element from the DOM
138
+ const element = document.getElementById('container')!;
139
+
140
+ // 3. Attach the micro component instance to the target element
141
+ DC.add(element, 'helloWorld', new HelloWorld(element));
142
+
143
+ // 4. Invoke methods on the attached component
144
+ element._mc.helloWorld.greet(); // "Hello World"
145
+ ```
146
+
147
+ html after:
148
+ ```html
149
+ <div id="container" data-mc></div>
150
+ ```
151
+
152
+ The `data-mc` attribute indicates that this element contains at least one microcomponent. The `_mc` property provides direct programmatic access to the attached instances.
153
+
154
+ ---
155
+
156
+ */
157
+ 'use strict';
158
+ import { trigger, on, one, off } from './events.js';
159
+ import { isMcDOM } from './mcGuards.js';
160
+ export { isMcDOM } from './mcGuards.js';
161
+ function add(targetHTMLElement, key, element = {}) {
162
+ const node = targetHTMLElement;
163
+ // create the _mc property if necessary ('in' is tested on the unwidened
164
+ // element — testing the McHost-cast alias would narrow it to never)
165
+ if (!('_mc' in targetHTMLElement)) {
166
+ node._mc = new DC(targetHTMLElement);
167
+ }
168
+ // the public registry type has no string index signature; the dynamic write
169
+ // goes through a keyed view of the same object
170
+ const _mc = node._mc;
171
+ // add the element with the given property key to the _mc property
172
+ if (key.length && !_mc[key]) {
173
+ _mc[key] = element;
174
+ // add attribute if HTMLElement
175
+ if (targetHTMLElement instanceof HTMLElement && !targetHTMLElement.getAttribute('data-mc')) {
176
+ targetHTMLElement.setAttribute('data-mc', '');
177
+ }
178
+ }
179
+ return node;
180
+ }
181
+ /**
182
+ Removes a named microcomponent from the internal `_mc` collection of a target DOM node.
183
+
184
+ This function deletes the specified key from the element's `_mc` hash object and, if the
185
+ collection becomes empty, removes the `data-mc` attribute from the DOM node.
186
+
187
+ The bare poisoned value (`el._mc.gone` without member access) is `never`,
188
+ which is assignable to anything — only member/method use of a removed entry is caught.
189
+
190
+ @template T - A plain or already-enhanced element type.
191
+ @template K - The literal registration key; its entry is poisoned in the registry type.
192
+ @param target - The DOM element whose microcomponent should be removed.
193
+ @param key - The property name/key corresponding to the microcomponent to remove.
194
+ */
195
+ function remove(target, key) {
196
+ if (!key)
197
+ return; // silent fail
198
+ const node = target;
199
+ // keyed view: the public registry type has no string index signature
200
+ const _mc = node._mc;
201
+ if (!_mc)
202
+ return;
203
+ // delete the property from the HTMLElement _mc collection
204
+ if (_mc[key]) {
205
+ delete _mc[key];
206
+ }
207
+ // remove the _mc attribute if the DOM collection is empty
208
+ if (!Object.keys(_mc).length && target instanceof HTMLElement && target.hasAttribute('data-mc')) {
209
+ target.removeAttribute('data-mc');
210
+ }
211
+ }
212
+ /**
213
+ Runs the "Init" lifecycle event on a DOM microcomponent instance asynchronously.
214
+
215
+ This function schedules the `trigger('Init')` call in a microtask, ensuring
216
+ that all synchronous initialization is complete before the lifecycle hook
217
+ executes. Only `DC` instances have an Init lifecycle — the `McBase`
218
+ constructor calls this exclusively behind an `instanceof DC` check, so the
219
+ signature encodes that invariant instead of re-checking it at runtime.
220
+
221
+ @param element - The DOM microcomponent instance on which to trigger the Init event.
222
+ */
223
+ function init(element) {
224
+ queueMicrotask(() => {
225
+ element.trigger('Init');
226
+ });
227
+ }
228
+ /**
229
+ Attaches event listeners to DOM elements based on method naming conventions.
230
+
231
+ Scans the prototype of the provided element and attaches listeners for methods
232
+ prefixed with `on` or `one`, corresponding to either native HTML events (e.g.,
233
+ 'onclick') or microcomponent-prefixed custom events (e.g., '_mcUpdate').
234
+
235
+ @param element - The instance whose prototype should be scanned for event handlers.
236
+ */
237
+ function autoAttachListeners(element) {
238
+ const sourceTarget = element.target;
239
+ const prototype = Object.getPrototypeOf(element);
240
+ const sourceElement = element;
241
+ const definedEventHandlerNames = Object.getOwnPropertyNames(prototype).filter((key) => /^(on[A-Z]|one[A-Z])/.test(key) && typeof prototype[key] === 'function');
242
+ for (const eventHandlerName of definedEventHandlerNames) {
243
+ const once = /^one/.test(eventHandlerName);
244
+ const eventName = eventHandlerName.replace(/^one?/, ''); // preserves upper/lowercase for non-native events
245
+ const listener = (ev) => sourceElement[eventHandlerName](ev);
246
+ on(sourceTarget, eventName, listener, once);
247
+ }
248
+ }
249
+ /**
250
+ Retrieves immediate child microcomponents of a DOM node.
251
+
252
+ Queries the DOM for descendant elements marked with `data-mc` that are direct
253
+ children of the provided element and returns their associated `_mc` instances.
254
+
255
+ @param domNode - The DOM element whose child microcomponents to find.
256
+ @returns An array of child microcomponent instances.
257
+ */
258
+ function getMcChildNodes(domNode) {
259
+ const selector = '[data-mc]:not(:scope [data-mc] [data-mc])';
260
+ const childMcElements = Array.from(domNode.querySelectorAll(selector))
261
+ .filter((e) => e instanceof HTMLElement && '_mc' in e)
262
+ .map((e) => e._mc);
263
+ return childMcElements;
264
+ }
265
+ /**
266
+ Walks up the DOM from `from`, returning the `_mc` controller of the nearest
267
+ `data-mc` ancestor — restricted, when `key` is given, to hosts whose registry
268
+ actually carries that key. Hosts marked `data-mc` but not (yet) carrying an
269
+ `_mc` record (e.g. autoload pending) are skipped rather than returned.
270
+
271
+ @param from - The element whose ancestors to search (exclusive).
272
+ @param key - Optional registry key the ancestor's `_mc` must contain.
273
+ @returns The nearest matching controller, or null.
274
+ */
275
+ function findMcAncestor(from, key) {
276
+ let el = from.parentElement?.closest('[data-mc]');
277
+ while (el instanceof HTMLElement) {
278
+ /* v8 ignore next */
279
+ if ('_mc' in el) {
280
+ const mc = el._mc;
281
+ if (key === undefined || key in mc)
282
+ return mc;
283
+ }
284
+ el = el.parentElement?.closest('[data-mc]');
285
+ }
286
+ return null;
287
+ }
288
+ /**
289
+ The base class for all microcomponents.
290
+
291
+ Implements a `WeakRef` to the target DOM element or other data, avoiding
292
+ circular references. Automatically triggers the "Init" lifecycle event and
293
+ attaches listeners via `autoAttachListeners` upon construction.
294
+ */
295
+ class McBase {
296
+ _target; // avoid circular reference
297
+ /**
298
+ The constructor for the base `McBase` class.
299
+
300
+ Initializes a weak reference to the target and triggers auto-attachment
301
+ of listeners and initialization lifecycle events.
302
+
303
+ @param target - The object or element this microcomponent represents.
304
+ */
305
+ constructor(target) {
306
+ this._target = new WeakRef(target || {});
307
+ if (this instanceof DC) {
308
+ autoAttachListeners(this);
309
+ init(this);
310
+ }
311
+ }
312
+ /**
313
+ Returns the current target object (DOM node or other data) referenced by this microcomponent.
314
+
315
+ Uses a `WeakRef` internally to avoid circular references, and safely dereferences it here.
316
+
317
+ The dereference is asserted non-null: the controller is stored on its target
318
+ (`element._mc`), so any code that can reach the controller through the element is
319
+ holding the target alive. Only a controller retained independently after its element
320
+ was garbage-collected could observe a stale reference — do not cache controllers
321
+ detached from their elements.
322
+
323
+ @returns The target object.
324
+ */
325
+ get target() {
326
+ return this._target.deref();
327
+ }
328
+ }
329
+ /**
330
+ A general-purpose base microcomponent that does not require a DOM target.
331
+
332
+ Inherits from `McBase`, but is primarily used as a lightweight container or
333
+ for logic not directly tied to DOM elements.
334
+
335
+ @template TTarget - The concrete type of the wrapped target data; preserved by
336
+ the `target` getter, so `new MC({ some: 'data' }).target.some` stays typed.
337
+ */
338
+ class MC extends McBase {
339
+ /**
340
+ Constructs an instance of the general-purpose microcomponent.
341
+
342
+ @param target - Target object or data.
343
+ */
344
+ constructor(target) {
345
+ super(target);
346
+ }
347
+ }
348
+ /**
349
+ A DOM-centric microcomponent that operates on a specific HTML element.
350
+
351
+ Extends `McBase` and associates itself with an actual DOM node via the
352
+ `_target` reference. Provides methods for event handling, DOM traversal,
353
+ and interaction with sibling or ancestor microcomponents.
354
+
355
+ @template T - The concrete element type of the host element.
356
+ @template EM - The component's custom event map (`event name → payload type`).
357
+ Declaring it registers events on the type level: `trigger()` enforces the
358
+ payload for registered names, and `on()`/`one()` handlers receive
359
+ `CustomEvent<McDetail<Payload>>` with no annotation needed. Unregistered
360
+ names keep the open `unknown`-payload behavior.
361
+ @example
362
+ ```ts
363
+ class Cart extends DC<HTMLFormElement, { CartUpdate: { count: number } }> {
364
+ checkout() {
365
+ this.trigger('CartUpdate', { count: 2 }, 'u'); // payload checked
366
+ this.on('CartUpdate', (ev) => ev.detail.data.count); // ev fully typed
367
+ }
368
+ }
369
+ ```
370
+ */
371
+ class DC extends McBase {
372
+ constructor(target) {
373
+ super(target);
374
+ }
375
+ static add(target, key, element = {}) {
376
+ return add(target, key, element);
377
+ }
378
+ /**
379
+ Static helper to remove a named microcomponent from a DOM element.
380
+
381
+ Wrapper around the `remove()` utility function, providing a class-level API
382
+ for removing child microcomponents.
383
+
384
+ @template T - A plain or already-enhanced element type.
385
+ @template K - The literal registration key; its entry is poisoned in the registry type.
386
+ @param target - The DOM element from which to remove the microcomponent.
387
+ @param key - The property name/key corresponding to the microcomponent to remove.
388
+ */
389
+ static remove(target, key) {
390
+ remove(target, key);
391
+ }
392
+ /**
393
+ Class-verified entry lookup: returns the component stored under `key` on the
394
+ element's `_mc` record — but only if it is an instance of `Klass`.
395
+
396
+ This is the *sound* retrieval primitive: presence AND type identity are both
397
+ proven at runtime (`instanceof`), so it needs no global `McRegistry`
398
+ declaration and stays correct even when different elements use the same key
399
+ name for different component types. Accepts any value (including
400
+ `querySelector` results that may be null) — every failure mode returns
401
+ `undefined`.
402
+
403
+ @template E - The component type, inferred from `Klass`.
404
+ @param element - The value to look the entry up on; anything non-host yields undefined.
405
+ @param key - The registry key name to look under.
406
+ @param elementMcClass - The class the entry must be an instance of.
407
+ @returns The verified instance, or undefined.
408
+ @example
409
+ ```ts
410
+ const cart = DC.get(document.querySelector('form'), 'cart', Cart);
411
+ cart?.addItem(item); // Cart | undefined — no global declaration involved
412
+ ```
413
+ */
414
+ static get(element, key, elementMcClass) {
415
+ if (!isMcDOM(element))
416
+ return undefined;
417
+ const entry = element._mc[key];
418
+ return entry instanceof elementMcClass ? entry : undefined;
419
+ }
420
+ off(eventName, handler) {
421
+ off(this.target, eventName, handler);
422
+ }
423
+ on(eventName, handler) {
424
+ return on(this.target, eventName, handler);
425
+ }
426
+ one(eventName, handler) {
427
+ return one(this.target, eventName, handler);
428
+ }
429
+ /**
430
+ Triggers a custom event on the DOM node and optionally bubbles it.
431
+
432
+ This is an instance method wrapper for `trigger()`, using the internal `_target` DOM reference.
433
+
434
+ For event names registered in the component's event map `EM` the payload type
435
+ is enforced; unregistered names accept any payload (typed via inference or an
436
+ explicit `trigger<Payload>(...)` argument).
437
+
438
+ @param eventName - The name of the event (will be prefixed if needed).
439
+ @param data - Additional data payload attached to `event.detail.data`.
440
+ @param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
441
+ @param controller - Optional controller for managing bubbling behavior.
442
+ */
443
+ trigger(eventName, data = {}, bubble = 'l', controller) {
444
+ trigger(this.target, eventName, data, bubble, controller);
445
+ }
446
+ ancestors(key) {
447
+ const result = [];
448
+ let current = findMcAncestor(this.target, key);
449
+ while (current) {
450
+ result.push(current);
451
+ current = findMcAncestor(current.target, key);
452
+ }
453
+ return result;
454
+ }
455
+ closest(key) {
456
+ return findMcAncestor(this.target, key);
457
+ }
458
+ children(key) {
459
+ const kids = getMcChildNodes(this.target);
460
+ return key === undefined ? kids : kids.filter((mc) => key in mc);
461
+ }
462
+ descendants(key) {
463
+ const hosts = Array.from(this.target.querySelectorAll('[data-mc]'));
464
+ const mcArray = hosts
465
+ .filter((e) => e instanceof HTMLElement && '_mc' in e)
466
+ .map((e) => e._mc)
467
+ .filter((mc) => key === undefined || key in mc);
468
+ return mcArray;
469
+ }
470
+ }
471
+ export { MC, DC, McBase };
@@ -0,0 +1,54 @@
1
+ /**
2
+ @module
3
+
4
+ The {@linkcode autoload} function enables automatic detection and instantiation of microcomponents in the DOM via a `MutationObserver`. When enabled, newly added elements with undefined custom tags or `data-mc` references will trigger dynamic script loading and component attachment.
5
+
6
+ Kept in its own module so the core controller classes in MC.ts stay free of
7
+ side-effect-bearing DOM machinery (`MutationObserver`, script injection):
8
+ importing `DC`/`MC` never pulls a DOM observer into the bundle unless
9
+ {@link autoload} itself is imported.
10
+ */
11
+ /**
12
+ The signature of {@link autoload}: an enable/disable toggle that also carries the
13
+ set of microcomponent names currently loading via its `state` property.
14
+
15
+ @example
16
+ ```ts
17
+ import { autoload } from '@twobirds/microcomponents';
18
+
19
+ autoload(true); // enable auto-loading
20
+ console.log(autoload.state); // Set of component names currently loading
21
+ autoload(false); // disable
22
+ ```
23
+ */
24
+ export interface Autoload {
25
+ /**
26
+ Enables or disables automatic detection and instantiation of microcomponents.
27
+
28
+ When enabled, the system uses a `MutationObserver` to watch for newly-added
29
+ DOM nodes. For each such node:
30
+ - If it contains custom elements that are not yet defined, attempts to load them
31
+ - If it references undefined microcomponent names in its `data-mc` attribute,
32
+ those definitions are also loaded and attached.
33
+
34
+ When disabled, the observer is disconnected and all internal tracking state is reset.
35
+
36
+ @param {boolean} [enable=true] - Whether to enable or disable auto-loading behavior.
37
+ @returns The new autoload state (true if enabled, false otherwise).
38
+ */
39
+ (enable?: boolean): boolean;
40
+ /**
41
+ The internal `loadingDCs` set, exposed for external inspection.
42
+
43
+ This property is attached to the `autoload()` function itself and contains
44
+ the names of microcomponent classes currently being loaded (or queued to load).
45
+ */
46
+ state: Set<string>;
47
+ }
48
+ declare global {
49
+ interface Window {
50
+ autoload?: Autoload;
51
+ }
52
+ }
53
+ export declare const autoload: Autoload;
54
+ //# sourceMappingURL=autoload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoload.d.ts","sourceRoot":"","sources":["../../src/core/autoload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;EASE;AAoJF;;;;;;;;;;;;EAYE;AACF,MAAM,WAAW,QAAQ;IACxB;;;;;;;;;;;;;IAaG;IACH,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5B;;;;;IAKG;IACH,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnB;AAED,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,MAAM;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;KACpB;CACD;AAID,eAAO,MAAM,QAAQ,EAAE,QAuBtB,CAAC"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ @module
3
+
4
+ The {@linkcode autoload} function enables automatic detection and instantiation of microcomponents in the DOM via a `MutationObserver`. When enabled, newly added elements with undefined custom tags or `data-mc` references will trigger dynamic script loading and component attachment.
5
+
6
+ Kept in its own module so the core controller classes in MC.ts stay free of
7
+ side-effect-bearing DOM machinery (`MutationObserver`, script injection):
8
+ importing `DC`/`MC` never pulls a DOM observer into the bundle unless
9
+ {@link autoload} itself is imported.
10
+ */
11
+ import { DC } from './MC.js';
12
+ import { isMcDOM } from './mcGuards.js';
13
+ const loadingDCs = new Set();
14
+ /**
15
+ Creates a closure that attaches DOM elements with the specified tag name to
16
+ their corresponding microcomponent instances.
17
+
18
+ Used internally by `makeLoadScript()` when dynamically loading external
19
+ custom element definitions. The returned function is invoked after the
20
+ element definition becomes available, and performs:
21
+ - Querying of all matching elements in the DOM
22
+ - Instantiation of new `DC` instances
23
+ - Attachment to each DOM node via `DC.add()`
24
+
25
+ @param element - The tag name (custom element name) of the microcomponent to instantiate.
26
+ @returns A closure that performs the attachment logic when invoked.
27
+ */
28
+ function attachDC(element) {
29
+ return () => {
30
+ const elements = Array.from(document.querySelectorAll(`[data-mc*="${element}"]`)).filter((e) => e instanceof HTMLElement);
31
+ if (elements.length === 0)
32
+ return;
33
+ const customElementClass = customElements.get(element);
34
+ for (let node of elements) {
35
+ try {
36
+ DC.add(node, customElementClass.name, new customElementClass(node));
37
+ }
38
+ catch (error) {
39
+ /* v8 ignore start */
40
+ if ('create' in customElementClass && typeof customElementClass.create === 'function') {
41
+ const customElement = customElementClass.create(node);
42
+ if (!isMcDOM(customElement))
43
+ throw `create() of Class ${customElementClass.name} did not return a McHost.`;
44
+ customElement.setAttribute('data-mc', node.getAttribute('data-mc') ?? '');
45
+ node.replaceWith(customElement);
46
+ node = customElement;
47
+ }
48
+ else {
49
+ throw `Class ${customElementClass.name} doesn't have a callable constructor AND class doesn't have a static .create() method.`;
50
+ }
51
+ /* v8 ignore end */
52
+ }
53
+ const newMcName = node
54
+ .getAttribute('data-mc')
55
+ .split(' ')
56
+ .filter((name) => name !== element && name !== '')
57
+ .join(' ')
58
+ .trim();
59
+ node.setAttribute('data-mc', newMcName);
60
+ }
61
+ };
62
+ }
63
+ /**
64
+ Dynamically loads and registers a custom element definition from an external JS file.
65
+
66
+ Constructs the script URL based on kebab-case naming (e.g., `my-element` → `./my/element.js`).
67
+ Registers a handler to be executed once the element is defined, then appends the
68
+ `<script>` tag to `<head>`. Handles both successful loading and error cases.
69
+
70
+ @param element - The custom element tag name (e.g., 'mc-button', 'my-widget').
71
+ */
72
+ function makeLoadScript(element) {
73
+ /* v8 ignore next */
74
+ if (loadingDCs.has(element))
75
+ return; // already loading
76
+ loadingDCs.add(element);
77
+ const fileName = './' + element.split('-').join('/') + '.js';
78
+ const scriptElement = document.createElement('script');
79
+ scriptElement.setAttribute('src', fileName);
80
+ scriptElement.setAttribute('blocking', 'render');
81
+ scriptElement.async = true;
82
+ scriptElement.setAttribute('type', 'module');
83
+ scriptElement.setAttribute('data-name', element);
84
+ scriptElement.setAttribute('loading', '');
85
+ const loadedPromise = customElements.whenDefined(element);
86
+ loadedPromise.then(attachDC(element));
87
+ scriptElement.addEventListener('load', (ev) => {
88
+ loadingDCs.delete(element);
89
+ document.head.querySelector(`script[data-name="${element}"]`)?.remove();
90
+ });
91
+ scriptElement.addEventListener('error', (ev) => {
92
+ console.error('could not load DC code from', fileName);
93
+ });
94
+ document.head.append(scriptElement);
95
+ }
96
+ /**
97
+ Scans the DOM for undefined custom elements and pending microcomponents,
98
+ then loads their definitions if necessary.
99
+
100
+ This is the main driver function for the autoload mechanism. It performs:
101
+ 1. Finds all undefined custom elements in the document
102
+ 2. Checks whether they are already loading or defined
103
+ 3. Calls `makeLoadScript()` to fetch missing definitions
104
+ 4. Invokes `attachDC()` where element definitions exist but haven't been attached yet
105
+
106
+ It is called both immediately after enabling autoload and on each DOM mutation.
107
+ */
108
+ function loadUndefinedMCs() {
109
+ // load unresolved custom elements
110
+ [...document.querySelectorAll(':not(:defined)')]
111
+ .filter((element) => !!element &&
112
+ !customElements.get(element.tagName.toLowerCase()) && // not in CustomElementsRegistry
113
+ !loadingDCs.has(element.tagName.toLowerCase()) // not in loading list
114
+ )
115
+ .forEach((el) => {
116
+ makeLoadScript(el.tagName.toLowerCase()); // will automatically be instantiated
117
+ });
118
+ // if defined in _mc attribute: instantiate DCs, load if necessary
119
+ [...document.querySelectorAll('[data-mc]:not([data-mc=""]')]
120
+ .filter((element) => !!element)
121
+ .forEach((el) => {
122
+ const _mc = el
123
+ .getAttribute('data-mc')
124
+ ?.split(' ')
125
+ .filter((name) => name !== '');
126
+ _mc?.forEach((element) => {
127
+ const isDefined = customElements.get(element);
128
+ if (!loadingDCs.has(element) && !isDefined) {
129
+ makeLoadScript(element);
130
+ }
131
+ else if (isDefined) {
132
+ attachDC(element)();
133
+ }
134
+ });
135
+ });
136
+ }
137
+ let observer = null;
138
+ export const autoload = function (enable = true) {
139
+ if (!enable) {
140
+ if (observer) {
141
+ observer.disconnect();
142
+ observer = null;
143
+ }
144
+ return false;
145
+ }
146
+ const wasEnabled = !!observer;
147
+ if (wasEnabled)
148
+ return true;
149
+ // Initialize observer only once
150
+ observer = new MutationObserver(loadUndefinedMCs);
151
+ setTimeout(() => {
152
+ observer.observe(document.body, { childList: true, subtree: true });
153
+ loadUndefinedMCs();
154
+ }, 5);
155
+ window.autoload ??= autoload;
156
+ return true;
157
+ };
158
+ autoload.state = loadingDCs;