@twobirds/microcomponents 0.1.21 → 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.
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
@@ -1,449 +0,0 @@
1
- import { LooseObject, flattenObject, debounce, deepEqual, copyGettersSetters } from './helpers.js';
2
- import { HTMLMcElement, DC } from './MC.js';
3
- import { trigger } from './events.js';
4
-
5
- /**
6
- Determines if a value is an observable object.
7
-
8
- @param value - The value to check.
9
- @returns `true` if the value is a non-null object, otherwise `false`.
10
- */
11
- function isObservableObject(value: any): boolean {
12
- return value != null && typeof value === 'object';
13
- }
14
-
15
- type Callback = (value: any) => void;
16
-
17
- /**
18
- Extracts placeholder names from an HTML element's attributes and text content.
19
-
20
- Placeholders are strings enclosed in curly braces `{}`. This function scans
21
- all attributes and direct child text nodes of the target element for placeholders
22
- and returns them as an array of unique names (without braces).
23
-
24
- @param target - The HTMLElement to scan for placeholders.
25
- @returns An array of placeholder names found within the element's attributes and text content.
26
- */
27
- function getPlaceholders(target: HTMLElement): string[] {
28
- let placeholders: Set<string> = new Set(),
29
- regEx = /\{[^\{\}]*\}/g;
30
-
31
- // scan attributes
32
- (target.getAttributeNames ? target.getAttributeNames() : [])
33
- .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
34
- .forEach((attr) => {
35
- let attrPlaceholders: Set<string> = new Set(target.getAttribute(attr)?.match(regEx));
36
- attrPlaceholders.forEach((attrPlaceholder: string) =>
37
- placeholders.add(attrPlaceholder.replace(/[\{\}]/g, ''))
38
- );
39
- });
40
-
41
- // scan children text nodes
42
- [...target.childNodes]
43
- .filter((childNode) => {
44
- return (
45
- // is text node and has placeholders
46
- childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(regEx)
47
- );
48
- })
49
- .forEach((childNode) => {
50
- let textContent: string = childNode?.nodeValue || '';
51
- let contentPlaceholders = textContent.match(regEx) as string[];
52
- contentPlaceholders.forEach((textNodePlaceholder) =>
53
- placeholders.add(textNodePlaceholder.replace(/[\{\}]/g, ''))
54
- );
55
- });
56
-
57
- return [...placeholders];
58
- }
59
-
60
- const inputs = [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement];
61
-
62
- /**
63
- A class that manages placeholders within an HTML element and updates them dynamically.
64
-
65
- This class extends `DC` (Data Controller) and processes the target element to identify
66
- placeholder patterns in attributes and text nodes. It then observes changes on its internal
67
- data object and updates the corresponding DOM locations when notified of changes.
68
- */
69
- class Placeholders extends DC {
70
- data: LooseObject = {};
71
- oldDisplay?: string = '';
72
-
73
- /**
74
- Creates a new `Placeholders` instance for the given HTML element.
75
-
76
- Initializes the internal data with empty strings for each placeholder
77
- found in the target and starts observing the data object. Attaches
78
- callbacks to update placeholders as needed.
79
-
80
- @param target - The HTMLElement to process for placeholders.
81
- */
82
- constructor(target: HTMLElement) {
83
- super(target);
84
- getPlaceholders(target).forEach((property) => (this.data[property] = ''));
85
- observe(this);
86
- this.#attachCallbacks();
87
- }
88
-
89
- #attachCallbacks() {
90
- let target = this.target,
91
- hide: boolean = false;
92
-
93
- // scan attributes
94
- (target.getAttributeNames ? target.getAttributeNames() : [])
95
- .filter((attr: string) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
96
- .forEach((attr: string) => {
97
- let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
98
- // attach callback
99
- this.data.subscribe(
100
- ((template: string) => (data: LooseObject) => {
101
- let result: string = template;
102
- placeholders.forEach((placeholder) => {
103
- result = result.replace(
104
- placeholder,
105
- // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
106
- data[placeholder.replace(/[\{\}]/g, '')]
107
- );
108
- });
109
- target.setAttribute(attr, result);
110
- })(target.getAttribute(attr))
111
- );
112
- });
113
-
114
- // scan children text nodes
115
- [...target.childNodes]
116
- .filter((childNode) => {
117
- return (
118
- // is text node and has placeholders
119
- childNode.nodeType === 3 && ((childNode as Node).nodeValue || '').match(/\{[^\{\}]*\}/g)
120
- );
121
- })
122
- .forEach((childNode) => {
123
- hide = true;
124
- let placeholders: Set<string> = new Set(childNode.nodeValue.match(/\{[^\{\}]*\}/g));
125
-
126
- // attach callback
127
- this.data.subscribe(
128
- ((template: string) => (data: LooseObject) => {
129
- let result: string = template;
130
- placeholders.forEach((placeholder) => {
131
- result = result.replace(
132
- placeholder,
133
- // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
134
- data[placeholder.replace(/[\{\}]/g, '')]
135
- );
136
- });
137
-
138
- if (this.oldDisplay) {
139
- target.style = this.oldDisplay;
140
- } else {
141
- target.removeAttribute('style');
142
- }
143
- delete this.oldDisplay;
144
-
145
- childNode.nodeValue = result;
146
- })(childNode.nodeValue)
147
- );
148
-
149
- // hide for now
150
- this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
151
-
152
- if (
153
- hide &&
154
- !inputs.includes((target as any).constructor) &&
155
- target.tagName !== 'FIELDSET'
156
- ) {
157
- target.style.display = 'none';
158
- }
159
- });
160
- }
161
-
162
- /**
163
- Handles a `data` custom event to update the internal placeholder data.
164
-
165
- Extracts matching keys from the event's detail data and updates only those
166
- properties in the internal `data` object that were previously defined as placeholders.
167
-
168
- @param ev - The CustomEvent containing new data to merge into the placeholders.
169
- */
170
- onData(ev: CustomEvent) {
171
- let data: LooseObject = {};
172
- Object.keys(ev.detail.data).forEach((key) => {
173
- if (this.data.hasOwnProperty(key)) data[key] = ev.detail.data[key];
174
- });
175
- if (JSON.stringify(data) !== JSON.stringify(this.data)) Object.assign(this.data, data); // will trigger callbacks
176
- }
177
- }
178
-
179
- /**
180
- Finds all elements within a given scope that contain placeholders.
181
-
182
- @param target - The HTMLElement, DocumentFragment, or ShadowRoot to search.
183
- @returns An array of HTMLElements containing placeholder patterns.
184
- */
185
- function getPlaceholderElements(
186
- target: HTMLElement | DocumentFragment | ShadowRoot
187
- ): HTMLElement[] {
188
- let elements: Array<Node> = [target]
189
- .concat([...(target.querySelectorAll('*') as unknown as HTMLElement[])])
190
- .filter((e) => !!getPlaceholders(e as HTMLElement).length)
191
- .filter((e) => (e as HTMLElement)?.tagName !== 'STYLE')
192
- .reverse(); // to start with inner elements
193
- return elements as HTMLElement[];
194
- }
195
-
196
- /**
197
- Applies placeholder support to an element and its descendants.
198
-
199
- This function scans the provided target for elements containing placeholders
200
- and attaches a `Placeholders` controller instance to each one if not already present.
201
-
202
- @param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
203
- */
204
- function makePlaceholders(element: HTMLElement | DocumentFragment | ShadowRoot): void {
205
- getPlaceholderElements(element).forEach((e: HTMLElement) => {
206
- if ((e as HTMLMcElement)?._mc?._placeholders) return;
207
- DC.add(e, '_placeholders', new Placeholders(e));
208
- });
209
- }
210
-
211
- /**
212
- Interface representing an observable object that supports subscription, notification, and DOM binding.
213
-
214
- Observable objects track registered callbacks and invoke them when their internal state changes.
215
- They can also be bound to a DOM target to automatically update placeholder values in the view layer.
216
- */
217
- export type Observable = {
218
- subscribe: (cb: Callback) => void; // Registers a callback function to be invoked when the observable is notified.
219
- notify: () => void; // Notifies all registered callbacks with the current observable data.
220
- bind: (target: HTMLElement | DocumentFragment | ShadowRoot) => void; // Binds the observable to a DOM target, setting up placeholder updates.
221
- callbacks: Array<Function>; // Array of registered callback functions.
222
- };
223
-
224
- type Class<T> = new (...args: any[]) => T;
225
-
226
- // repository for observable class wrappers
227
- const classRepo: LooseObject = {};
228
-
229
- /**
230
- Creates a new observable wrapper class for the given value's constructor.
231
-
232
- Returns a subclass of the original constructor that implements the Observable interface,
233
- allowing observation and notification of changes. The new class is cached in `classRepo`.
234
-
235
- @param val - The value whose constructor should be wrapped with observable behavior.
236
- @returns A new Observable class that wraps the original constructor.
237
- */
238
- function makeObservable(val: any): Class<typeof val> {
239
- const Constructor = val.constructor,
240
- ConstructorName: string = Constructor.name;
241
-
242
- let ObservableClass = class extends Constructor {
243
- #notify: boolean = true;
244
- #callbacks: Array<Function> = [];
245
-
246
- constructor(val: any) {
247
- super(val);
248
- (this.constructor as LooseObject).callbacks = [];
249
- if (isObservableObject(val)) {
250
- // you cannot bind native values to the dom
251
- Object.getOwnPropertyNames(val).forEach((key) => {
252
- this[key] = val[key];
253
- });
254
- }
255
- }
256
-
257
- get callbacks(): Array<Function> {
258
- return this.#callbacks;
259
- }
260
-
261
- set callbacks(callbacks: Array<Function>) {
262
- this.#callbacks = callbacks;
263
- }
264
-
265
- /**
266
- Registers a callback function to be invoked when the observable object is notified.
267
-
268
- @param callback - The function to invoke on notification.
269
- @returns This instance for chaining.
270
- */
271
- subscribe(callback: Function) {
272
- this.#callbacks.push(callback);
273
- return this;
274
- }
275
-
276
- /**
277
- Binds the observable to a DOM target, setting up placeholder updates and
278
- triggering initial callbacks.
279
-
280
- Scans the target and its descendants for placeholders and registers handlers
281
- to update them with flattened data on notification.
282
-
283
- @param target - The HTMLElement, DocumentFragment, or ShadowRoot to bind.
284
- @returns This instance for chaining.
285
- */
286
- bind(target: HTMLElement | DocumentFragment | ShadowRoot) {
287
- const iso = isObservableObject(this.valueOf());
288
-
289
- if (iso) {
290
- makePlaceholders(target);
291
- const placeholders: unknown = [target, ...target.querySelectorAll('[data-mc]')].filter(
292
- (e) => !!(e as LooseObject)?._mc?._placeholders
293
- );
294
-
295
- const func = function callback(data: LooseObject) {
296
- // avoid bubbling for speed
297
- let flattenedData = flattenObject(data);
298
- (placeholders as Node[]).forEach((node) => {
299
- // console.log('trigger callback', e);
300
- if (!node) return;
301
- trigger(node as HTMLMcElement, 'data', flattenedData);
302
- });
303
- };
304
- (this as any).#callbacks.push(func);
305
- this.notify();
306
- } else {
307
- console.warn(
308
- 'IGNORED - cannot bind a non-Object to the dom, IS:',
309
- typeof this.valueOf(),
310
- this
311
- );
312
- }
313
-
314
- return this;
315
- }
316
-
317
- /**
318
- Notifies registered callbacks with the current observable data.
319
-
320
- Clones the internal value (if it's an object and not an array) before passing
321
- to callbacks. Respects the internal `#notify` flag that can suppress notifications.
322
-
323
- @param notify - Optional boolean override for the notification state.
324
- @returns This instance for chaining.
325
- */
326
- notify(notify?: boolean) {
327
- let data =
328
- isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
329
- ? structuredClone(Object.assign({}, this))
330
- : this.valueOf();
331
-
332
- if (notify !== undefined) {
333
- this.#notify = notify;
334
- // return;
335
- }
336
-
337
- if (this.#notify) {
338
- (this as any).#callbacks.forEach((f: Function) => {
339
- f(data);
340
- });
341
- }
342
-
343
- return this;
344
- }
345
- };
346
-
347
- (classRepo as any)[ConstructorName] = ObservableClass;
348
-
349
- return ObservableClass;
350
- }
351
-
352
- /**
353
- Retrieves or creates an observable wrapper class for the given value.
354
-
355
- @param val - The value whose constructor should be wrapped with observable behavior.
356
- @returns A class that wraps the original constructor and implements observable functionality.
357
- */
358
- function getConstructor(val: any): Class<typeof val> {
359
- let key: string = val.constructor.name;
360
- return classRepo[key] || makeObservable(val);
361
- }
362
-
363
- /**
364
- Makes a target object's properties observable by replacing them with wrapped values.
365
-
366
- Wraps each property of `target` that is not already observable (and meets criteria)
367
- in an observable wrapper. Also sets up deep change detection for objects and
368
- debounced notifications when the data changes.
369
-
370
- @param target - The LooseObject whose properties should be made observable.
371
- @param propertyName - Optional specific property name to make one property observable instead of all properties.
372
- @returns The original target object, now with observable properties.
373
- */
374
- export const observe = (target: LooseObject, propertyName?: string): LooseObject => {
375
- let keys = !propertyName ? [...Object.getOwnPropertyNames(target)] : [propertyName];
376
-
377
- keys.forEach((key) => {
378
- if (
379
- target.hasOwnProperty(key) &&
380
- typeof target[key] !== 'function' &&
381
- !target?.[key]?.constructor?.prototype?.subscribe
382
- ) {
383
- if (key[0] === '_' || target?.[key] === undefined) return target;
384
- let val: any = target[key];
385
-
386
- let o = new (getConstructor(val) as Class<typeof val>)(val);
387
-
388
- let debouncedNotify: Function = debounce(function debouncedNotify() {
389
- o.notify();
390
- }, 0);
391
-
392
- Object.defineProperty(target, key, {
393
- enumerable: true,
394
- get() {
395
- if (isObservableObject(o.valueOf())) {
396
- let check = structuredClone(Object.assign({}, o));
397
- setTimeout(function checkChanges() {
398
- let state = structuredClone(Object.assign({}, o));
399
- if (!deepEqual(state, check)) {
400
- debouncedNotify();
401
- }
402
- }, 0);
403
- }
404
- return o;
405
- },
406
- set(val: any) {
407
- if (typeof val === typeof o.valueOf()) {
408
- let iso = isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false;
409
- if (iso) {
410
- // is observable object
411
- if (o?.constructor?.prototype?.subscribe === undefined) {
412
- // not observed yet
413
- o = new (getConstructor(val) as Class<typeof val>)(
414
- structuredClone(Object.assign({}, val))
415
- );
416
- copyGettersSetters(val, o);
417
- //o.seal();
418
- }
419
- Object.assign(o, val); // mixin set object into observable object
420
- o.notify(); // dont test, just assume "changed" status
421
- } else {
422
- // simple native var
423
- let old = o.valueOf(),
424
- callbacks = (o as Observable).callbacks;
425
-
426
- if (val !== old) {
427
- o = new (getConstructor(val) as Class<typeof val>)(val);
428
- (o as Observable).callbacks = callbacks;
429
- debouncedNotify();
430
- }
431
- }
432
- } else {
433
- // set type is not stored type
434
- console.warn(
435
- 'IGNORED - cannot change observable types, IS:',
436
- typeof o.valueOf(),
437
- o,
438
- '- CANNOT BECOME:',
439
- typeof val
440
- );
441
- }
442
- },
443
- });
444
- target[key] = val; // replace old property with proxy (observable)
445
- }
446
- });
447
-
448
- return target;
449
- };