@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
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@module Observables
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
This module provides reactive data binding capabilities using observables,
|
|
6
|
+
enabling automatic DOM updates when underlying data changes.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The observable system allows you to make object properties "observable" so that
|
|
11
|
+
registered callbacks are automatically invoked whenever those properties change.
|
|
12
|
+
It's particularly designed for reactive templating and binding data to HTML elements.
|
|
13
|
+
It can also handle simple object properties like strings, boolean and numbers.
|
|
14
|
+
|
|
15
|
+
## How `observe()` Works
|
|
16
|
+
|
|
17
|
+
The main function is {@link observe}, which takes an object and makes its properties
|
|
18
|
+
or a specific property observable.
|
|
19
|
+
|
|
20
|
+
### Simple properties
|
|
21
|
+
|
|
22
|
+
javascript
|
|
23
|
+
```js
|
|
24
|
+
let person = { name: "John", count: 0 };
|
|
25
|
+
|
|
26
|
+
observe( person ); // all properties inside are now observables
|
|
27
|
+
|
|
28
|
+
person.name.subscribe( (name) => {
|
|
29
|
+
console.log("Name changed:", name);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
person.count.subscribe( (count) => {
|
|
33
|
+
console.log("Count changed:", count);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
person.name = "Jane"; // > Name changed: Jane
|
|
37
|
+
person.count += 10; // > Count changed: 10
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Deep Change Detection
|
|
41
|
+
|
|
42
|
+
This means that if you modify any of the properties inside an observed object -> all callbacks are triggered.
|
|
43
|
+
|
|
44
|
+
javascript
|
|
45
|
+
```js
|
|
46
|
+
let obj = { data: { name: "John", count: 0 } };
|
|
47
|
+
|
|
48
|
+
observe( obj, 'data' ); // make the data property observable
|
|
49
|
+
|
|
50
|
+
obj.data.subscribe( (data) => {
|
|
51
|
+
console.log("Data changed:", data);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
obj.data.name = "Jane"; // > { data: { name: "Jane", count: 0 } }
|
|
55
|
+
obj.data.count += 10; // > { data: { name: "Jane", count: 10 } }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Debounced Notifications
|
|
59
|
+
|
|
60
|
+
To prevent excessive updates, notifications are debounced briefly:
|
|
61
|
+
|
|
62
|
+
javascript
|
|
63
|
+
```js
|
|
64
|
+
let obj = observe({ value: 1 });
|
|
65
|
+
for (let i = 0; i < 100; i++) {
|
|
66
|
+
obj.value++; // Only triggers one callback after all changes complete
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## DOM Binding with Placeholders
|
|
71
|
+
|
|
72
|
+
The observables integrate tightly with placeholder-based templating:
|
|
73
|
+
|
|
74
|
+
- Elements with `{placeholder}` syntax are scanned for observable data.
|
|
75
|
+
- When observed data changes, matching placeholders in the DOM are updated automatically.
|
|
76
|
+
|
|
77
|
+
@example
|
|
78
|
+
|
|
79
|
+
HTML
|
|
80
|
+
```html
|
|
81
|
+
<div class="model {visibility}">
|
|
82
|
+
Hello {name}! You have {count} messages.
|
|
83
|
+
</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Javascript
|
|
87
|
+
```js
|
|
88
|
+
const div = document.querySelector('.model');
|
|
89
|
+
|
|
90
|
+
const model = observe({
|
|
91
|
+
data: { name: "Alice", count: 0 },
|
|
92
|
+
visibility: ''
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (div) {
|
|
96
|
+
model.data.bind( div );
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
model.data.count++; // Automatically updates the DOM with new count value
|
|
100
|
+
model.visibility = 'hidden'; // adds the hidden class to the DOM element
|
|
101
|
+
```
|
|
102
|
+
*/
|
|
103
|
+
import { flattenObject, deepEqual, copyGettersSetters, isBrowser } from '../utils/helpers.js';
|
|
104
|
+
import { makePlaceholders } from './placeholders.js';
|
|
105
|
+
import { trigger } from '../core/events.js';
|
|
106
|
+
/**
|
|
107
|
+
Determines if a value is an observable object.
|
|
108
|
+
|
|
109
|
+
@param value - The value to check.
|
|
110
|
+
@returns `true` if the value is a non-null object, otherwise `false`.
|
|
111
|
+
*/
|
|
112
|
+
function isObservableObject(value) {
|
|
113
|
+
return value != null && typeof value === 'object';
|
|
114
|
+
}
|
|
115
|
+
// repository for observable class wrappers
|
|
116
|
+
const classRepo = {};
|
|
117
|
+
/**
|
|
118
|
+
Creates a new observable wrapper class for the given value's constructor.
|
|
119
|
+
|
|
120
|
+
Returns a subclass of the original constructor that implements the Observable interface,
|
|
121
|
+
allowing observation and notification of changes. The new class is cached in `classRepo`.
|
|
122
|
+
|
|
123
|
+
@param val - The value whose constructor should be wrapped with observable behavior.
|
|
124
|
+
@returns A new Observable class that wraps the original constructor.
|
|
125
|
+
*/
|
|
126
|
+
function makeObservable(val) {
|
|
127
|
+
const Constructor = val.constructor, ConstructorName = Constructor.name;
|
|
128
|
+
const ObservableClass = class extends Constructor {
|
|
129
|
+
#notify = true;
|
|
130
|
+
#callbacks = [];
|
|
131
|
+
constructor(valArg) {
|
|
132
|
+
super(valArg);
|
|
133
|
+
const ctor = this.constructor;
|
|
134
|
+
ctor.callbacks = [];
|
|
135
|
+
if (isObservableObject(valArg)) {
|
|
136
|
+
// you cannot bind native values to the dom
|
|
137
|
+
Object.getOwnPropertyNames(valArg).forEach((key) => {
|
|
138
|
+
this[key] = valArg[key];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
get callbacks() {
|
|
143
|
+
return this.#callbacks;
|
|
144
|
+
}
|
|
145
|
+
set callbacks(callbacks) {
|
|
146
|
+
this.#callbacks = callbacks;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
Coerces the observable to its underlying primitive value when used in
|
|
150
|
+
arithmetic, string interpolation, or other contexts requiring a primitive.
|
|
151
|
+
|
|
152
|
+
This allows `observable + 1` or `` `${observable}` `` to work naturally
|
|
153
|
+
while still exposing `.subscribe()`, `.notify()`, etc. as methods on the object.
|
|
154
|
+
|
|
155
|
+
@param hint - The coercion context: 'number', 'string', or 'default'.
|
|
156
|
+
@returns The underlying primitive value coerced to the requested type.
|
|
157
|
+
|
|
158
|
+
@example
|
|
159
|
+
```ts
|
|
160
|
+
const state = observe({ count: 42 });
|
|
161
|
+
console.log(state.count + 10); // 52 (hint === 'number')
|
|
162
|
+
console.log(`${state.count}`); // "42" (hint === 'string')
|
|
163
|
+
state.count.subscribe(v => console.log('changed to', v)); // still callable
|
|
164
|
+
```
|
|
165
|
+
*/
|
|
166
|
+
[Symbol.toPrimitive](hint) {
|
|
167
|
+
const value = this.valueOf();
|
|
168
|
+
if (hint === 'string')
|
|
169
|
+
return String(value);
|
|
170
|
+
if (hint === 'number')
|
|
171
|
+
return Number(value);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
Registers a callback function to be invoked when the observable object is notified.
|
|
176
|
+
|
|
177
|
+
@param callback - The function to invoke on notification.
|
|
178
|
+
@returns This instance for chaining.
|
|
179
|
+
|
|
180
|
+
@example
|
|
181
|
+
```ts
|
|
182
|
+
const state = observe({ count: 0 });
|
|
183
|
+
state.count.subscribe((v) => console.log('count changed to', v));
|
|
184
|
+
state.count++; // logs: count changed to 1
|
|
185
|
+
```
|
|
186
|
+
*/
|
|
187
|
+
subscribe(callback) {
|
|
188
|
+
this.#callbacks.push(callback);
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
Removes a previously registered callback so it no longer fires on notify.
|
|
193
|
+
|
|
194
|
+
Detaching by the same function reference that was passed to `subscribe()` —
|
|
195
|
+
unknown callbacks are a silent no-op. Long-lived subscribers should call
|
|
196
|
+
this when they are done to avoid leaking callbacks.
|
|
197
|
+
|
|
198
|
+
@param callback - The exact callback reference to remove.
|
|
199
|
+
@returns This instance for chaining.
|
|
200
|
+
|
|
201
|
+
@example
|
|
202
|
+
```ts
|
|
203
|
+
const state = observe({ count: 0 });
|
|
204
|
+
const log = (v) => console.log(v);
|
|
205
|
+
state.count.subscribe(log);
|
|
206
|
+
state.count.unsubscribe(log); // will no longer fire on changes
|
|
207
|
+
```
|
|
208
|
+
*/
|
|
209
|
+
unsubscribe(callback) {
|
|
210
|
+
this.#callbacks = this.#callbacks.filter((registered) => registered !== callback);
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
Binds the observable to a DOM target, setting up placeholder updates and
|
|
215
|
+
triggering initial callbacks.
|
|
216
|
+
|
|
217
|
+
Scans the target and its descendants for placeholders and registers handlers
|
|
218
|
+
to update them with flattened data on notification. Only works in browser environments.
|
|
219
|
+
|
|
220
|
+
@param target - The HTMLElement, DocumentFragment, or ShadowRoot to bind.
|
|
221
|
+
@returns This instance for chaining.
|
|
222
|
+
|
|
223
|
+
@example
|
|
224
|
+
```ts
|
|
225
|
+
const state = observe({ name: 'Alice', count: 0 });
|
|
226
|
+
state.name.bind(document.querySelector('.model')); // updates {name} placeholders in DOM
|
|
227
|
+
```
|
|
228
|
+
*/
|
|
229
|
+
bind(target) {
|
|
230
|
+
if (!isBrowser())
|
|
231
|
+
return; // DOM does not exist on the server side
|
|
232
|
+
const iso = isObservableObject(this.valueOf());
|
|
233
|
+
if (iso) {
|
|
234
|
+
makePlaceholders(target);
|
|
235
|
+
const placeholders = [target, ...target.querySelectorAll('[data-mc]')].filter((e) => {
|
|
236
|
+
// _placeholders is only present on already-decorated Mc host nodes.
|
|
237
|
+
const node = e;
|
|
238
|
+
return !!node?._mc?._placeholders;
|
|
239
|
+
});
|
|
240
|
+
const func = function callback(data) {
|
|
241
|
+
// avoid bubbling for speed
|
|
242
|
+
const flattenedData = flattenObject(data);
|
|
243
|
+
placeholders.forEach((node) => {
|
|
244
|
+
/* v8 ignore next */
|
|
245
|
+
if (!node)
|
|
246
|
+
return;
|
|
247
|
+
trigger(node, 'data', flattenedData);
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
this.#callbacks.push(func);
|
|
251
|
+
this.notify();
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.warn('IGNORED - cannot bind a non-Object to the dom, IS:', typeof this.valueOf(), this);
|
|
255
|
+
}
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
Notifies registered callbacks with the current observable data.
|
|
260
|
+
|
|
261
|
+
Clones the internal value (if it's an object and not an array) before passing
|
|
262
|
+
to callbacks. Respects the internal `#notify` flag that can suppress notifications.
|
|
263
|
+
|
|
264
|
+
@param notify - Optional boolean override for the notification state.
|
|
265
|
+
@returns This instance for chaining.
|
|
266
|
+
|
|
267
|
+
@example
|
|
268
|
+
```ts
|
|
269
|
+
const state = observe({ count: 0 });
|
|
270
|
+
state.count.subscribe((v) => console.log('notified:', v));
|
|
271
|
+
state.count.notify(); // logs: notified: 0
|
|
272
|
+
```
|
|
273
|
+
*/
|
|
274
|
+
notify(notify) {
|
|
275
|
+
const data = isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
|
|
276
|
+
? structuredClone(Object.assign({}, this))
|
|
277
|
+
: this.valueOf();
|
|
278
|
+
if (notify !== undefined) {
|
|
279
|
+
this.#notify = notify;
|
|
280
|
+
// return;
|
|
281
|
+
}
|
|
282
|
+
if (this.#notify) {
|
|
283
|
+
this.#callbacks.forEach((f) => {
|
|
284
|
+
f(data);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
classRepo[ConstructorName] = ObservableClass;
|
|
291
|
+
return ObservableClass;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
Retrieves or creates an observable wrapper class for the given value.
|
|
295
|
+
|
|
296
|
+
@param val - The value whose constructor should be wrapped with observable behavior.
|
|
297
|
+
@returns A class that wraps the original constructor and implements observable functionality.
|
|
298
|
+
*/
|
|
299
|
+
function getConstructor(val) {
|
|
300
|
+
const key = val.constructor.name;
|
|
301
|
+
return classRepo[key] || makeObservable(val);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
Whether a value is already an observable wrapper — `subscribe` is reachable on its prototype chain.
|
|
305
|
+
Used by {@link observe} to skip re-wrapping and to detect the "not yet observed" state on reassignment.
|
|
306
|
+
|
|
307
|
+
@example
|
|
308
|
+
```ts
|
|
309
|
+
const state = observe({ count: 0 });
|
|
310
|
+
isAlreadyObserved(state.count); // true
|
|
311
|
+
isAlreadyObserved(42); // false
|
|
312
|
+
```
|
|
313
|
+
*/
|
|
314
|
+
export function isAlreadyObserved(value) {
|
|
315
|
+
return ((typeof value === 'object' || typeof value === 'function') &&
|
|
316
|
+
value !== null &&
|
|
317
|
+
'subscribe' in value);
|
|
318
|
+
}
|
|
319
|
+
export function observe(target, propertyName) {
|
|
320
|
+
const source = target;
|
|
321
|
+
const keys = propertyName ? [propertyName] : [...Object.getOwnPropertyNames(target)];
|
|
322
|
+
keys.forEach((key) => {
|
|
323
|
+
const current = source[key];
|
|
324
|
+
if (Object.prototype.hasOwnProperty.call(target, key) &&
|
|
325
|
+
typeof current !== 'function' &&
|
|
326
|
+
!isAlreadyObserved(current)) {
|
|
327
|
+
if (key[0] === '_' || current === undefined)
|
|
328
|
+
return;
|
|
329
|
+
const wrappedVal = current;
|
|
330
|
+
let o = new (getConstructor(wrappedVal))(wrappedVal);
|
|
331
|
+
const debouncedNotify = function () {
|
|
332
|
+
queueMicrotask(function () {
|
|
333
|
+
o.notify();
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
Object.defineProperty(target, key, {
|
|
337
|
+
enumerable: true,
|
|
338
|
+
get() {
|
|
339
|
+
if (isObservableObject(o.valueOf())) {
|
|
340
|
+
const snapshot = structuredClone(Object.assign({}, o));
|
|
341
|
+
queueMicrotask(function checkChanges() {
|
|
342
|
+
const currentState = structuredClone(Object.assign({}, o));
|
|
343
|
+
if (!deepEqual(currentState, snapshot)) {
|
|
344
|
+
debouncedNotify();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return o;
|
|
349
|
+
},
|
|
350
|
+
set(val) {
|
|
351
|
+
if (typeof val === typeof o.valueOf()) {
|
|
352
|
+
const iso = isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false;
|
|
353
|
+
if (iso) {
|
|
354
|
+
/* v8 ignore next */
|
|
355
|
+
if (!isAlreadyObserved(o)) {
|
|
356
|
+
/* v8 ignore next */
|
|
357
|
+
o = new (getConstructor(val))(structuredClone(Object.assign({}, val)));
|
|
358
|
+
/* v8 ignore next */
|
|
359
|
+
copyGettersSetters(val, o);
|
|
360
|
+
}
|
|
361
|
+
Object.assign(o, val); // mixin set object into observable object
|
|
362
|
+
o.notify(); // dont test, just assume "changed" status
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
const old = o.valueOf(), callbacks = o.callbacks;
|
|
366
|
+
if (val !== old) {
|
|
367
|
+
o = new (getConstructor(val))(val);
|
|
368
|
+
o.callbacks = callbacks;
|
|
369
|
+
debouncedNotify();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
console.warn('IGNORED - cannot change observable types, IS:', typeof o.valueOf(), o, '- CANNOT BECOME:', typeof val);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
source[key] = wrappedVal; // replace old property with proxy (observable)
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
DOM placeholder binding: `{name}` templates in attributes and text nodes,
|
|
3
|
+
kept up to date from observable data.
|
|
4
|
+
|
|
5
|
+
Split from observables.ts so the pure observable machinery carries no DOM
|
|
6
|
+
scanning/decoration code. The two modules reference each other only inside
|
|
7
|
+
function bodies (`Placeholders` calls `observe(this)`; `bind()` calls
|
|
8
|
+
{@link makePlaceholders}), never during module evaluation, so the circular
|
|
9
|
+
import is benign under ESM live bindings.
|
|
10
|
+
*/
|
|
11
|
+
import { DC, type McDetail, type McHost } from '../core/MC.js';
|
|
12
|
+
import { type Observable } from './observables.js';
|
|
13
|
+
/**
|
|
14
|
+
A class that manages placeholders within an HTML element and updates them dynamically.
|
|
15
|
+
|
|
16
|
+
This class extends `DC` (Data Controller) and processes the target element to identify
|
|
17
|
+
placeholder patterns in attributes and text nodes. It then observes changes on its internal
|
|
18
|
+
data object and updates the corresponding DOM locations when notified of changes.
|
|
19
|
+
*/
|
|
20
|
+
declare class Placeholders extends DC {
|
|
21
|
+
#private;
|
|
22
|
+
data: Observable<Record<string, unknown>>;
|
|
23
|
+
oldDisplay?: string;
|
|
24
|
+
/**
|
|
25
|
+
Creates a new `Placeholders` instance for the given HTML element.
|
|
26
|
+
|
|
27
|
+
Initializes the internal data with empty strings for each placeholder
|
|
28
|
+
found in the target and starts observing the data object. Attaches
|
|
29
|
+
callbacks to update placeholders as needed.
|
|
30
|
+
|
|
31
|
+
@param target - The HTMLElement to process for placeholders.
|
|
32
|
+
*/
|
|
33
|
+
constructor(target: HTMLElement);
|
|
34
|
+
/**
|
|
35
|
+
Handles a `data` custom event to update the internal placeholder data.
|
|
36
|
+
|
|
37
|
+
Extracts matching keys from the event's detail data and updates only those
|
|
38
|
+
properties in the internal `data` object that were previously defined as placeholders.
|
|
39
|
+
|
|
40
|
+
@param ev - The CustomEvent containing new data to merge into the placeholders.
|
|
41
|
+
*/
|
|
42
|
+
onData(ev: CustomEvent<McDetail<Record<string, unknown>>>): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
Applies placeholder support to an element and its descendants.
|
|
46
|
+
|
|
47
|
+
This function scans the provided target for elements containing placeholders
|
|
48
|
+
and attaches a `Placeholders` controller instance to each one if not already present.
|
|
49
|
+
|
|
50
|
+
@param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
|
|
51
|
+
*/
|
|
52
|
+
export declare function makePlaceholders(element: HTMLElement | DocumentFragment | ShadowRoot): void;
|
|
53
|
+
/**
|
|
54
|
+
A host element whose `_mc` registry may carry the `_placeholders` entry that
|
|
55
|
+
{@link makePlaceholders} registers. The entry is optional: candidates are tested
|
|
56
|
+
through this type before being decorated.
|
|
57
|
+
*/
|
|
58
|
+
export type PlaceholdersHost = McHost<HTMLElement, {
|
|
59
|
+
_placeholders?: Placeholders;
|
|
60
|
+
}>;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=placeholders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholders.d.ts","sourceRoot":"","sources":["../../src/state/placeholders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;EASE;AAEF,OAAO,EAAE,EAAE,EAAE,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAoD5D;;;;;;EAME;AACF,cAAM,YAAa,SAAQ,EAAE;;IAE5B,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAwD;IACjG,UAAU,CAAC,EAAE,MAAM,CAAM;IAEzB;;;;;;;;MAQE;gBACU,MAAM,EAAE,WAAW;IAkF/B;;;;;;;MAOE;IACF,MAAM,CAAC,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAOzD;AAmBD;;;;;;;EAOE;AACF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,gBAAgB,GAAG,UAAU,GAAG,IAAI,CAK3F;AAED;;;;EAIE;AACF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,WAAW,EAAE;IAAE,aAAa,CAAC,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
DOM placeholder binding: `{name}` templates in attributes and text nodes,
|
|
3
|
+
kept up to date from observable data.
|
|
4
|
+
|
|
5
|
+
Split from observables.ts so the pure observable machinery carries no DOM
|
|
6
|
+
scanning/decoration code. The two modules reference each other only inside
|
|
7
|
+
function bodies (`Placeholders` calls `observe(this)`; `bind()` calls
|
|
8
|
+
{@link makePlaceholders}), never during module evaluation, so the circular
|
|
9
|
+
import is benign under ESM live bindings.
|
|
10
|
+
*/
|
|
11
|
+
import { DC } from '../core/MC.js';
|
|
12
|
+
import { observe } from './observables.js';
|
|
13
|
+
/**
|
|
14
|
+
Extracts placeholder names from an HTML element's attributes and text content.
|
|
15
|
+
|
|
16
|
+
Placeholders are strings enclosed in curly braces `{}`. This function scans
|
|
17
|
+
all attributes and direct child text nodes of the target element for placeholders
|
|
18
|
+
and returns them as an array of unique names (without braces).
|
|
19
|
+
|
|
20
|
+
@param target - The HTMLElement to scan for placeholders.
|
|
21
|
+
@returns An array of placeholder names found within the element's attributes and text content.
|
|
22
|
+
*/
|
|
23
|
+
/* v8 ignore start */
|
|
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
|
+
const textContent = childNode?.nodeValue || '';
|
|
42
|
+
const contentPlaceholders = textContent.match(regEx);
|
|
43
|
+
contentPlaceholders.forEach((textNodePlaceholder) => placeholders.add(textNodePlaceholder.replace(/[\{\}]/g, '')));
|
|
44
|
+
});
|
|
45
|
+
return [...placeholders];
|
|
46
|
+
}
|
|
47
|
+
/* v8 ignore end */
|
|
48
|
+
// Tag names instead of constructor references (HTMLInputElement etc.):
|
|
49
|
+
// evaluating those globals at module scope crashes any DOM-less import
|
|
50
|
+
// of the package (e.g. server-side use of MC).
|
|
51
|
+
const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
|
|
52
|
+
/**
|
|
53
|
+
A class that manages placeholders within an HTML element and updates them dynamically.
|
|
54
|
+
|
|
55
|
+
This class extends `DC` (Data Controller) and processes the target element to identify
|
|
56
|
+
placeholder patterns in attributes and text nodes. It then observes changes on its internal
|
|
57
|
+
data object and updates the corresponding DOM locations when notified of changes.
|
|
58
|
+
*/
|
|
59
|
+
class Placeholders extends DC {
|
|
60
|
+
// observe(this) in the constructor swaps this for the live observable wrapper.
|
|
61
|
+
data = {};
|
|
62
|
+
oldDisplay = '';
|
|
63
|
+
/**
|
|
64
|
+
Creates a new `Placeholders` instance for the given HTML element.
|
|
65
|
+
|
|
66
|
+
Initializes the internal data with empty strings for each placeholder
|
|
67
|
+
found in the target and starts observing the data object. Attaches
|
|
68
|
+
callbacks to update placeholders as needed.
|
|
69
|
+
|
|
70
|
+
@param target - The HTMLElement to process for placeholders.
|
|
71
|
+
*/
|
|
72
|
+
constructor(target) {
|
|
73
|
+
super(target);
|
|
74
|
+
getPlaceholders(target).forEach((property) => (this.data[property] = ''));
|
|
75
|
+
observe(this);
|
|
76
|
+
this.#attachCallbacks();
|
|
77
|
+
}
|
|
78
|
+
#attachCallbacks() {
|
|
79
|
+
const target = this.target;
|
|
80
|
+
let hide = false;
|
|
81
|
+
// scan attributes
|
|
82
|
+
(target.getAttributeNames ? target.getAttributeNames() : [])
|
|
83
|
+
.filter((attr) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
|
|
84
|
+
.forEach((attr) => {
|
|
85
|
+
const placeholders = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
|
|
86
|
+
// attach callback
|
|
87
|
+
this.data.subscribe(((template) => (data) => {
|
|
88
|
+
let result = template;
|
|
89
|
+
placeholders.forEach((placeholder) => {
|
|
90
|
+
result = result.replace(placeholder,
|
|
91
|
+
// JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
|
|
92
|
+
String(data[placeholder.replace(/[\{\}]/g, '')]));
|
|
93
|
+
});
|
|
94
|
+
target.setAttribute(attr, result);
|
|
95
|
+
})(target.getAttribute(attr) ?? ''));
|
|
96
|
+
});
|
|
97
|
+
// scan children text nodes
|
|
98
|
+
[...target.childNodes]
|
|
99
|
+
.filter((childNode) => {
|
|
100
|
+
return (
|
|
101
|
+
// is text node and has placeholders
|
|
102
|
+
childNode.nodeType === 3 && (childNode.nodeValue || '').match(/\{[^\{\}]*\}/g));
|
|
103
|
+
})
|
|
104
|
+
.forEach((childNode) => {
|
|
105
|
+
hide = true;
|
|
106
|
+
const placeholders = new Set((childNode.nodeValue ?? '').match(/\{[^\{\}]*\}/g));
|
|
107
|
+
// attach callback
|
|
108
|
+
this.data.subscribe(
|
|
109
|
+
/* v8 ignore start */
|
|
110
|
+
((template) => (data) => {
|
|
111
|
+
let result = template;
|
|
112
|
+
placeholders.forEach((placeholder) => {
|
|
113
|
+
result = result.replace(placeholder,
|
|
114
|
+
// JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
|
|
115
|
+
String(data[placeholder.replace(/[\{\}]/g, '')]));
|
|
116
|
+
});
|
|
117
|
+
if (this.oldDisplay) {
|
|
118
|
+
target.style = this.oldDisplay;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
target.removeAttribute('style');
|
|
122
|
+
}
|
|
123
|
+
delete this.oldDisplay;
|
|
124
|
+
childNode.nodeValue = result;
|
|
125
|
+
})(childNode.nodeValue ?? '')
|
|
126
|
+
/* v8 ignore end */
|
|
127
|
+
);
|
|
128
|
+
// hide for now
|
|
129
|
+
this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
|
|
130
|
+
if (hide && !inputTags.includes(target.tagName) && target.tagName !== 'FIELDSET') {
|
|
131
|
+
target.style.display = 'none';
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
Handles a `data` custom event to update the internal placeholder data.
|
|
137
|
+
|
|
138
|
+
Extracts matching keys from the event's detail data and updates only those
|
|
139
|
+
properties in the internal `data` object that were previously defined as placeholders.
|
|
140
|
+
|
|
141
|
+
@param ev - The CustomEvent containing new data to merge into the placeholders.
|
|
142
|
+
*/
|
|
143
|
+
onData(ev) {
|
|
144
|
+
const data = {};
|
|
145
|
+
Object.keys(ev.detail.data).forEach((key) => {
|
|
146
|
+
if (this.data.hasOwnProperty(key))
|
|
147
|
+
data[key] = ev.detail.data[key];
|
|
148
|
+
});
|
|
149
|
+
if (JSON.stringify(data) !== JSON.stringify(this.data))
|
|
150
|
+
Object.assign(this.data, data); // will trigger callbacks
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
Finds all elements within a given scope that contain placeholders.
|
|
155
|
+
|
|
156
|
+
@param target - The HTMLElement, DocumentFragment, or ShadowRoot to search.
|
|
157
|
+
@returns An array of HTMLElements containing placeholder patterns.
|
|
158
|
+
*/
|
|
159
|
+
function getPlaceholderElements(target) {
|
|
160
|
+
const elements = [target]
|
|
161
|
+
.concat([...target.querySelectorAll('*')])
|
|
162
|
+
.filter((e) => !!getPlaceholders(e).length)
|
|
163
|
+
.filter((e) => e?.tagName !== 'STYLE')
|
|
164
|
+
.reverse(); // to start with inner elements
|
|
165
|
+
return elements;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
Applies placeholder support to an element and its descendants.
|
|
169
|
+
|
|
170
|
+
This function scans the provided target for elements containing placeholders
|
|
171
|
+
and attaches a `Placeholders` controller instance to each one if not already present.
|
|
172
|
+
|
|
173
|
+
@param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
|
|
174
|
+
*/
|
|
175
|
+
export function makePlaceholders(element) {
|
|
176
|
+
getPlaceholderElements(element).forEach((e) => {
|
|
177
|
+
if (e?._mc?._placeholders)
|
|
178
|
+
return;
|
|
179
|
+
DC.add(e, '_placeholders', new Placeholders(e));
|
|
180
|
+
});
|
|
181
|
+
}
|