@twobirds/microcomponents 0.1.20 → 0.1.23
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/LICENSE +21 -0
- package/README.md +97 -102
- package/dist/MC.min.js +1 -342
- package/dist/autoload.min.js +1 -0
- package/dist/core/MC.d.ts +401 -0
- package/dist/core/MC.d.ts.map +1 -0
- package/dist/core/MC.js +471 -0
- package/dist/core/autoload.d.ts +54 -0
- package/dist/core/autoload.d.ts.map +1 -0
- package/dist/core/autoload.js +158 -0
- package/dist/core/elements.d.ts +232 -0
- package/dist/core/elements.d.ts.map +1 -0
- package/dist/core/elements.js +123 -0
- package/dist/core/events.d.ts +231 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +228 -0
- package/dist/core/mcGuards.d.ts +49 -0
- package/dist/core/mcGuards.d.ts.map +1 -0
- package/dist/core/mcGuards.js +19 -0
- package/dist/elements.min.js +1 -1
- package/dist/events.min.js +1 -0
- package/dist/formValues.min.js +1 -1
- package/dist/mcGuards.min.js +1 -0
- package/dist/microcomponents.d.ts +8 -5
- package/dist/microcomponents.d.ts.map +1 -1
- package/dist/microcomponents.js +8 -5
- package/dist/microcomponents.min.js +1 -1
- package/dist/observables.min.js +1 -1
- package/dist/state/formValues.d.ts +70 -0
- package/dist/state/formValues.d.ts.map +1 -0
- package/dist/state/formValues.js +334 -0
- package/dist/state/observables.d.ts +276 -0
- package/dist/state/observables.d.ts.map +1 -0
- package/dist/state/observables.js +382 -0
- package/dist/state/placeholders.d.ts +62 -0
- package/dist/state/placeholders.d.ts.map +1 -0
- package/dist/state/placeholders.js +181 -0
- package/dist/types/mcTypes.d.ts +201 -0
- package/dist/types/mcTypes.d.ts.map +1 -0
- package/dist/types/mcTypes.js +9 -0
- package/dist/utils/helpers.d.ts +195 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +340 -0
- package/package.json +95 -61
- package/dist/MC.d.ts +0 -39
- package/dist/MC.d.ts.map +0 -1
- package/dist/MC.js +0 -299
- package/dist/MC.ts +0 -457
- package/dist/MicroComponents.svg +0 -15
- package/dist/elements.d.ts +0 -5
- package/dist/elements.d.ts.map +0 -1
- package/dist/elements.js +0 -50
- package/dist/elements.ts +0 -94
- package/dist/formValues.d.ts +0 -3
- package/dist/formValues.d.ts.map +0 -1
- package/dist/formValues.js +0 -158
- package/dist/formValues.ts +0 -213
- package/dist/helpers.d.ts +0 -14
- package/dist/helpers.d.ts.map +0 -1
- package/dist/helpers.js +0 -111
- package/dist/helpers.ts +0 -138
- package/dist/microcomponents.ts +0 -6
- package/dist/observables.d.ts +0 -11
- package/dist/observables.d.ts.map +0 -1
- package/dist/observables.js +0 -228
- package/dist/observables.ts +0 -338
package/dist/core/MC.js
ADDED
|
@@ -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;
|