solarite 0.2.3 → 0.3.0

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.
@@ -1,5 +1,24 @@
1
+ import Globals from "./Globals.js";
2
+ import delve from "../util/delve.js";
3
+
1
4
  let Util = {
2
5
 
6
+ bindId(root, el) {
7
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
8
+ if (id) { // If something hasn't removed the id.
9
+
10
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
11
+ if (root[id] && !(root[id] instanceof Node))
12
+ throw new Error(`${root.constructor.name}.${id} already has a value. ` +
13
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
14
+
15
+ delve(root, id.split(/\./g), el);
16
+ }
17
+ },
18
+
19
+ /**
20
+ * @param style {HTMLStyleElement}
21
+ * @param root {HTMLElement} */
3
22
  bindStyles(style, root) {
4
23
  let styleId = root.getAttribute('data-style');
5
24
  if (!styleId) {
@@ -12,17 +31,60 @@ let Util = {
12
31
  root.setAttribute('data-style', styleId)
13
32
  }
14
33
 
34
+ // Replace ":host" with "tagName[data-style=...]" in the css.
15
35
  let tagName = root.tagName.toLowerCase();
16
36
  for (let child of style.childNodes) {
17
37
  if (child.nodeType === 3) {
18
38
  let oldText = child.textContent;
19
- let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName + '[data-style="' + styleId + '"]')
39
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`)
20
40
  if (oldText !== newText)
21
41
  child.textContent = newText;
22
42
  }
23
43
  }
24
44
  },
25
45
 
46
+
47
+ /**
48
+ * Convert a Proper Case name to a name with dashes.
49
+ * Dashes will be placed between letters and numbers.
50
+ * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
51
+ * @param str {string}
52
+ * @return {string}
53
+ *
54
+ * @example
55
+ * 'ProperName' => 'proper-name'
56
+ * 'HTMLElement' => 'html-element'
57
+ * 'BigUI' => 'big-ui'
58
+ * 'UIForm' => 'ui-form'
59
+ * 'A100' => 'a-100' */
60
+ camelToDashes(str) {
61
+ // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
62
+ str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
63
+
64
+ // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
65
+ str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
66
+
67
+ // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
68
+ str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
69
+
70
+ // Convert all the remaining capital letters to lowercase.
71
+ return str.toLowerCase();
72
+ },
73
+
74
+ /**
75
+ * Converts a string written in kebab-case to camelCase.
76
+ *
77
+ * @param {string} str - The input string written in kebab-case.
78
+ * @return {string} - The resulting camelCase string.
79
+ *
80
+ * @example
81
+ * dashesToCamel('example-string') // Returns 'exampleString'
82
+ * dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
83
+ dashesToCamel(str) {
84
+ return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
85
+ },
86
+
87
+
26
88
  /**
27
89
  * A generator function that recursively traverses and flattens a value.
28
90
  *
@@ -66,6 +128,7 @@ let Util = {
66
128
  * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
67
129
  * @return {string|string[]|number|[]|File[]|Date|boolean} */
68
130
  getInputValue(node) {
131
+ // .type is a built-in DOM property
69
132
  if (node.type === 'checkbox' || node.type === 'radio')
70
133
  return node.checked; // Boolean
71
134
  if (node.type === 'file')
@@ -80,48 +143,63 @@ let Util = {
80
143
  return node.value; // String
81
144
  },
82
145
 
146
+ /**
147
+ * @param el {HTMLElement}
148
+ * @param prop {string}
149
+ * @returns {boolean} */
150
+ isHtmlProp(el, prop) {
151
+ let key = el.tagName + '.' + prop;
152
+ let result = Globals.htmlProps[key];
153
+ if (result === undefined) { // Caching just barely makes this slightly faster.
154
+ let proto = Object.getPrototypeOf(el);
155
+
156
+ // Find the first HTMLElement that we inherit from (not our own classes)
157
+ while (proto) {
158
+ const ctorName = proto.constructor.name;
159
+ if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
160
+ break
161
+ proto = Object.getPrototypeOf(proto);
162
+ }
163
+ Globals.htmlProps[key] = result = (proto
164
+ ? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
165
+ : false);
166
+ }
167
+ return result;
168
+ },
169
+
83
170
  /**
84
171
  * Is it an array and a path that can be evaluated by delve() ?
172
+ * We allow the first element to be null/undefined so binding can report errors.
85
173
  * @param arr {Array|*}
86
174
  * @returns {boolean} */
87
175
  isPath(arr) {
88
- return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
176
+ return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
177
+ && (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
178
+ && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
89
179
  },
90
180
 
91
- /**
92
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
93
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
94
- * This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
95
- * This is necessary because a NodeGroup normally only stores the first and last node.
96
- * Called from ExprPath.apply().
97
- * @param oldNodeGroups {NodeGroup[]}
98
- * @param oldNodes {Node[]} */
99
- saveOrphans(oldNodeGroups, oldNodes) {
100
- let oldNgMap = new Map();
101
- for (let ng of oldNodeGroups) {
102
- oldNgMap.set(ng.startNode, ng)
103
-
104
- // TODO: Is this necessary?
105
- // if (ng.parentPath)
106
- // ng.parentPath.clearNodesCache();
107
- }
181
+ isFalsy(val) {
182
+ return val === undefined || val === false || val === null;
183
+ },
108
184
 
109
- for (let i=0, node; node = oldNodes[i]; i++) {
110
- let ng;
111
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
112
- //ng.nodesCache = [];
113
- let fragment = document.createDocumentFragment();
114
- let endNode = ng.endNode;
115
- while (node !== endNode) {
116
- fragment.append(node);
117
- //ng.nodesCache.push(node);
118
- i++;
119
- node = oldNodes[i];
120
- }
121
- fragment.append(endNode);
122
- //ng.nodesCache.push(endNode);
123
- }
124
- }
185
+ isPrimitive(val) {
186
+ return typeof val === 'string' || typeof val === 'number'
187
+ },
188
+
189
+ /**
190
+ * If val is a function, evaluate it recursively until the result is not a function.
191
+ * If it's an array or an object, convert it to Json.
192
+ * If it's a Date, format it as Y-m-d H:i:s
193
+ * @param val
194
+ * @returns {string|number|boolean} */
195
+ makePrimitive(val) {
196
+ if (typeof val === 'function')
197
+ return Util.makePrimitive(val());
198
+ else if (val instanceof Date)
199
+ return val.toISOString().replace(/T/, ' ');
200
+ else if (Array.isArray(val) || typeof val === 'object')
201
+ return ''; // JSON.stringify(val);
202
+ return val;
125
203
  },
126
204
 
127
205
  /**
@@ -154,38 +232,12 @@ export default Util;
154
232
 
155
233
 
156
234
 
157
- let div = document.createElement('div');
158
- export {div}
159
-
160
- let isEvent = attrName => attrName.startsWith('on') && attrName in div;
235
+ let isEvent = attrName => attrName.startsWith('on') && attrName in Globals.div;
161
236
  export {isEvent};
162
237
 
163
238
 
164
- /**
165
- * Convert a Proper Case name to a name with dashes.
166
- * Dashes will be placed between letters and numbers.
167
- * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
168
- * @param str {string}
169
- * @return {string}
170
- *
171
- * @example
172
- * 'ProperName' => 'proper-name'
173
- * 'HTMLElement' => 'html-element'
174
- * 'BigUI' => 'big-ui'
175
- * 'UIForm' => 'ui-form'
176
- * 'A100' => 'a-100' */
177
- export function camelToDashes(str) {
178
- // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
179
- str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
180
-
181
- // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
182
- str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
183
-
184
- // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
185
- str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
186
-
187
- // Convert all the remaining capital letters to lowercase.
188
- return str.toLowerCase();
239
+ export function dashesToCamel(str) {
240
+ return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
189
241
  }
190
242
 
191
243
 
@@ -193,7 +245,7 @@ export function camelToDashes(str) {
193
245
 
194
246
 
195
247
  /**
196
- * Returns false if they're the same. Or the first index where they differ.
248
+ * Returns true if they're the same.
197
249
  * @param a
198
250
  * @param b
199
251
  * @returns {boolean} */
@@ -208,121 +260,7 @@ export function arraySame(a, b) {
208
260
  }
209
261
 
210
262
 
211
- /**
212
- * TODO: Turn this into a class because it has internal state.
213
- * TODO: Don't break on 3<a inside a <script> or <style> tag.
214
- * @param html {?string} Pass null to reset context.
215
- * @returns {string} */
216
- export function htmlContext(html) {
217
- if (html === null) {
218
- state = {...defaultState};
219
- return state.context;
220
- }
221
- for (let i = 0; i < html.length; i++) {
222
- const char = html[i];
223
- switch (state.context) {
224
- case htmlContext.Text:
225
- if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
226
- // if (html.slice(i, i+4) === '<!--')
227
- // state.context = htmlContext.Comment;
228
- // else
229
- state.context = htmlContext.Tag;
230
- state.buffer = '';
231
- }
232
- break;
233
- case htmlContext.Tag:
234
- if (char === '>') {
235
- state.context = htmlContext.Text;
236
- state.quote = null;
237
- state.buffer = '';
238
- } else if (char === ' ' && !state.buffer) {
239
- // No attribute name is present. Skipping the space.
240
- continue;
241
- } else if (char === ' ' || char === '/' || char === '?') {
242
- state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
243
- } else if (char === '"' || char === "'" || char === '=') {
244
- state.context = htmlContext.Attribute;
245
- state.quote = char === '=' ? null : char;
246
- state.buffer = '';
247
- } else {
248
- state.buffer += char;
249
- }
250
- break;
251
- case htmlContext.Attribute:
252
- if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
253
- state.quote = char;
254
-
255
- else if (char === state.quote || (!state.quote && state.buffer.length)) {
256
- state.context = htmlContext.Tag;
257
- state.quote = null;
258
- state.buffer = '';
259
- } else if (!state.quote && char === '>') {
260
- state.context = htmlContext.Text;
261
- state.quote = null;
262
- state.buffer = '';
263
- } else if (char !== ' ') {
264
- state.buffer += char;
265
- }
266
- break;
267
- }
268
-
269
- }
270
- return state.context;
271
- }
272
-
273
-
274
- htmlContext.Attribute = 'Attribute';
275
- htmlContext.Text = 'Text';
276
- htmlContext.Tag = 'Tag';
277
- //htmlContext.Comment = 'Comment';
278
- let defaultState = {
279
- context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
280
- quote: null, // possible values: null, '"', "'"
281
- buffer: '',
282
- lastChar: null
283
- };
284
- let state = {...defaultState};
285
-
286
-
287
-
288
-
289
-
290
263
 
291
- let cacheItems = {};
292
-
293
- /**
294
- * @param item {string}
295
- * @param initial {*}
296
- * @returns {*} */
297
- export function cache(item, initial) {
298
- let result = cacheItems[item];
299
- if (!result) {
300
- cacheItems[item] = initial
301
- result = initial;
302
- }
303
- return result;
304
- }
305
-
306
-
307
-
308
- export class WeakCache {
309
-
310
- items = new WeakMap();
311
-
312
- constructor(initial) {
313
- this.initial = initial;
314
- }
315
-
316
- get(item) {
317
- let result = this.items.get(item);
318
- if (!result) {
319
- let value = typeof this.initial === 'function' ? this.initial() : this.initial;
320
- this.items.set(item, value)
321
- result = this.initial;
322
- }
323
- return result;
324
- }
325
- }
326
264
 
327
265
 
328
266
  // For debugging only
@@ -1,21 +1,10 @@
1
1
  import Util from "../util/Util.js";
2
- //import {assert} from "../util/Errors.js";
3
- import delve from "../util/delve.js";
4
- import {getArg, ArgType} from "./getArg.js";
5
- import {getObjectHash} from "./hash.js";
6
- //import NodeGroupManager from "./NodeGroupManager.js";
7
- import r from "./r.js";
8
- import {camelToDashes} from "./Util.js";
2
+ import SolariteUtil from './Util.js';
9
3
  import Globals from "./Globals.js";
10
4
 
11
-
12
- //import {watchGet, watchSet} from "./watch.js";
13
-
14
-
15
-
16
5
  function defineClass(Class, tagName, extendsTag) {
17
- if (!customElements.getName(Class)) { // If not previously defined.
18
- tagName = tagName || camelToDashes(Class.name)
6
+ if (!customElements[getName](Class)) { // If not previously defined.
7
+ tagName = tagName || SolariteUtil.camelToDashes(Class.name)
19
8
  if (!tagName.includes('-'))
20
9
  tagName += '-element';
21
10
 
@@ -23,21 +12,17 @@ function defineClass(Class, tagName, extendsTag) {
23
12
  if (extendsTag)
24
13
  options = {extends: extendsTag}
25
14
 
26
- customElements.define(tagName, Class, options)
15
+ customElements[define](tagName, Class, options)
27
16
  }
28
17
  }
29
18
 
30
-
31
-
32
-
33
-
34
19
  /**
35
20
  * Create a version of the Solarite class that extends from the given tag name.
36
21
  * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
37
22
  * 1. customElements.define() is called automatically when you create the first instance.
38
23
  * 2. Calls render() when added to the DOM, if it hasn't been called already.
39
- * 3. Child elements are added before constructor is called. But they're also passed to the constructor.
40
- * 4. We can use this.html = r`...` to set html.
24
+ * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
25
+ * 4. We can use this.html = r`...` to set html. (deprecated)
41
26
  * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
42
27
  * Can't figure out how to have these work standalone though, and still be synchronous.
43
28
  * 6. Can we extend from other element types like TR?
@@ -102,14 +87,15 @@ export default function createSolarite(extendsTag=null) {
102
87
  else if (options.render===false)
103
88
  Globals.rendered.add(this); // Don't render on connectedCallback()
104
89
 
105
- // Add children before constructor code executes.
90
+ // Add slot children before constructor code executes.
91
+ // This breaks the styleStaticNested test.
106
92
  // PendingChildren is setup in NodeGroup.createNewComponent()
107
93
  // TODO: Match named slots.
108
- let ch = Globals.pendingChildren.pop();
109
- if (ch)
110
- (this.querySelector('slot') || this).append(...ch);
94
+ //let ch = Globals.pendingChildren.pop();
95
+ //if (ch) // TODO: how could there be a slot before render is called?
96
+ // (this.querySelector('slot') || this).append(...ch);
111
97
 
112
- /** @deprecated */
98
+ /** @deprecated
113
99
  Object.defineProperty(this, 'html', {
114
100
  set(html) {
115
101
  Globals.rendered.add(this);
@@ -120,7 +106,7 @@ export default function createSolarite(extendsTag=null) {
120
106
  else
121
107
  this.modifications = r(this, html, options);
122
108
  }
123
- })
109
+ })*/
124
110
 
125
111
  /*
126
112
  let pthis = new Proxy(this, {
@@ -158,117 +144,9 @@ export default function createSolarite(extendsTag=null) {
158
144
  static define(tagName=null) {
159
145
  defineClass(this, tagName, extendsTag)
160
146
  }
161
-
162
- //#IFDEV
163
-
164
- /** @deprecated */
165
- renderWatched() {
166
- let ngm = NodeGroupManager.get(this);
167
-
168
- let nodeGroupUpdates = [];
169
-
170
- for (let change of ngm.changes) {
171
- if (change.action === 'set') {
172
- for (let transformerInfo of change.transformerInfo) {
173
-
174
- let oldHash = transformerInfo.hash;
175
-
176
- let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
177
- let newTemplate = transformerInfo.transformer(newObj);
178
- let newHash = getObjectHash(newTemplate);
179
- let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
180
- for (let ng of ngs) {
181
- nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
182
- }
183
- }
184
- }
185
-
186
- else if (change.action === 'delete') {
187
- for (let hash of change.value) {
188
- let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
189
-
190
- for (let ng of ngs) {
191
- if (ng.parentPath)
192
- ng.parentPath.clearNodesCache();
193
-
194
- for (let node of ng.getNodes())
195
- node.remove();
196
-
197
- // TODO: Update ancestor NodeGroup exactKeys
198
- }
199
- }
200
- }
201
- else if (change.action === 'insert') {
202
-
203
- let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
204
- let arrayPath = [change.root, ...change.path];
205
-
206
- // Get anchor so we can use it to get the parent
207
- // TODO: Should this be watchGet(change.root) ?
208
- for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
209
-
210
- // Change.extra is aTemplate telling us where to insert before.
211
- let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
212
-
213
- // Loop over every item added to the array.
214
- let i = 0; // TODO: How to get real insert index.
215
- for (let obj of change.value) {
216
-
217
- // Same logic as forEach() function.
218
-
219
- let callback = loopInfo.itemTransformer;
220
- let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
221
-
222
- // Shortened logic found in watchGet(), but not any faster?
223
- // the watchSet() is what makes this slower!
224
- // let obj = delve(watchSet(path[0]), path.slice(1));
225
- // let template = callback(obj);
226
- // let serializedPath = serializePath(path);
227
- // pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
228
-
229
- let template = watchGet(path, callback);
230
- i++;
231
-
232
-
233
- //let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
234
-
235
- // Create new NodeGroup
236
- let ng = ngm.getNodeGroup(template, false, true);
237
- ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
238
-
239
- for (let node of ng.getNodes())
240
- beforeNode.parentNode.insertBefore(node, beforeNode);
241
-
242
- if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
243
- ng.parentPath.clearNodesCache();
244
- }
245
-
246
- // TODO: Update ancestor NodeGroup exactKeys
247
- }
248
- }
249
- }
250
-
251
- // Update them all at once, that way we can reassign the same value twice.
252
- for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
253
- ng.applyExprs(exprs);
254
- ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
255
- ng.exactKey = ti.hash = newHash;
256
- ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
257
- }
258
-
259
-
260
- ngm.changes = [];
261
-
262
- return []; // TODO
263
- }
264
-
265
- /**
266
- * @deprecated Use the getArg() function instead. */
267
- getArg(name, val=null, type=ArgType.String) {
268
- throw new Error('deprecated');
269
- return getArg(this, name, val, type);
270
- }
271
- //#ENDIF
272
147
  }
273
148
  }
274
149
 
150
+ // Trick to prevent minifier from renaming this method.
151
+ let define = 'define';
152
+ let getName = 'getName';
@@ -1,10 +1,10 @@
1
-
1
+ import Util from "./Util.js";
2
2
 
3
3
 
4
4
  /**
5
5
  * There are three ways to create an instance of a Solarite Component:
6
6
  * 1. new ComponentName(); // direct class instantiation
7
- * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another Component.
7
+ * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
8
8
  * 3. <body><component-name></component-name></body> // in the Document html.
9
9
  *
10
10
  * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
@@ -24,19 +24,22 @@
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.
27
+ * @param attributeName {string} Attribute name. Not case-sensitive.
28
+ * @param defaultValue {*} Default value to use if attribute doesn't exist.
29
29
  * @param type {ArgType|function|*[]}
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
- * @return {*} */
33
- export function getArg(el, name, val=null, type=ArgType.String) {
34
- let attrVal = el.getAttribute(name);
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?
34
+ * @return {*} Undefined if attribute isn't set. */
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));
35
38
  if (attrVal !== null) // If attribute doesn't exist.
36
39
  val = attrVal;
37
40
 
38
41
  if (Array.isArray(type))
39
- return type.includes(val) ? val : undefined;
42
+ return type.includes(val) ? val : fallback;
40
43
 
41
44
  if (typeof type === 'function')
42
45
  return type(val);
@@ -44,22 +47,29 @@ export function getArg(el, name, val=null, type=ArgType.String) {
44
47
  // If bool, it's true as long as it exists and its value isn't falsey.
45
48
  if (type===ArgType.Bool) {
46
49
  let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
47
- return !['false', '0', false, 0, null, undefined].includes(lAttrVal);
50
+ if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
51
+ return false;
52
+ if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
53
+ return true;
54
+ return fallback;
48
55
  }
49
56
 
50
57
  // Attribute doesn't exist
58
+ let result;
51
59
  switch (type) {
52
60
  case ArgType.Int:
53
- return parseInt(val);
61
+ result = parseInt(val);
62
+ return isNaN(result) ? fallback : result;
54
63
  case ArgType.Float:
55
- return parseFloat(val);
64
+ result = parseFloat(val);
65
+ return isNaN(result) ? fallback : result;
56
66
  case ArgType.String:
57
67
  return [undefined, null, false].includes(val) ? '' : val+'';
58
- case ArgType.JSON:
68
+ case ArgType.Json:
59
69
  case ArgType.Eval:
60
70
  if (typeof val === 'string' && val.length)
61
71
  try {
62
- if (type === ArgType.JSON)
72
+ if (type === ArgType.Json)
63
73
  return JSON.parse(val);
64
74
  else
65
75
  return eval(`(${val})`);
@@ -67,6 +77,8 @@ export function getArg(el, name, val=null, type=ArgType.String) {
67
77
  return val;
68
78
  }
69
79
  else return val;
80
+
81
+ // type not provided
70
82
  default:
71
83
  return val;
72
84
  }
@@ -85,12 +97,15 @@ var ArgType = {
85
97
  Int: 'Int',
86
98
  Float: 'Float',
87
99
  String: 'String',
88
-
100
+
101
+ /** @deprecated for Json */
102
+ JSON: 'Json',
103
+
89
104
  /**
90
105
  * Parse the string value as JSON.
91
106
  * If it's not parsable, return the value as a string. */
92
- JSON: 'JSON',
93
-
107
+ Json: 'Json',
108
+
94
109
  /**
95
110
  * Evaluate the string as JavaScript using the eval() function.
96
111
  * If it can't be evaluated, return the original string. */