solarite 0.2.4 → 0.3.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 (38) hide show
  1. package/dist/Solarite-debug.js +1457 -1402
  2. package/dist/Solarite.js +1425 -1272
  3. package/dist/Solarite.min.js +2 -2
  4. package/package.json +5 -6
  5. package/readme.md +2 -4
  6. package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
  7. package/src/Globals.js +79 -0
  8. package/src/HtmlParser.js +91 -0
  9. package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
  10. package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
  11. package/src/{solarite/Shell.js → Shell.js} +119 -92
  12. package/src/Solarite.d.ts +62 -0
  13. package/src/{solarite/Solarite.js → Solarite.js} +15 -13
  14. package/src/{solarite/Template.js → Template.js} +22 -19
  15. package/src/Util.js +330 -0
  16. package/src/{util/Errors.js → assert.js} +1 -0
  17. package/src/createSolarite.js +154 -0
  18. package/src/{util/delve.js → delve.js} +5 -4
  19. package/src/{solarite/getArg.js → getArg.js} +41 -15
  20. package/src/{solarite/r.js → h.js} +59 -29
  21. package/src/{solarite/hash.js → hash.js} +12 -9
  22. package/src/unused/FastLookupArray.js +54 -0
  23. package/src/unused/Hashes.js +339 -0
  24. package/src/unused/InUse.test.js +92 -0
  25. package/src/unused/InUseMap.js +98 -0
  26. package/src/unused/LinkedList.js +117 -0
  27. package/src/unused/LinkedList.test.js +115 -0
  28. package/src/unused/Misc.js +13 -0
  29. package/src/unused/Perf.js +47 -0
  30. package/src/unused/TrackedArray.js +54 -0
  31. package/src/watch.js +546 -0
  32. package/src/solarite/Globals.js +0 -54
  33. package/src/solarite/Util.js +0 -388
  34. package/src/solarite/createSolarite.js +0 -274
  35. package/src/solarite/watch3.js +0 -189
  36. package/src/util/Util.js +0 -113
  37. /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
  38. /package/src/{util → unused}/WeakArray.js +0 -0
package/src/Util.js ADDED
@@ -0,0 +1,330 @@
1
+ import Globals from "./Globals.js";
2
+ import delve from "./delve.js";
3
+
4
+ let Util = {
5
+
6
+ /**
7
+ * Returns true if they're the same.
8
+ * @param a
9
+ * @param b
10
+ * @returns {boolean} */
11
+ arraySame(a, b) {
12
+ let aLength = a.length;
13
+ if (aLength !== b.length)
14
+ return false;
15
+ for (let i=0; i<aLength; i++)
16
+ if (a[i] !== b[i])
17
+ return false;
18
+ return true; // the same.
19
+ },
20
+
21
+ bindId(root, el) {
22
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
23
+ if (id) { // If something hasn't removed the id.
24
+
25
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
26
+ if (root[id] && !(root[id] instanceof Node))
27
+ throw new Error(`${root.constructor.name}.${id} already has a value. ` +
28
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
29
+
30
+ delve(root, id.split(/\./g), el);
31
+ }
32
+ },
33
+
34
+ /**
35
+ * @param style {HTMLStyleElement}
36
+ * @param root {HTMLElement} */
37
+ bindStyles(style, root) {
38
+ let styleId = root.getAttribute('data-style');
39
+ if (!styleId) {
40
+ // Keep track of one style id for each class.
41
+ // TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
42
+ if (!root.constructor.styleId)
43
+ root.constructor.styleId = 1;
44
+ styleId = root.constructor.styleId++;
45
+
46
+ root.setAttribute('data-style', styleId)
47
+ }
48
+
49
+ // Replace ":host" with "tagName[data-style=...]" in the css.
50
+ let tagName = root.tagName.toLowerCase();
51
+ for (let child of style.childNodes) {
52
+ if (child.nodeType === 3) {
53
+ let oldText = child.textContent;
54
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`)
55
+ if (oldText !== newText)
56
+ child.textContent = newText;
57
+ }
58
+ }
59
+ },
60
+
61
+ /**
62
+ * Convert a Proper Case name to a name with dashes.
63
+ * Dashes will be placed between letters and numbers.
64
+ * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
65
+ * @param str {string}
66
+ * @return {string}
67
+ *
68
+ * @example
69
+ * 'ProperName' => 'proper-name'
70
+ * 'HTMLElement' => 'html-element'
71
+ * 'BigUI' => 'big-ui'
72
+ * 'UIForm' => 'ui-form'
73
+ * 'A100' => 'a-100' */
74
+ camelToDashes(str) {
75
+ // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
76
+ str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
77
+
78
+ // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
79
+ str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
80
+
81
+ // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
82
+ str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
83
+
84
+ // Convert all the remaining capital letters to lowercase.
85
+ return str.toLowerCase();
86
+ },
87
+
88
+ /**
89
+ * Converts a string written in kebab-case to camelCase.
90
+ *
91
+ * @param {string} str - The input string written in kebab-case.
92
+ * @return {string} - The resulting camelCase string.
93
+ *
94
+ * @example
95
+ * dashesToCamel('example-string') // Returns 'exampleString'
96
+ * dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
97
+ dashesToCamel(str) {
98
+ return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
99
+ },
100
+
101
+
102
+ /**
103
+ * A generator function that recursively traverses and flattens a value.
104
+ *
105
+ * - If the input is an array, it recursively traverses and flattens the array.
106
+ * - If the input is a function, it calls the function, replaces the function
107
+ * with its result, and flattens the result if necessary. It will recursively
108
+ * call functions that return other functions.
109
+ * - Otherwise it yields the value as is.
110
+ *
111
+ * This function does not create a new array for the flattened values. Instead,
112
+ * it lazily yields each item as it is encountered. This can be more memory-efficient
113
+ * for large or deeply nested structures.
114
+ *
115
+ * @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
116
+ * @yields {any} - The next item in the flattened structure.
117
+ *
118
+ * @example
119
+ * const complexArray = [
120
+ * 1,
121
+ * [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
122
+ * () => () => 7,
123
+ * () => [() => 8, 9],
124
+ * ]; *
125
+ * for (const item of flatten(complexArray))
126
+ * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
127
+ */
128
+ // *flatten(value) {
129
+ // if (Array.isArray(value)) {
130
+ // for (const item of value) {
131
+ // yield* Util.flatten(item); // Recursively flatten arrays
132
+ // }
133
+ // } else if (typeof value === 'function') {
134
+ // const result = value();
135
+ // yield* Util.flatten(result); // Recursively flatten the result of a function
136
+ // } else
137
+ // yield value; // Yield primitive values as is
138
+ // },
139
+
140
+ /**
141
+ * Get the value of an input as the most appropriate JavaScript type.
142
+ * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
143
+ * @return {string|string[]|number|[]|File[]|Date|boolean} */
144
+ getInputValue(node) {
145
+ // .type is a built-in DOM property
146
+ if (node.type === 'checkbox' || node.type === 'radio')
147
+ return node.checked; // Boolean
148
+ if (node.type === 'file')
149
+ return [...node.files]; // FileList
150
+ if (node.type === 'number' || node.type === 'range')
151
+ return node.valueAsNumber; // Number
152
+ if (node.type === 'date' || node.type === 'time' || node.type === 'datetime-local')
153
+ return node.valueAsDate; // Date Object
154
+ if (node.type === 'select-multiple') // <select multiple>
155
+ return [...node.selectedOptions].map(option => option.value); // Array of Strings
156
+
157
+ return node.value; // String
158
+ },
159
+
160
+ isEvent(attrName) {
161
+ return attrName.startsWith('on') && attrName in Globals.div;
162
+ },
163
+
164
+ /**
165
+ * @param el {HTMLElement}
166
+ * @param prop {string}
167
+ * @returns {boolean} */
168
+ isHtmlProp(el, prop) {
169
+ let key = el.tagName + '.' + prop;
170
+ let result = Globals.htmlProps[key];
171
+ if (result === undefined) { // Caching just barely makes this slightly faster.
172
+ let proto = Object.getPrototypeOf(el);
173
+
174
+ // Find the first HTMLElement that we inherit from (not our own classes)
175
+ while (proto) {
176
+ const ctorName = proto.constructor.name;
177
+ if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
178
+ break
179
+ proto = Object.getPrototypeOf(proto);
180
+ }
181
+ Globals.htmlProps[key] = result = (proto
182
+ ? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
183
+ : false);
184
+ }
185
+ return result;
186
+ },
187
+
188
+ /**
189
+ * Is it an array and a path that can be evaluated by delve() ?
190
+ * We allow the first element to be null/undefined so binding can report errors.
191
+ * @param arr {Array|*}
192
+ * @returns {boolean} */
193
+ isPath(arr) {
194
+ return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
195
+ && (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
196
+ && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
197
+ },
198
+
199
+ isFalsy(val) {
200
+ return val === undefined || val === false || val === null;
201
+ },
202
+
203
+ /*
204
+ isPrimitive(val) {
205
+ return typeof val === 'string' || typeof val === 'number'
206
+ },*/
207
+
208
+ /**
209
+ * If val is a function, evaluate it recursively until the result is not a function.
210
+ * If it's an array or an object, convert it to Json.
211
+ * If it's a Date, format it as Y-m-d H:i:s
212
+ * @param val
213
+ * @returns {string|number|boolean} */
214
+ makePrimitive(val) {
215
+ if (typeof val === 'function')
216
+ return Util.makePrimitive(val());
217
+ else if (val instanceof Date)
218
+ return val.toISOString().replace(/T/, ' ');
219
+ else if (Array.isArray(val) || typeof val === 'object')
220
+ return ''; // JSON.stringify(val);
221
+ return val;
222
+ },
223
+
224
+ /**
225
+ * Use an array as the value of a map, appending to it when we add.
226
+ * Used by watch.js.
227
+ * @param map {Map|WeakMap|Object}
228
+ * @param key
229
+ * @param value */
230
+ mapArrayAdd(map, key, value) {
231
+ let result = map.get(key);
232
+ if (!result) {
233
+ result = [value];
234
+ map.set(key, result);
235
+ }
236
+ else
237
+ result.push(value);
238
+ },
239
+
240
+ /**
241
+ * Remove nodes from the beginning and end that are not:
242
+ * 1. Elements.
243
+ * 2. Non-whitespace text nodes.
244
+ * @param nodes {Node[]|NodeList}
245
+ * @returns {Node[]} */
246
+ trimEmptyNodes(nodes) {
247
+ const shouldTrimNode = node =>
248
+ node.nodeType !== Node.ELEMENT_NODE &&
249
+ (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === '');
250
+
251
+ // Convert nodeList to an array for easier manipulation
252
+ const result = [...nodes]
253
+
254
+ // Trim from the start
255
+ while (result.length > 0 && shouldTrimNode(result[0]))
256
+ result.shift();
257
+
258
+ // Trim from the end
259
+ while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
260
+ result.pop();
261
+
262
+ return result;
263
+ }
264
+ };
265
+
266
+ export default Util;
267
+
268
+
269
+
270
+ // For debugging only
271
+ //#IFDEV
272
+ export function setIndent(items, level=1) {
273
+ if (typeof items === 'string')
274
+ items = items.split(/\r?\n/g)
275
+
276
+ return items.map(str => {
277
+ if (level > 0)
278
+ return ' '.repeat(level) + str;
279
+ else if (level < 0)
280
+ return str.replace(new RegExp(`^ {0,${Math.abs(level)}}`), '');
281
+ return str;
282
+ })
283
+ }
284
+
285
+ export function nodeToArrayTree(node, callback=null) {
286
+ if (!node) return [];
287
+
288
+ let result = [];
289
+
290
+ if (callback)
291
+ result.push(...callback(node))
292
+
293
+ if (node.nodeType === 1) {
294
+ let attrs = Array.from(node.attributes).map(attr => `${attr.name}="${attr.value}"`).join(' ');
295
+ let openingTag = `<${node.nodeName.toLowerCase()}${attrs ? ' ' + attrs : ''}>`;
296
+
297
+ let childrenArray = [];
298
+ for (let child of node.childNodes) {
299
+ let childResult = nodeToArrayTree(child, callback);
300
+ if (childResult.length > 0) {
301
+ childrenArray.push(childResult);
302
+ }
303
+ }
304
+
305
+ //let closingTag = `</${node.nodeName.toLowerCase()}>`;
306
+
307
+ result.push(openingTag, ...childrenArray);
308
+ } else if (node.nodeType === 3) {
309
+ result.push("'"+node.nodeValue+"'");
310
+ }
311
+
312
+ return result;
313
+ }
314
+
315
+
316
+ export function flattenAndIndent(inputArray, indent = "") {
317
+ let result = [];
318
+
319
+ for (let item of inputArray) {
320
+ if (Array.isArray(item)) {
321
+ // Recursively handle nested arrays with increased indentation
322
+ result = result.concat(flattenAndIndent(item, indent + " "));
323
+ } else {
324
+ result.push(indent + item);
325
+ }
326
+ }
327
+
328
+ return result;
329
+ }
330
+ //#ENDIF
@@ -6,4 +6,5 @@ export function assert(val) {
6
6
  throw new Error('Assertion failed: ' + val);
7
7
  }
8
8
  }
9
+
9
10
  //#ENDIF
@@ -0,0 +1,154 @@
1
+ import Util from './Util.js';
2
+ import Globals from "./Globals.js";
3
+
4
+ function defineClass(Class, tagName, extendsTag) {
5
+ if (!customElements[getName](Class)) { // If not previously defined.
6
+ tagName = tagName || Util.camelToDashes(Class.name)
7
+ if (!tagName.includes('-'))
8
+ tagName += '-element';
9
+
10
+ let options = null;
11
+ if (extendsTag)
12
+ options = {extends: extendsTag}
13
+
14
+ customElements[define](tagName, Class, options)
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Create a version of the Solarite class that extends from the given tag name.
20
+ * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
21
+ * 1. customElements.define() is called automatically when you create the first instance.
22
+ * 2. Calls render() when added to the DOM, if it hasn't been called already.
23
+ * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
24
+ * 4. We can use this.html = r`...` to set html. (deprecated)
25
+ * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
26
+ * Can't figure out how to have these work standalone though, and still be synchronous.
27
+ * 6. Can we extend from other element types like TR?
28
+ * 7. Shows default text if render() function isn't defined.
29
+ *
30
+ * Advantages to inheriting from HTMLElement
31
+ * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
32
+ * 2. We can inherit from things like HTMLTableRowElement directly.
33
+ * 3. There's less magic, since everyone is familiar with defining custom elements.
34
+ *
35
+ * @param extendsTag {?string}
36
+ * @return {Class} */
37
+ export default function createSolarite(extendsTag=null) {
38
+
39
+ let BaseClass = HTMLElement;
40
+ if (extendsTag && !extendsTag.includes('-')) {
41
+ extendsTag = extendsTag.toLowerCase();
42
+
43
+ BaseClass = Globals.elementClasses[extendsTag];
44
+ if (!BaseClass) { // TODO: Use Cache
45
+ BaseClass = document.createElement(extendsTag).constructor;
46
+ Globals.elementClasses[extendsTag] = BaseClass
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Intercept the construct call to auto-define the class before the constructor is called.
52
+ * @type {HTMLElement} */
53
+ let HTMLElementAutoDefine = new Proxy(BaseClass, {
54
+ construct(Parent, args, Class) {
55
+ defineClass(Class, null, extendsTag)
56
+
57
+ // This is a good place to manipulate any args before they're sent to the constructor.
58
+ // Such as loading them from attributes, if I could find a way to do so.
59
+
60
+ // This line is equivalent the to super() call.
61
+ return Reflect.construct(Parent, args, Class);
62
+ }
63
+ });
64
+
65
+ return class Solarite extends HTMLElementAutoDefine {
66
+
67
+
68
+ /**
69
+ * TODO: Make these standalone functions.
70
+ * Callbacks.
71
+ * Use onConnect.push(() => ...); to add new callbacks. */
72
+ onConnect;
73
+
74
+ onFirstConnect;
75
+ onDisconnect;
76
+
77
+ /**
78
+ * @param options {RenderOptions} */
79
+ constructor(options={}) {
80
+ super();
81
+
82
+ // TODO: Is options.render ever used?
83
+ if (options.render===true)
84
+ this.render();
85
+
86
+ else if (options.render===false)
87
+ Globals.rendered.add(this); // Don't render on connectedCallback()
88
+
89
+ // Add slot children before constructor code executes.
90
+ // This breaks the styleStaticNested test.
91
+ // PendingChildren is setup in NodeGroup.instantiateComponent()
92
+ // TODO: Match named slots.
93
+ //let ch = Globals.pendingChildren.pop();
94
+ //if (ch) // TODO: how could there be a slot before render is called?
95
+ // (this.querySelector('slot') || this).append(...ch);
96
+
97
+ /** @deprecated
98
+ Object.defineProperty(this, 'html', {
99
+ set(html) {
100
+ Globals.rendered.add(this);
101
+ if (typeof html === 'string') {
102
+ console.warn("Assigning to this.html without the r template prefix.")
103
+ this.innerHTML = html;
104
+ }
105
+ else
106
+ this.modifications = r(this, html, options);
107
+ }
108
+ })*/
109
+
110
+ /*
111
+ let pthis = new Proxy(this, {
112
+ get(obj, prop) {
113
+ return Reflect.get(obj, prop)
114
+ }
115
+ });
116
+ this.render = this.render.bind(pthis);
117
+ */
118
+ }
119
+
120
+ /**
121
+ * Call render() only if it hasn't already been called. */
122
+ renderFirstTime() {
123
+ if (!Globals.rendered.has(this) && this.render)
124
+ this.render();
125
+ }
126
+
127
+ /**
128
+ * Called automatically by the browser. */
129
+ connectedCallback() {
130
+ this.renderFirstTime();
131
+ if (!Globals.connected.has(this)) {
132
+ Globals.connected.add(this);
133
+ if (this.onFirstConnect)
134
+ this.onFirstConnect();
135
+ }
136
+ if (this.onConnect)
137
+ this.onConnect();
138
+ }
139
+
140
+ disconnectedCallback() {
141
+ if (this.onDisconnect)
142
+ this.onDisconnect();
143
+ }
144
+
145
+
146
+ static define(tagName=null) {
147
+ defineClass(this, tagName, extendsTag)
148
+ }
149
+ }
150
+ }
151
+
152
+ // Trick to prevent minifier from renaming this method.
153
+ let define = 'define';
154
+ let getName = 'getName';
@@ -2,10 +2,10 @@
2
2
  * Follow a path into an object.
3
3
  * @param obj {object}
4
4
  * @param path {string[]}
5
- * @param createVal {*} If set, non-existant paths will be created and value at path will be set to createVal.
5
+ * @param createVal {*} If set, non-existent paths will be created and value at path will be set to createVal.
6
6
  * @return {*} The value, or undefined if it can't be reached. */
7
- export default function delve(obj, path, createVal = delveDontCreate) {
8
- let isCreate = createVal !== delveDontCreate;
7
+ export default function delve(obj, path, createVal = d) {
8
+ let isCreate = createVal !== d;
9
9
 
10
10
  let len = path.length;
11
11
  if (!obj && !isCreate && len)
@@ -40,4 +40,5 @@ export default function delve(obj, path, createVal = delveDontCreate) {
40
40
  return obj;
41
41
  }
42
42
 
43
- let delveDontCreate = {};
43
+ // d means "don't create"
44
+ let d = {};
@@ -1,4 +1,4 @@
1
-
1
+ import Util from "./Util.js";
2
2
 
3
3
 
4
4
  /**
@@ -24,23 +24,28 @@
24
24
  * }
25
25
  *
26
26
  * @param el {HTMLElement}
27
- * @param name {string} Attribute name. Not case-sensitive.
28
- * @param val {*} Default value to use if attribute doesn't exist.
29
- * @param type {ArgType|function|*[]}
27
+ * @param attributeName {string} Attribute name. Not case-sensitive.
28
+ * @param defaultValue {*} Default value to use if attribute doesn't exist.
29
+ * @param type {ArgType|function|Class|*[]}
30
30
  * If an array, use the value if it's in the array, otherwise return undefined.
31
31
  * If it's a function, pass the value to the function and return the result.
32
- * @param fallback {*} If the type can't be parsed as the given type, use this value.
32
+ * @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
33
+ * TODO: Should this be merged with the defaultValue argument?
33
34
  * @return {*} Undefined if attribute isn't set. */
34
- export function getArg(el, name, val=undefined, type=ArgType.String, fallback=undefined) {
35
- let attrVal = el.getAttribute(name);
35
+ export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
36
+ let val = defaultValue;
37
+ let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
36
38
  if (attrVal !== null) // If attribute doesn't exist.
37
39
  val = attrVal;
38
40
 
39
41
  if (Array.isArray(type))
40
42
  return type.includes(val) ? val : fallback;
41
43
 
42
- if (typeof type === 'function')
43
- return type(val);
44
+ if (typeof type === 'function') {
45
+ return type.constructor
46
+ ? new type(val) // arg type is custom Class
47
+ : type(val); // arg type is custom function
48
+ }
44
49
 
45
50
  // If bool, it's true as long as it exists and its value isn't falsey.
46
51
  if (type===ArgType.Bool) {
@@ -63,18 +68,18 @@ export function getArg(el, name, val=undefined, type=ArgType.String, fallback=un
63
68
  return isNaN(result) ? fallback : result;
64
69
  case ArgType.String:
65
70
  return [undefined, null, false].includes(val) ? '' : val+'';
66
- case ArgType.JSON:
71
+ case ArgType.Json:
67
72
  case ArgType.Eval:
68
73
  if (typeof val === 'string' && val.length)
69
74
  try {
70
- if (type === ArgType.JSON)
75
+ if (type === ArgType.Json)
71
76
  return JSON.parse(val);
72
77
  else
73
78
  return eval(`(${val})`);
74
79
  } catch (e) {
75
80
  return val;
76
81
  }
77
- else return fallback;
82
+ else return val;
78
83
 
79
84
  // type not provided
80
85
  default:
@@ -82,6 +87,24 @@ export function getArg(el, name, val=undefined, type=ArgType.String, fallback=un
82
87
  }
83
88
  }
84
89
 
90
+
91
+ /**
92
+ * Experimental. Set multiple arguments/attributes all at once.
93
+ * @param el {HTMLElement}
94
+ * @param args {Record<string, any>}
95
+ * @param types {Record<string, ArgType|function|Class>}
96
+ *
97
+ * @example
98
+ * constructor({user, path}={}) {
99
+ * setArgs(this, arguments[0], {user: User, path: ArgType.String});
100
+ * }
101
+ */
102
+ export function setArgs(el, args, types) {
103
+ for (let name in args)
104
+ this[name] = getArg(el, name, args[name], types[name] || ArgType.String);
105
+ }
106
+
107
+
85
108
  /**
86
109
  * @enum */
87
110
  var ArgType = {
@@ -95,12 +118,15 @@ var ArgType = {
95
118
  Int: 'Int',
96
119
  Float: 'Float',
97
120
  String: 'String',
98
-
121
+
122
+ /** @deprecated for Json */
123
+ JSON: 'Json',
124
+
99
125
  /**
100
126
  * Parse the string value as JSON.
101
127
  * If it's not parsable, return the value as a string. */
102
- JSON: 'JSON',
103
-
128
+ Json: 'Json',
129
+
104
130
  /**
105
131
  * Evaluate the string as JavaScript using the eval() function.
106
132
  * If it can't be evaluated, return the original string. */