@twobirds/microcomponents 0.1.16 → 0.1.19

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.
@@ -5,169 +5,159 @@ import { LooseObject, debounce, deepEqual, flattenObject, copyGettersSetters } f
5
5
  import { DC, HTMLMcElement, trigger } from './MC.js';
6
6
 
7
7
  function isObservableObject(value: any): boolean {
8
- return value != null && typeof value === 'object';
8
+ return value != null && typeof value === 'object';
9
9
  }
10
10
 
11
11
  type Callback = (value: any) => void;
12
12
 
13
13
  function getPlaceholders(target: HTMLElement): string[] {
14
- let placeholders: Set<string> = new Set(),
15
- regEx = /\{[^\{\}]*\}/g;
16
-
17
- // scan attributes
18
- (target.getAttributeNames ? target.getAttributeNames() : [])
19
- .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
20
- .forEach((attr) => {
21
- let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(regEx));
22
- placeholders.forEach((placeholder) =>
23
- placeholders.add(placeholder.replace(/[\{\}]/g, ''))
24
- );
25
- });
26
-
27
- // scan children text nodes
28
- [...target.childNodes]
29
- .filter((childNode) => {
30
- return (
31
- // is text node and has placeholders
32
- childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(regEx)
33
- );
34
- })
35
- .forEach((childNode) => {
36
- ((childNode as any).nodeValue || '').match(regEx).forEach((placeholder: string) => {
37
- placeholders.add(placeholder.replace(/[\{\}]/g, ''));
38
- });
39
- });
40
-
41
- // extract {} and return result
42
- return [...placeholders].map((placeholder) => placeholder.replace(/[\{\}]/g, ''));
14
+ let placeholders: Set<string> = new Set(),
15
+ regEx = /\{[^\{\}]*\}/g;
16
+
17
+ // scan attributes
18
+ (target.getAttributeNames ? target.getAttributeNames() : [])
19
+ .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
20
+ .forEach((attr) => {
21
+ let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(regEx));
22
+ placeholders.forEach((placeholder) => placeholders.add(placeholder.replace(/[\{\}]/g, '')));
23
+ });
24
+
25
+ // scan children text nodes
26
+ [...target.childNodes]
27
+ .filter((childNode) => {
28
+ return (
29
+ // is text node and has placeholders
30
+ childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(regEx)
31
+ );
32
+ })
33
+ .forEach((childNode) => {
34
+ ((childNode as any).nodeValue || '').match(regEx).forEach((placeholder: string) => {
35
+ placeholders.add(placeholder.replace(/[\{\}]/g, ''));
36
+ });
37
+ });
38
+
39
+ // extract {} and return result
40
+ return [...placeholders].map((placeholder) => placeholder.replace(/[\{\}]/g, ''));
43
41
  }
44
42
 
45
43
  const inputs = [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement];
46
44
 
47
45
  class Placeholders extends DC {
48
- data: LooseObject = {};
49
- oldDisplay?: string = '';
50
-
51
- constructor(target: HTMLElement) {
52
- super(target);
53
- getPlaceholders(target).forEach((property) => (this.data[property] = ''));
54
- observe(this);
55
- this.#attachCallbacks();
56
- }
57
-
58
- #attachCallbacks() {
59
- let target = this.target,
60
- hide: boolean = false;
61
-
62
- //console.log('attachCallbacks', this.target);
63
-
64
- // scan attributes
65
- (target.getAttributeNames ? target.getAttributeNames() : [])
66
- .filter((attr: string) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
67
- .forEach((attr: string) => {
68
- let placeholders: Set<string> = new Set(
69
- target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)
70
- );
71
- // attach callback
72
- this.data.observe(
73
- ((template: string) => (data: LooseObject) => {
74
- let result: string = template;
75
- placeholders.forEach((placeholder) => {
76
- result = result.replace(
77
- placeholder,
78
- data[placeholder.replace(/[\{\}]/g, '')]
79
- );
80
- });
81
- target.setAttribute(attr, result);
82
- //console.log('cb setAttribute()', data, template, attr, result);
83
- })(target.getAttribute(attr))
84
- );
85
- });
86
-
87
- // scan children text nodes
88
- [...target.childNodes]
89
- .filter((childNode) => {
90
- return (
91
- // is text node and has placeholders
92
- childNode.nodeType === 3 &&
93
- ((childNode as Node).nodeValue || '').match(/\{[^\{\}]*\}/g)
94
- );
95
- })
96
- .forEach((childNode) => {
97
- hide = true;
98
- let placeholders: Set<string> = new Set(childNode.nodeValue.match(/\{[^\{\}]*\}/g));
99
-
100
- // attach callback
101
- this.data.observe(
102
- ((template: string) => (data: LooseObject) => {
103
- let result: string = template;
104
- //console.log('text node "', template, '" callback data:', data);
105
- placeholders.forEach((placeholder) => {
106
- result = result.replace(
107
- placeholder,
108
- data[placeholder.replace(/[\{\}]/g, '')]
109
- );
110
- });
111
-
112
- if (this.oldDisplay) {
113
- target.style = this.oldDisplay;
114
- } else {
115
- target.removeAttribute('style');
116
- }
117
- delete this.oldDisplay;
118
-
119
- childNode.nodeValue = result;
120
- })(childNode.nodeValue)
121
- );
122
-
123
- // hide for now
124
- this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
125
-
126
- if (
127
- hide &&
128
- !inputs.includes((target as any).constructor) &&
129
- target.tagName !== 'FIELDSET'
130
- ) {
131
- target.style.display = 'none';
132
- }
133
- });
134
- }
135
-
136
- onData(ev: CustomEvent) {
137
- let data: LooseObject = {};
138
- Object.keys(ev.detail.data).forEach((key) => {
139
- if (this.data.hasOwnProperty(key)) data[key] = ev.detail.data[key];
140
- });
141
- //console.log( 'Placeholder onData', ev, data );
142
- if (JSON.stringify(data) !== JSON.stringify(this.data)) Object.assign(this.data, data); // will trigger callbacks
143
- }
46
+ data: LooseObject = {};
47
+ oldDisplay?: string = '';
48
+
49
+ constructor(target: HTMLElement) {
50
+ super(target);
51
+ getPlaceholders(target).forEach((property) => (this.data[property] = ''));
52
+ observe(this);
53
+ this.#attachCallbacks();
54
+ }
55
+
56
+ #attachCallbacks() {
57
+ let target = this.target,
58
+ hide: boolean = false;
59
+
60
+ // scan attributes
61
+ (target.getAttributeNames ? target.getAttributeNames() : [])
62
+ .filter((attr: string) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
63
+ .forEach((attr: string) => {
64
+ let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
65
+ // attach callback
66
+ this.data.observe(
67
+ ((template: string) => (data: LooseObject) => {
68
+ let result: string = template;
69
+ placeholders.forEach((placeholder) => {
70
+ result = result.replace(
71
+ placeholder,
72
+ // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
73
+ data[placeholder.replace(/[\{\}]/g, '')]
74
+ );
75
+ });
76
+ target.setAttribute(attr, result);
77
+ })(target.getAttribute(attr))
78
+ );
79
+ });
80
+
81
+ // scan children text nodes
82
+ [...target.childNodes]
83
+ .filter((childNode) => {
84
+ return (
85
+ // is text node and has placeholders
86
+ childNode.nodeType === 3 && ((childNode as Node).nodeValue || '').match(/\{[^\{\}]*\}/g)
87
+ );
88
+ })
89
+ .forEach((childNode) => {
90
+ hide = true;
91
+ let placeholders: Set<string> = new Set(childNode.nodeValue.match(/\{[^\{\}]*\}/g));
92
+
93
+ // attach callback
94
+ this.data.observe(
95
+ ((template: string) => (data: LooseObject) => {
96
+ let result: string = template;
97
+ placeholders.forEach((placeholder) => {
98
+ result = result.replace(
99
+ placeholder,
100
+ // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
101
+ data[placeholder.replace(/[\{\}]/g, '')]
102
+ );
103
+ });
104
+
105
+ if (this.oldDisplay) {
106
+ target.style = this.oldDisplay;
107
+ } else {
108
+ target.removeAttribute('style');
109
+ }
110
+ delete this.oldDisplay;
111
+
112
+ childNode.nodeValue = result;
113
+ })(childNode.nodeValue)
114
+ );
115
+
116
+ // hide for now
117
+ this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
118
+
119
+ if (
120
+ hide &&
121
+ !inputs.includes((target as any).constructor) &&
122
+ target.tagName !== 'FIELDSET'
123
+ ) {
124
+ target.style.display = 'none';
125
+ }
126
+ });
127
+ }
128
+
129
+ onData(ev: CustomEvent) {
130
+ let data: LooseObject = {};
131
+ Object.keys(ev.detail.data).forEach((key) => {
132
+ if (this.data.hasOwnProperty(key)) data[key] = ev.detail.data[key];
133
+ });
134
+ if (JSON.stringify(data) !== JSON.stringify(this.data)) Object.assign(this.data, data); // will trigger callbacks
135
+ }
144
136
  }
145
137
 
146
138
  function getPlaceholderElements(
147
- target: HTMLElement | DocumentFragment | ShadowRoot
139
+ target: HTMLElement | DocumentFragment | ShadowRoot
148
140
  ): HTMLElement[] {
149
- let elements: Array<Node> = [target]
150
- .concat([...(target.querySelectorAll('*') as unknown as HTMLElement[])])
151
- .filter((e) => !!getPlaceholders(e as HTMLElement).length)
152
- .filter((e) => (e as HTMLElement)?.tagName !== 'STYLE')
153
- .reverse(); // to start with inner elements
154
- //console.log('getPlaceholderElements', elements);
155
- return elements as HTMLElement[];
141
+ let elements: Array<Node> = [target]
142
+ .concat([...(target.querySelectorAll('*') as unknown as HTMLElement[])])
143
+ .filter((e) => !!getPlaceholders(e as HTMLElement).length)
144
+ .filter((e) => (e as HTMLElement)?.tagName !== 'STYLE')
145
+ .reverse(); // to start with inner elements
146
+ return elements as HTMLElement[];
156
147
  }
157
148
 
158
149
  function makePlaceholders(element: HTMLElement | DocumentFragment | ShadowRoot): void {
159
- //console.log('makePlaceholders', element);
160
- getPlaceholderElements(element).forEach((e: HTMLElement) => {
161
- if ((e as HTMLMcElement)?._mc?._placeholders) return;
162
- DC.add(e, '_placeholders', new Placeholders(e));
163
- });
150
+ getPlaceholderElements(element).forEach((e: HTMLElement) => {
151
+ if ((e as HTMLMcElement)?._mc?._placeholders) return;
152
+ DC.add(e, '_placeholders', new Placeholders(e));
153
+ });
164
154
  }
165
155
 
166
156
  export type Observable = {
167
- observe: (cb: Callback) => void;
168
- notify: () => void;
169
- bind: (target: HTMLElement | DocumentFragment | ShadowRoot) => void;
170
- callbacks: Array<Function>;
157
+ observe: (cb: Callback) => void;
158
+ notify: () => void;
159
+ bind: (target: HTMLElement | DocumentFragment | ShadowRoot) => void;
160
+ callbacks: Array<Function>;
171
161
  };
172
162
 
173
163
  type Class<T> = new (...args: any[]) => T;
@@ -177,192 +167,172 @@ const classRepo: LooseObject = {};
177
167
 
178
168
  // creates a new observable class wrapper
179
169
  function makeObservable(val: any): Class<typeof val> {
180
- const Constructor = val.constructor,
181
- ConstructorName: string = Constructor.name;
182
-
183
- let ObservableClass = class extends Constructor {
184
- #notify: boolean = true;
185
- #callbacks: Array<Function> = [];
186
-
187
- constructor(val: any) {
188
- super(val);
189
- (this.constructor as LooseObject).callbacks = [];
190
- if (isObservableObject(val)) {
191
- // you cannot bind native values to the dom
192
- Object.getOwnPropertyNames(val).forEach((key) => {
193
- this[key] = val[key];
194
- });
195
- }
196
- }
197
-
198
- get callbacks(): Array<Function> {
199
- return this.#callbacks;
200
- }
201
-
202
- set callbacks(callbacks: Array<Function>) {
203
- this.#callbacks = callbacks;
204
- }
205
-
206
- observe(callback: Function) {
207
- //console.log('observe');
208
- this.#callbacks.push(callback);
209
- return this;
210
- }
211
-
212
- bind(target: HTMLElement | DocumentFragment | ShadowRoot) {
213
- const iso = isObservableObject(this.valueOf());
214
- //console.log('bind', iso, target);
215
-
216
- if (iso) {
217
- makePlaceholders(target);
218
- const placeholders: unknown = [
219
- target,
220
- ...target.querySelectorAll('[data-mc]'),
221
- ].filter((e) => !!(e as LooseObject)?._mc?._placeholders);
222
-
223
- //console.log('placeholders:', target, placeholders);
224
-
225
- const func = function callback(data: LooseObject) {
226
- // avoid bubbling for speed
227
- let flattenedData = flattenObject(data);
228
- //console.log('inside placeholder callback', target, flattenedData);
229
- (placeholders as Node[]).forEach((node) => {
230
- // console.log('trigger callback', e);
231
- if (!node) return;
232
- trigger(node as HTMLMcElement, 'data', flattenedData);
233
- });
234
- };
235
- (this as any).#callbacks.push(func);
236
- this.notify();
237
- } else {
238
- console.warn(
239
- 'IGNORED - cannot bind a non-Object to the dom, IS:',
240
- typeof this.valueOf(),
241
- this
242
- );
243
- }
244
-
245
- return this;
246
- }
247
-
248
- notify(notify?: boolean) {
249
- let data =
250
- isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
251
- ? structuredClone(Object.assign({}, this))
252
- : this.valueOf();
253
-
254
- if (notify !== undefined) {
255
- this.#notify = notify;
256
- // return;
257
- }
258
-
259
- if (this.#notify) {
260
- (this as any).#callbacks.forEach((f: Function) => {
261
- f(data);
262
- });
263
- }
264
-
265
- return this;
266
- }
267
- };
268
-
269
- (classRepo as any)[ConstructorName] = ObservableClass;
270
-
271
- // console.log( 'classRepo:', classRepo );
272
-
273
- return ObservableClass;
170
+ const Constructor = val.constructor,
171
+ ConstructorName: string = Constructor.name;
172
+
173
+ let ObservableClass = class extends Constructor {
174
+ #notify: boolean = true;
175
+ #callbacks: Array<Function> = [];
176
+
177
+ constructor(val: any) {
178
+ super(val);
179
+ (this.constructor as LooseObject).callbacks = [];
180
+ if (isObservableObject(val)) {
181
+ // you cannot bind native values to the dom
182
+ Object.getOwnPropertyNames(val).forEach((key) => {
183
+ this[key] = val[key];
184
+ });
185
+ }
186
+ }
187
+
188
+ get callbacks(): Array<Function> {
189
+ return this.#callbacks;
190
+ }
191
+
192
+ set callbacks(callbacks: Array<Function>) {
193
+ this.#callbacks = callbacks;
194
+ }
195
+
196
+ observe(callback: Function) {
197
+ this.#callbacks.push(callback);
198
+ return this;
199
+ }
200
+
201
+ bind(target: HTMLElement | DocumentFragment | ShadowRoot) {
202
+ const iso = isObservableObject(this.valueOf());
203
+
204
+ if (iso) {
205
+ makePlaceholders(target);
206
+ const placeholders: unknown = [target, ...target.querySelectorAll('[data-mc]')].filter(
207
+ (e) => !!(e as LooseObject)?._mc?._placeholders
208
+ );
209
+
210
+ const func = function callback(data: LooseObject) {
211
+ // avoid bubbling for speed
212
+ let flattenedData = flattenObject(data);
213
+ (placeholders as Node[]).forEach((node) => {
214
+ // console.log('trigger callback', e);
215
+ if (!node) return;
216
+ trigger(node as HTMLMcElement, 'data', flattenedData);
217
+ });
218
+ };
219
+ (this as any).#callbacks.push(func);
220
+ this.notify();
221
+ } else {
222
+ console.warn(
223
+ 'IGNORED - cannot bind a non-Object to the dom, IS:',
224
+ typeof this.valueOf(),
225
+ this
226
+ );
227
+ }
228
+
229
+ return this;
230
+ }
231
+
232
+ notify(notify?: boolean) {
233
+ let data =
234
+ isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
235
+ ? structuredClone(Object.assign({}, this))
236
+ : this.valueOf();
237
+
238
+ if (notify !== undefined) {
239
+ this.#notify = notify;
240
+ // return;
241
+ }
242
+
243
+ if (this.#notify) {
244
+ (this as any).#callbacks.forEach((f: Function) => {
245
+ f(data);
246
+ });
247
+ }
248
+
249
+ return this;
250
+ }
251
+ };
252
+
253
+ (classRepo as any)[ConstructorName] = ObservableClass;
254
+
255
+ return ObservableClass;
274
256
  }
275
257
 
276
258
  function getConstructor(val: any): Class<typeof val> {
277
- let key: string = val.constructor.name;
278
- return classRepo[key] || makeObservable(val);
259
+ let key: string = val.constructor.name;
260
+ return classRepo[key] || makeObservable(val);
279
261
  }
280
262
 
281
263
  export const observe = (target: LooseObject, propertyName?: string): LooseObject => {
282
- let keys = !propertyName ? [...Object.getOwnPropertyNames(target)] : [propertyName];
283
-
284
- keys.forEach((key) => {
285
- if (
286
- target.hasOwnProperty(key) &&
287
- typeof target[key] !== 'function' &&
288
- !target?.[key]?.constructor?.prototype?.observe
289
- ) {
290
- if (key[0] === '_' || target?.[key] === undefined) return target;
291
- let val: any = target[key];
292
-
293
- let o = new (getConstructor(val) as Class<typeof val>)(val);
294
-
295
- let debouncedNotify: Function = debounce(function debouncedNotify() {
296
- o.notify();
297
- }, 0);
298
-
299
- Object.defineProperty(target, key, {
300
- enumerable: true,
301
- get() {
302
- if (isObservableObject(o.valueOf())) {
303
- let check = structuredClone(Object.assign({}, o));
304
- setTimeout(function checkChanges() {
305
- let state = structuredClone(Object.assign({}, o));
306
- console.log(
307
- 'compare',
308
- check,
309
- 'to',
310
- state,
311
- 'with',
312
- o.callbacks,
313
- 'isChanged:',
314
- !deepEqual(state, check)
315
- );
316
- if (!deepEqual(state, check)) {
317
- debouncedNotify();
318
- }
319
- }, 0);
320
- }
321
- return o;
322
- },
323
- set(val: any) {
324
- if (typeof val === typeof o.valueOf()) {
325
- let iso =
326
- isObservableObject(o.valueOf()) &&
327
- o.valueOf() instanceof Array === false;
328
- if (iso) {
329
- // is observable object
330
- if (o?.constructor?.prototype?.observe === undefined) {
331
- // not observed yet
332
- o = new (getConstructor(val) as Class<typeof val>)(
333
- structuredClone(Object.assign({}, val))
334
- );
335
- copyGettersSetters(val, o);
336
- //o.seal();
337
- }
338
- Object.assign(o, val); // mixin set object into observable object
339
- o.notify();
340
- } else {
341
- // simple native var
342
- let old = o.valueOf(),
343
- callbacks = (o as Observable).callbacks;
344
-
345
- if (val !== old) {
346
- o = new (getConstructor(val) as Class<typeof val>)(val);
347
- (o as Observable).callbacks = callbacks;
348
- debouncedNotify();
349
- }
350
- }
351
- } else {
352
- // set type is not stored type
353
- console.warn(
354
- 'IGNORED - cannot change observable types, IS:',
355
- typeof o.valueOf(),
356
- o,
357
- '- CANNOT BECOME:',
358
- typeof val
359
- );
360
- }
361
- },
362
- });
363
- target[key] = val; // replace old property with proxy (observable)
364
- }
365
- });
366
-
367
- return target;
264
+ let keys = !propertyName ? [...Object.getOwnPropertyNames(target)] : [propertyName];
265
+
266
+ keys.forEach((key) => {
267
+ if (
268
+ target.hasOwnProperty(key) &&
269
+ typeof target[key] !== 'function' &&
270
+ !target?.[key]?.constructor?.prototype?.observe
271
+ ) {
272
+ if (key[0] === '_' || target?.[key] === undefined) return target;
273
+ let val: any = target[key];
274
+
275
+ let o = new (getConstructor(val) as Class<typeof val>)(val);
276
+
277
+ let debouncedNotify: Function = debounce(function debouncedNotify() {
278
+ o.notify();
279
+ }, 0);
280
+
281
+ Object.defineProperty(target, key, {
282
+ enumerable: true,
283
+ get() {
284
+ if (isObservableObject(o.valueOf())) {
285
+ let check = structuredClone(Object.assign({}, o));
286
+ setTimeout(function checkChanges() {
287
+ let state = structuredClone(Object.assign({}, o));
288
+ if (!deepEqual(state, check)) {
289
+ debouncedNotify();
290
+ }
291
+ }, 0);
292
+ }
293
+ return o;
294
+ },
295
+ set(val: any) {
296
+ if (typeof val === typeof o.valueOf()) {
297
+ let iso = isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false;
298
+ if (iso) {
299
+ // is observable object
300
+ if (o?.constructor?.prototype?.observe === undefined) {
301
+ // not observed yet
302
+ o = new (getConstructor(val) as Class<typeof val>)(
303
+ structuredClone(Object.assign({}, val))
304
+ );
305
+ copyGettersSetters(val, o);
306
+ //o.seal();
307
+ }
308
+ Object.assign(o, val); // mixin set object into observable object
309
+ o.notify(); // dont test, just assume "changed" status
310
+ } else {
311
+ // simple native var
312
+ let old = o.valueOf(),
313
+ callbacks = (o as Observable).callbacks;
314
+
315
+ if (val !== old) {
316
+ o = new (getConstructor(val) as Class<typeof val>)(val);
317
+ (o as Observable).callbacks = callbacks;
318
+ debouncedNotify();
319
+ }
320
+ }
321
+ } else {
322
+ // set type is not stored type
323
+ console.warn(
324
+ 'IGNORED - cannot change observable types, IS:',
325
+ typeof o.valueOf(),
326
+ o,
327
+ '- CANNOT BECOME:',
328
+ typeof val
329
+ );
330
+ }
331
+ },
332
+ });
333
+ target[key] = val; // replace old property with proxy (observable)
334
+ }
335
+ });
336
+
337
+ return target;
368
338
  };