@twobirds/microcomponents 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +7 -8
  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
@@ -1,72 +0,0 @@
1
- /**
2
- # Overview
3
-
4
- 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.
5
-
6
- In general, your code extends from either of these classes depending on whether you need DOM interaction or pure logic encapsulation.
7
-
8
- ---
9
-
10
- ## MC vs DC: Server vs Client
11
-
12
- The key distinction between the two base classes is their execution context:
13
-
14
- - **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.
15
-
16
- - **DC (DOM Component)** – A DOM-centric component that operates wherever a real ( browser ) or simulated ( jsdom, etc. ) DOM exists. It attaches itself to an HTMLElement and provides event handling, DOM traversal, and lifecycle management. The `target` parameter is required and must be an HTMLElement.
17
-
18
- **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>.
19
-
20
- ### When to use which?
21
-
22
- | Scenario | Recommended Class |
23
- |----------|-------------------|
24
- | Server-side rendering | DC |
25
- | Interactive UI elements in the browser | DC |
26
- | Event-driven components attached to HTML nodes | DC |
27
- | Pure logic without DOM | MC |
28
-
29
- ---
30
-
31
- ## Backreference Concept
32
-
33
- 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.
34
-
35
- - For **DC instances**, `this.target` refers to the HTMLElement in the DOM (or testing DOM such as jsdom) to which the component is attached.
36
- - For **MC instances**, `this.target` can be any arbitrary object, allowing flexible composition patterns.
37
-
38
- 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.
39
-
40
- ---
41
- # DC Class Specific
42
- ---
43
-
44
- ## DOM Integration
45
-
46
- When a DC is attached to an element via {@linkcode DC.add}, two things happen:
47
-
48
- 1. The element gains a `_mc` property (of type {@link McHost}) that holds references to all attached microcomponents, keyed by their unique identifiers.
49
- 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.
50
-
51
- This `_mc` property is the **only** interference with the native DOM structure, ensuring compatibility with other frameworks or libraries.
52
-
53
- ---
54
-
55
- ## Lifecycle
56
-
57
- - DC instances automatically trigger an `"Init"` lifecycle event asynchronously - using queueMicrotask() - after construction. A component with a 'oneInit()' method will catch that lifecycle event, and when it runs it is guaranteed that the constructor() has run.
58
- - 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.
59
- - When using the static `assets` property to on-demand load shadow/target templates and css
60
- - those are automatically attached to their respective target locations
61
- - and all native event handlers in template code (<button click="...">) are automatically attached also
62
- ---
63
-
64
- @module Core
65
- @showGroups
66
- */
67
- /** @group MicroComponents */
68
- export { MC, DC, McBase } from './MC.js';
69
- /** @group Elements */
70
- export { defineCE, getCE, listCE } from './elements.js';
71
- /** @group Helpers */
72
- export { isMcHost } from './mcGuards.js';
@@ -1,5 +0,0 @@
1
- /**
2
- @module State
3
- */
4
- export { observe, observePlugin, isAlreadyObserved } from './observables.js';
5
- export { formValues, formValuePlugin } from './formValues.js';
@@ -1,187 +0,0 @@
1
- /**
2
- @hidden
3
- DOM placeholder binding: `{name}` templates in attributes and text nodes,
4
- kept up to date from observable data.
5
-
6
- Split from observables.ts so the pure observable machinery carries no DOM
7
- scanning/decoration code. The two modules reference each other only inside
8
- function bodies (`Placeholders` calls `observe(this)`; `bind()` calls
9
- {@link makePlaceholders}), never during module evaluation, so the circular
10
- import is benign under ESM live bindings.
11
- */
12
- import { DC } from '../core/MC.js';
13
- import { observe } from './observables.js';
14
- /**
15
- Extracts placeholder names from an HTML element's attributes and text content.
16
-
17
- Placeholders are strings enclosed in curly braces `{}`. This function scans
18
- all attributes and direct child text nodes of the target element for placeholders
19
- and returns them as an array of unique names (without braces).
20
-
21
- @param target - The HTMLElement to scan for placeholders.
22
- @returns An array of placeholder names found within the element's attributes and text content.
23
- */
24
- function getPlaceholders(target) {
25
- const placeholders = new Set(), regEx = /\{[^\{\}]*\}/g;
26
- // scan attributes
27
- (target.getAttributeNames ? target.getAttributeNames() : [])
28
- .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
29
- .forEach((attr) => {
30
- const attrPlaceholders = new Set(target.getAttribute(attr)?.match(regEx));
31
- attrPlaceholders.forEach((attrPlaceholder) => placeholders.add(attrPlaceholder.replace(/[\{\}]/g, '')));
32
- });
33
- // scan children text nodes
34
- [...target.childNodes]
35
- .filter((childNode) => {
36
- return (
37
- // is text node and has placeholders
38
- childNode.nodeType === 3 && (childNode.nodeValue || '').match(regEx));
39
- })
40
- .forEach((childNode) => {
41
- /* v8 ignore next -- the filter above matched nodeValue against the placeholder
42
- pattern, so it is non-null and non-empty by the time we get here */
43
- const textContent = childNode?.nodeValue || '';
44
- const contentPlaceholders = textContent.match(regEx);
45
- contentPlaceholders.forEach((textNodePlaceholder) => placeholders.add(textNodePlaceholder.replace(/[\{\}]/g, '')));
46
- });
47
- return [...placeholders];
48
- }
49
- // Tag names instead of constructor references (HTMLInputElement etc.):
50
- // evaluating those globals at module scope crashes any DOM-less import
51
- // of the package (e.g. server-side use of MC).
52
- const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
53
- /**
54
- A class that manages placeholders within an HTML element and updates them dynamically.
55
-
56
- This class extends `DC` (Data Controller) and processes the target element to identify
57
- placeholder patterns in attributes and text nodes. It then observes changes on its internal
58
- data object and updates the corresponding DOM locations when notified of changes.
59
- */
60
- class Placeholders extends DC {
61
- // observe(this) in the constructor swaps this for the live observable wrapper.
62
- data = {};
63
- oldDisplay = '';
64
- /**
65
- Creates a new `Placeholders` instance for the given HTML element.
66
-
67
- Initializes the internal data with empty strings for each placeholder
68
- found in the target and starts observing the data object. Attaches
69
- callbacks to update placeholders as needed.
70
-
71
- @param target - The HTMLElement to process for placeholders.
72
- */
73
- constructor(target) {
74
- super(target);
75
- getPlaceholders(target).forEach((property) => (this.data[property] = ''));
76
- observe(this);
77
- this.#attachCallbacks();
78
- }
79
- #attachCallbacks() {
80
- const target = this.target;
81
- let hide = false;
82
- // scan attributes
83
- /* v8 ignore next -- Placeholders is only ever constructed with an HTMLElement
84
- (DC's constructor reads target.tagName), so the fragment/shadow-root guard
85
- that getPlaceholders needs is dead here. */
86
- const attrNames = target.getAttributeNames ? target.getAttributeNames() : [];
87
- attrNames
88
- .filter((attr) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
89
- .forEach((attr) => {
90
- const placeholders = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
91
- /* v8 ignore next -- attr passed the filter above, so its value is a non-null string */
92
- const attrTemplate = target.getAttribute(attr) ?? '';
93
- // attach callback
94
- this.data.subscribe(((template) => (data) => {
95
- let result = template;
96
- placeholders.forEach((placeholder) => {
97
- result = result.replace(placeholder,
98
- // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
99
- String(data[placeholder.replace(/[\{\}]/g, '')]));
100
- });
101
- target.setAttribute(attr, result);
102
- })(attrTemplate));
103
- });
104
- // scan children text nodes
105
- [...target.childNodes]
106
- .filter((childNode) => {
107
- return (
108
- // is text node and has placeholders
109
- childNode.nodeType === 3 && (childNode.nodeValue || '').match(/\{[^\{\}]*\}/g));
110
- })
111
- .forEach((childNode) => {
112
- hide = true;
113
- /* v8 ignore next -- the filter above only admits text nodes with a non-empty
114
- nodeValue, so the ?? fallback is unreachable */
115
- const textTemplate = childNode.nodeValue ?? '';
116
- const placeholders = new Set(textTemplate.match(/\{[^\{\}]*\}/g));
117
- // attach callback
118
- this.data.subscribe(((template) => (data) => {
119
- let result = template;
120
- placeholders.forEach((placeholder) => {
121
- result = result.replace(placeholder,
122
- // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
123
- String(data[placeholder.replace(/[\{\}]/g, '')]));
124
- });
125
- if (this.oldDisplay) {
126
- target.style = this.oldDisplay;
127
- }
128
- else {
129
- target.removeAttribute('style');
130
- }
131
- delete this.oldDisplay;
132
- childNode.nodeValue = result;
133
- })(textTemplate));
134
- // hide for now
135
- this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
136
- if (hide && !inputTags.includes(target.tagName) && target.tagName !== 'FIELDSET') {
137
- target.style.display = 'none';
138
- }
139
- });
140
- }
141
- /**
142
- Handles a `data` custom event to update the internal placeholder data.
143
-
144
- Extracts matching keys from the event's detail data and updates only those
145
- properties in the internal `data` object that were previously defined as placeholders.
146
-
147
- @param ev - The CustomEvent containing new data to merge into the placeholders.
148
- */
149
- onData(ev) {
150
- const data = {};
151
- Object.keys(ev.detail.data).forEach((key) => {
152
- if (this.data.hasOwnProperty(key))
153
- data[key] = ev.detail.data[key];
154
- });
155
- if (JSON.stringify(data) !== JSON.stringify(this.data))
156
- Object.assign(this.data, data); // will trigger callbacks
157
- }
158
- }
159
- /**
160
- Finds all elements within a given scope that contain placeholders.
161
-
162
- @param target - The HTMLElement, DocumentFragment, or ShadowRoot to search.
163
- @returns An array of HTMLElements containing placeholder patterns.
164
- */
165
- function getPlaceholderElements(target) {
166
- const elements = [target]
167
- .concat([...target.querySelectorAll('*')])
168
- .filter((e) => !!getPlaceholders(e).length)
169
- .filter((e) => e?.tagName !== 'STYLE')
170
- .reverse(); // to start with inner elements
171
- return elements;
172
- }
173
- /**
174
- Applies placeholder support to an element and its descendants.
175
-
176
- This function scans the provided target for elements containing placeholders
177
- and attaches a `Placeholders` controller instance to each one if not already present.
178
-
179
- @param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
180
- */
181
- export function makePlaceholders(element) {
182
- getPlaceholderElements(element).forEach((e) => {
183
- if (e?._mc?._placeholders)
184
- return;
185
- DC.add(e, '_placeholders', new Placeholders(e));
186
- });
187
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,9 +0,0 @@
1
- /**
2
-
3
- Pure type layer of the microcomponent system.
4
-
5
- RULE: types only — no runtime exports, no imports. It sits at the bottom of the
6
- dependency graph so every other module can import from it without cycles.
7
- @module Types
8
- */
9
- export {};
@@ -1,160 +0,0 @@
1
- /**
2
- @module Autoload
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 '../core/MC.js';
12
- import { isMcHost } from '../core/mcGuards.js';
13
- import { resolveComponentPath } from './resolveComponentPath.js';
14
- const loadingDCs = new Set();
15
- /**
16
- Creates a closure that attaches DOM elements with the specified tag name to
17
- their corresponding microcomponent instances.
18
-
19
- Used internally by `makeLoadScript()` when dynamically loading external
20
- custom element definitions. The returned function is invoked after the
21
- element definition becomes available, and performs:
22
- - Querying of all matching elements in the DOM
23
- - Instantiation of new `DC` instances
24
- - Attachment to each DOM node via `DC.add()`
25
-
26
- @param element - The tag name (custom element name) of the microcomponent to instantiate.
27
- @returns A closure that performs the attachment logic when invoked.
28
- @group Autoload
29
- */
30
- function attachDC(element) {
31
- return () => {
32
- const elements = Array.from(document.querySelectorAll(`[data-mc*="${element}"]`)).filter((e) => e instanceof HTMLElement);
33
- if (elements.length === 0)
34
- return;
35
- const customElementClass = customElements.get(element);
36
- for (let node of elements) {
37
- try {
38
- DC.add(node, customElementClass.name, new customElementClass(node));
39
- }
40
- catch (error) {
41
- /* v8 ignore start */
42
- if ('create' in customElementClass && typeof customElementClass.create === 'function') {
43
- const customElement = customElementClass.create(node);
44
- if (!isMcHost(customElement))
45
- throw `create() of Class ${customElementClass.name} did not return a McHost.`;
46
- customElement.setAttribute('data-mc', node.getAttribute('data-mc') ?? '');
47
- node.replaceWith(customElement);
48
- node = customElement;
49
- }
50
- else {
51
- throw `Class ${customElementClass.name} doesn't have a callable constructor AND class doesn't have a static .create() method.`;
52
- }
53
- /* v8 ignore end */
54
- }
55
- const newMcName = node
56
- .getAttribute('data-mc')
57
- .split(' ')
58
- .filter((name) => name !== element && name !== '')
59
- .join(' ')
60
- .trim();
61
- node.setAttribute('data-mc', newMcName);
62
- }
63
- };
64
- }
65
- /**
66
- Dynamically loads and registers a custom element definition from an external JS file.
67
-
68
- Constructs the script URL based on kebab-case naming (e.g., `my-element` → `./my/element.js`).
69
- Registers a handler to be executed once the element is defined, then appends the
70
- `<script>` tag to `<head>`. Handles both successful loading and error cases.
71
-
72
- @param element - The custom element tag name (e.g., 'mc-button', 'my-widget').
73
- */
74
- function makeLoadScript(element) {
75
- /* v8 ignore next */
76
- if (loadingDCs.has(element))
77
- return; // already loading
78
- loadingDCs.add(element);
79
- const fileName = resolveComponentPath(element);
80
- const scriptElement = document.createElement('script');
81
- scriptElement.setAttribute('src', fileName);
82
- scriptElement.setAttribute('blocking', 'render');
83
- scriptElement.async = true;
84
- scriptElement.setAttribute('type', 'module');
85
- scriptElement.setAttribute('data-name', element);
86
- scriptElement.setAttribute('loading', '');
87
- const loadedPromise = customElements.whenDefined(element);
88
- loadedPromise.then(attachDC(element));
89
- scriptElement.addEventListener('load', (ev) => {
90
- loadingDCs.delete(element);
91
- document.head.querySelector(`script[data-name="${element}"]`)?.remove();
92
- });
93
- scriptElement.addEventListener('error', (ev) => {
94
- console.error('could not load DC code from', fileName);
95
- });
96
- document.head.append(scriptElement);
97
- }
98
- /**
99
- Scans the DOM for undefined custom elements and pending microcomponents,
100
- then loads their definitions if necessary.
101
-
102
- This is the main driver function for the autoload mechanism. It performs:
103
- 1. Finds all undefined custom elements in the document
104
- 2. Checks whether they are already loading or defined
105
- 3. Calls `makeLoadScript()` to fetch missing definitions
106
- 4. Invokes `attachDC()` where element definitions exist but haven't been attached yet
107
-
108
- It is called both immediately after enabling autoload and on each DOM mutation.
109
- */
110
- function loadUndefinedMCs() {
111
- // load unresolved custom elements
112
- [...document.querySelectorAll(':not(:defined)')]
113
- .filter((element) => !!element &&
114
- !customElements.get(element.tagName.toLowerCase()) && // not in CustomElementsRegistry
115
- !loadingDCs.has(element.tagName.toLowerCase()) // not in loading list
116
- )
117
- .forEach((el) => {
118
- makeLoadScript(el.tagName.toLowerCase()); // will automatically be instantiated
119
- });
120
- // if defined in _mc attribute: instantiate DCs, load if necessary
121
- [...document.querySelectorAll('[data-mc]:not([data-mc=""]')]
122
- .filter((element) => !!element)
123
- .forEach((el) => {
124
- const _mc = el
125
- .getAttribute('data-mc')
126
- ?.split(' ')
127
- .filter((name) => name !== '');
128
- _mc?.forEach((element) => {
129
- const isDefined = customElements.get(element);
130
- if (!loadingDCs.has(element) && !isDefined) {
131
- makeLoadScript(element);
132
- }
133
- else if (isDefined) {
134
- attachDC(element)();
135
- }
136
- });
137
- });
138
- }
139
- let observer = null;
140
- export function autoload(enable = true) {
141
- if (!enable) {
142
- if (observer) {
143
- observer.disconnect();
144
- observer = null;
145
- }
146
- return false;
147
- }
148
- const wasEnabled = !!observer;
149
- if (wasEnabled)
150
- return true;
151
- // Initialize observer only once
152
- observer = new MutationObserver(loadUndefinedMCs);
153
- setTimeout(() => {
154
- observer.observe(document.body, { childList: true, subtree: true });
155
- loadUndefinedMCs();
156
- }, 5);
157
- window.autoload ??= autoload;
158
- return true;
159
- }
160
- autoload.state = loadingDCs;