@webtides/element-js 1.0.0-alpha.1 → 1.0.0-alpha.2
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/dist/index.js +25 -6
- package/dist/index.js.map +3 -3
- package/index.js +2 -1
- package/package.json +1 -1
- package/src/BaseElement.js +100 -23
- package/src/StyledElement.js +33 -3
- package/src/TemplateElement.js +24 -0
- package/src/dom-parts/TemplateResult.js +2 -1
- package/src/dom-parts/directives.js +40 -15
- package/src/util/AttributeParser.js +54 -21
- package/src/util/DOMHelper.js +43 -14
- package/src/util/Directive.js +13 -0
- package/src/util/Store.js +25 -4
- package/src/util/defineElement.js +2 -2
- package/src/util/i18n.js +3 -3
- package/src/util/toString.js +2 -2
package/src/BaseElement.js
CHANGED
|
@@ -5,7 +5,44 @@ import { Store } from './util/Store.js';
|
|
|
5
5
|
export { defineElement } from './util/defineElement.js';
|
|
6
6
|
export { toString } from './util/toString.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Options object for the BaseElement
|
|
10
|
+
* @typedef {Object} BaseElementOptions
|
|
11
|
+
* @property {boolean} [autoUpdate] - When set to true the element will call the requestUpdate() method on the instance every time a property or attribute was changed. This will re-evaluate everything on the element and trigger a re-render (if a template is provided) and trigger the watchers for the affected properties/attributes. Default is `true`
|
|
12
|
+
* @property {boolean} [deferUpdate] - When set to true the element will not call the requestUpdate() method upon connecting and therefore will not render (if template was provided) initially. Default is `true`
|
|
13
|
+
* @property {MutationObserverOptions} [mutationObserverOptions]
|
|
14
|
+
* @property {PropertyOptions} [propertyOptions]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options object for the BaseElement mutationObserverOptions option
|
|
19
|
+
* @typedef {Object} MutationObserverOptions
|
|
20
|
+
* @property {boolean} [childList] - When childList is set to true the element will also call the requestUpdate method for childList modifications (eg. Adding or removing child elements). Default is `true`
|
|
21
|
+
* @property {boolean} [subtree] - Please note that observing subtree mutations might have performance implications and use it only if necessary. Default is `false`
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options object for the BaseElement propertyOptions option
|
|
26
|
+
* @typedef {Object} PropertyOptions
|
|
27
|
+
* @property {undefined | boolean | function} [reflect] - When set to true the element will reflect property changes back to attributes if the attribute was not present when connecting the element. By default, all attributes that are present when connecting the element will be reflected anyway. Default is `undefined`
|
|
28
|
+
* @property {undefined | boolean | function} [parse] - When set to false the element will not automatically try to parse the attributes string value to a complex type (number, array, object). Default is `undefined`
|
|
29
|
+
* @property {undefined | boolean | function} [notify] - When set to true the element will dispatch CustomEvents for every property/attribute change. The event name will be the property name all lowercase and camel to dashes with a postfix of -changed. Default is `undefined`
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options object for updating properties
|
|
34
|
+
* @typedef {Object} PropertyUpdateOptions
|
|
35
|
+
* @property {boolean} [notify] - Whether the update should trigger the lifecycle methods. The default is `true`
|
|
36
|
+
* @property {string} property - The property name
|
|
37
|
+
* @property {any} newValue - The new value for the property
|
|
38
|
+
* @property {string} newValueString - The new value as `string` for the property (for comparison reasons)
|
|
39
|
+
* @property {any} oldValue - The old value for the property
|
|
40
|
+
*/
|
|
41
|
+
|
|
8
42
|
class BaseElement extends HTMLElement {
|
|
43
|
+
/**
|
|
44
|
+
* @param {BaseElementOptions} options
|
|
45
|
+
*/
|
|
9
46
|
constructor(options = {}) {
|
|
10
47
|
super();
|
|
11
48
|
this.$refs = {};
|
|
@@ -28,6 +65,11 @@ class BaseElement extends HTMLElement {
|
|
|
28
65
|
};
|
|
29
66
|
}
|
|
30
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
|
|
70
|
+
* This will define the attributes and properties as reactive getters and setter,
|
|
71
|
+
* register observers, refs and events.
|
|
72
|
+
*/
|
|
31
73
|
connectedCallback() {
|
|
32
74
|
// define all attributes to "this" as properties
|
|
33
75
|
this.defineAttributesAsProperties();
|
|
@@ -54,6 +96,9 @@ class BaseElement extends HTMLElement {
|
|
|
54
96
|
}
|
|
55
97
|
}
|
|
56
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Define a MutationObserver on the element to get notified on attribute changes and childList updates according to the options specified via the constructor options.
|
|
101
|
+
*/
|
|
57
102
|
defineObserver() {
|
|
58
103
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
|
|
59
104
|
this._mutationObserver = new MutationObserver((mutations) => {
|
|
@@ -86,6 +131,9 @@ class BaseElement extends HTMLElement {
|
|
|
86
131
|
});
|
|
87
132
|
}
|
|
88
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Native `disconnectedCallback` of the HTMLElement
|
|
136
|
+
*/
|
|
89
137
|
disconnectedCallback() {
|
|
90
138
|
// remove events for elements in the component
|
|
91
139
|
this.removeEvents();
|
|
@@ -107,6 +155,11 @@ class BaseElement extends HTMLElement {
|
|
|
107
155
|
this.triggerHook('disconnected');
|
|
108
156
|
}
|
|
109
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Request and batch an asynchronous update cycle
|
|
160
|
+
* @param {PropertyUpdateOptions} options
|
|
161
|
+
* @returns {Promise<unknown>}
|
|
162
|
+
*/
|
|
110
163
|
requestUpdate(options = { notify: true }) {
|
|
111
164
|
if (options.notify === true) {
|
|
112
165
|
this.triggerHook('beforeUpdate');
|
|
@@ -136,6 +189,7 @@ class BaseElement extends HTMLElement {
|
|
|
136
189
|
* This should be called by the template component AFTER adding the template to
|
|
137
190
|
* the DOM.
|
|
138
191
|
* Here we will register the events and the refs for the element.
|
|
192
|
+
* @param {PropertyUpdateOptions} options
|
|
139
193
|
*/
|
|
140
194
|
update(options = { notify: true }) {
|
|
141
195
|
this.registerEventsAndRefs();
|
|
@@ -202,6 +256,9 @@ class BaseElement extends HTMLElement {
|
|
|
202
256
|
* Define a property on the element
|
|
203
257
|
* Will trigger watch() function when a property was changed
|
|
204
258
|
* Will trigger update() when a property was changed
|
|
259
|
+
* @param {string} property
|
|
260
|
+
* @param {any} value
|
|
261
|
+
* @param {boolean} reflectAttribute
|
|
205
262
|
*/
|
|
206
263
|
defineProperty(property, value, reflectAttribute = false) {
|
|
207
264
|
if (value instanceof Store) {
|
|
@@ -265,12 +322,10 @@ class BaseElement extends HTMLElement {
|
|
|
265
322
|
}
|
|
266
323
|
|
|
267
324
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* @param
|
|
271
|
-
* @param
|
|
272
|
-
* @param newValue
|
|
273
|
-
* @param oldValue
|
|
325
|
+
* Call the watch callbacks on property changes
|
|
326
|
+
* @param {string} property property that changes
|
|
327
|
+
* @param {any} newValue
|
|
328
|
+
* @param {any} oldValue
|
|
274
329
|
*/
|
|
275
330
|
callPropertyWatcher(property, newValue, oldValue) {
|
|
276
331
|
// notify watched properties (after update())
|
|
@@ -279,12 +334,11 @@ class BaseElement extends HTMLElement {
|
|
|
279
334
|
watch[property](newValue, oldValue);
|
|
280
335
|
}
|
|
281
336
|
}
|
|
337
|
+
|
|
282
338
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* @param
|
|
286
|
-
* @param property property that changes
|
|
287
|
-
* @param newValue
|
|
339
|
+
* Notify property observer via change event
|
|
340
|
+
* @param {string} property property that changes
|
|
341
|
+
* @param {any} newValue
|
|
288
342
|
*/
|
|
289
343
|
notifyPropertyChange(property, newValue) {
|
|
290
344
|
// dispatch change event
|
|
@@ -296,6 +350,10 @@ class BaseElement extends HTMLElement {
|
|
|
296
350
|
}
|
|
297
351
|
}
|
|
298
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Reflects a property as attribute on the element
|
|
355
|
+
* @param {PropertyUpdateOptions} options
|
|
356
|
+
*/
|
|
299
357
|
reflectProperty(options) {
|
|
300
358
|
const { property, newValue } = options;
|
|
301
359
|
const newValueString = options.newValueString || JSON.stringify(newValue);
|
|
@@ -344,9 +402,8 @@ class BaseElement extends HTMLElement {
|
|
|
344
402
|
|
|
345
403
|
/**
|
|
346
404
|
* Helper Function to initialize the context requests
|
|
347
|
-
*
|
|
348
|
-
* @param
|
|
349
|
-
* @param valueOrCallback
|
|
405
|
+
* @param {string} propertyName
|
|
406
|
+
* @param {any | function} valueOrCallback
|
|
350
407
|
*/
|
|
351
408
|
requestContext(propertyName, valueOrCallback) {
|
|
352
409
|
this.dispatch('request-context', { [propertyName]: valueOrCallback }, true);
|
|
@@ -356,8 +413,7 @@ class BaseElement extends HTMLElement {
|
|
|
356
413
|
* Internal Context Request Handler
|
|
357
414
|
* This will check if the current/receiving instance actually provides a value under the requested name.
|
|
358
415
|
* If there is something to provide it will either assign the value to the requesting element or call the callback.
|
|
359
|
-
*
|
|
360
|
-
* @param event
|
|
416
|
+
* @param {CustomEvent} event
|
|
361
417
|
*/
|
|
362
418
|
onRequestContext(event) {
|
|
363
419
|
const properties = this.properties();
|
|
@@ -379,19 +435,30 @@ class BaseElement extends HTMLElement {
|
|
|
379
435
|
});
|
|
380
436
|
}
|
|
381
437
|
|
|
382
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Connected lifecycle hook
|
|
440
|
+
*/
|
|
383
441
|
connected() {}
|
|
384
442
|
|
|
385
|
-
|
|
443
|
+
/**
|
|
444
|
+
* BeforeUpdate lifecycle hook
|
|
445
|
+
*/
|
|
386
446
|
beforeUpdate() {}
|
|
387
447
|
|
|
388
|
-
|
|
448
|
+
/**
|
|
449
|
+
* AfterUpdate lifecycle hook
|
|
450
|
+
*/
|
|
389
451
|
afterUpdate() {}
|
|
390
452
|
|
|
391
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Disconnected lifecycle hook
|
|
455
|
+
*/
|
|
392
456
|
disconnected() {}
|
|
393
457
|
|
|
394
|
-
|
|
458
|
+
/**
|
|
459
|
+
* Triggers a lifecycle hook based on the name
|
|
460
|
+
* @param {string} name
|
|
461
|
+
*/
|
|
395
462
|
triggerHook(name) {
|
|
396
463
|
if (name in this) {
|
|
397
464
|
this[name]();
|
|
@@ -501,7 +568,14 @@ class BaseElement extends HTMLElement {
|
|
|
501
568
|
this.$refs = refsMap;
|
|
502
569
|
}
|
|
503
570
|
|
|
504
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Helper function for dispatching custom events
|
|
573
|
+
* @param {string} name
|
|
574
|
+
* @param {Object} data
|
|
575
|
+
* @param {boolean} bubble
|
|
576
|
+
* @param {boolean} cancelable
|
|
577
|
+
* @param {boolean} composed
|
|
578
|
+
*/
|
|
505
579
|
dispatch(name, data, bubble = false, cancelable = false, composed = false) {
|
|
506
580
|
const event = new CustomEvent(name, {
|
|
507
581
|
bubbles: bubble,
|
|
@@ -512,7 +586,10 @@ class BaseElement extends HTMLElement {
|
|
|
512
586
|
this.dispatchEvent(event);
|
|
513
587
|
}
|
|
514
588
|
|
|
515
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Get the root element
|
|
591
|
+
* @returns {HTMLElement}
|
|
592
|
+
*/
|
|
516
593
|
getRoot() {
|
|
517
594
|
return this;
|
|
518
595
|
}
|
package/src/StyledElement.js
CHANGED
|
@@ -2,13 +2,22 @@ import { BaseElement } from './BaseElement.js';
|
|
|
2
2
|
import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper.js';
|
|
3
3
|
export { i18n } from './util/i18n.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Options object for the StyledElement
|
|
7
|
+
* @typedef {Object} StyledElementOptions
|
|
8
|
+
* @extends BaseElementOptions
|
|
9
|
+
* @property {boolean} [shadowRender] - When set to true the element will render the template (if provided) in the Shadow DOM and therefore encapsulate the element and styles from the rest of the document. Default is `false`
|
|
10
|
+
* @property {[]} [styles] - Via the styles option you can add multiple styles/stylesheets to your element. Default is `[]`
|
|
11
|
+
* @property {boolean} [adoptGlobalStyles] - When set to true element-js will look for a style element in the global document with an id of "globalStyles" and apply it before any custom/element styles inside the shadow DOM. Default is `true`
|
|
12
|
+
*/
|
|
13
|
+
|
|
5
14
|
class StyledElement extends BaseElement {
|
|
6
15
|
static globalStyles = null;
|
|
7
16
|
|
|
8
17
|
static updateGlobalStyles() {
|
|
9
18
|
// this is a runtime dependency so that every shadow dom can make use of global css
|
|
10
19
|
// we assume these styles to be inlined into the document
|
|
11
|
-
StyledElement.globalStyles = document
|
|
20
|
+
StyledElement.globalStyles = globalThis.document?.getElementById('globalStyles');
|
|
12
21
|
|
|
13
22
|
if (StyledElement.globalStyles && StyledElement['globalStyleSheet']) {
|
|
14
23
|
//updates already adopted global styles
|
|
@@ -16,6 +25,9 @@ class StyledElement extends BaseElement {
|
|
|
16
25
|
}
|
|
17
26
|
}
|
|
18
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @param {StyledElementOptions} options
|
|
30
|
+
*/
|
|
19
31
|
constructor(options) {
|
|
20
32
|
super({
|
|
21
33
|
deferUpdate: false,
|
|
@@ -27,6 +39,9 @@ class StyledElement extends BaseElement {
|
|
|
27
39
|
this._styles = [...this._options.styles, ...this.styles()];
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Overrides the `connectedCallback` to adopt optional styles when the element is connected
|
|
44
|
+
*/
|
|
30
45
|
connectedCallback() {
|
|
31
46
|
super.connectedCallback();
|
|
32
47
|
|
|
@@ -36,10 +51,19 @@ class StyledElement extends BaseElement {
|
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The styles method is another way to return a list of styles to be adopted when the element is connected.
|
|
56
|
+
* You can either provide a list in the constructor options or return it here.
|
|
57
|
+
* @returns {string[]}
|
|
58
|
+
*/
|
|
39
59
|
styles() {
|
|
40
60
|
return [];
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Overrides the `update` method to adopt optional styles
|
|
65
|
+
* @param {PropertyUpdateOptions} options
|
|
66
|
+
*/
|
|
43
67
|
update(options) {
|
|
44
68
|
if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
|
|
45
69
|
// append stylesheets to template if not already adopted
|
|
@@ -52,6 +76,9 @@ class StyledElement extends BaseElement {
|
|
|
52
76
|
super.update(options);
|
|
53
77
|
}
|
|
54
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Adopt stylesheets
|
|
81
|
+
*/
|
|
55
82
|
adoptStyleSheets() {
|
|
56
83
|
if (!this.constructor['cssStyleSheets']) {
|
|
57
84
|
this.constructor['cssStyleSheets'] = this._styles.map((style) => {
|
|
@@ -79,7 +106,10 @@ class StyledElement extends BaseElement {
|
|
|
79
106
|
];
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Custom polyfill for constructable stylesheets by appending styles to the end of an element
|
|
111
|
+
* @param {string[]} styles
|
|
112
|
+
*/
|
|
83
113
|
appendStyleSheets(styles) {
|
|
84
114
|
const parentDocument = getShadowParentOrBody(this.getRoot());
|
|
85
115
|
styles.forEach((style, index) => {
|
|
@@ -87,7 +117,7 @@ class StyledElement extends BaseElement {
|
|
|
87
117
|
|
|
88
118
|
// only append stylesheet if not already appended to shadowRoot or document
|
|
89
119
|
if (!parentDocument.querySelector(`#${identifier}`)) {
|
|
90
|
-
const styleElement = document
|
|
120
|
+
const styleElement = globalThis.document?.createElement('style');
|
|
91
121
|
styleElement.id = identifier;
|
|
92
122
|
styleElement.style.display = 'none';
|
|
93
123
|
styleElement.textContent = style;
|
package/src/TemplateElement.js
CHANGED
|
@@ -3,7 +3,16 @@ import { html } from './dom-parts/html.js';
|
|
|
3
3
|
import { render } from './dom-parts/render.js';
|
|
4
4
|
export { i18n } from './util/i18n.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Options object for the TemplateElement
|
|
8
|
+
* @typedef {Object} TemplateElementOptions
|
|
9
|
+
* @extends StyledElementOptions
|
|
10
|
+
*/
|
|
11
|
+
|
|
6
12
|
class TemplateElement extends StyledElement {
|
|
13
|
+
/**
|
|
14
|
+
* @param {TemplateElementOptions} options
|
|
15
|
+
*/
|
|
7
16
|
constructor(options) {
|
|
8
17
|
super({
|
|
9
18
|
deferUpdate: false,
|
|
@@ -20,15 +29,26 @@ class TemplateElement extends StyledElement {
|
|
|
20
29
|
if (this._options.shadowRender) this.attachShadow({ mode: 'open' });
|
|
21
30
|
}
|
|
22
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The template method should be overridden in extending elements and return the template to be rendered to the root
|
|
34
|
+
* @returns {TemplateResult}
|
|
35
|
+
*/
|
|
23
36
|
template() {
|
|
24
37
|
return html``;
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Override update method to render the template to the root
|
|
42
|
+
* @param {PropertyUpdateOptions} options
|
|
43
|
+
*/
|
|
27
44
|
update(options) {
|
|
28
45
|
this.renderTemplate();
|
|
29
46
|
super.update(options);
|
|
30
47
|
}
|
|
31
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Render the template to the root
|
|
51
|
+
*/
|
|
32
52
|
renderTemplate() {
|
|
33
53
|
const template = this._template || this.template();
|
|
34
54
|
if (typeof template === 'string') {
|
|
@@ -39,6 +59,10 @@ class TemplateElement extends StyledElement {
|
|
|
39
59
|
}
|
|
40
60
|
}
|
|
41
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get the root element - either the element or the shadow root
|
|
64
|
+
* @returns {ShadowRoot | HTMLElement}
|
|
65
|
+
*/
|
|
42
66
|
getRoot() {
|
|
43
67
|
return this.shadowRoot !== null ? this.shadowRoot : this;
|
|
44
68
|
}
|
|
@@ -213,7 +213,8 @@ export class TemplateResult {
|
|
|
213
213
|
// TODO: we currently cannot distinguish between NodeParts and AttributeParts before
|
|
214
214
|
if (isObjectLike(value) && value.directiveClass) {
|
|
215
215
|
// NodePart
|
|
216
|
-
|
|
216
|
+
const directive = new value.directiveClass();
|
|
217
|
+
result += directive.stringify(...value.values);
|
|
217
218
|
} else if (value != null) {
|
|
218
219
|
result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
|
|
219
220
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { camelToDash, decodeAttribute, encodeAttribute } from '../util/AttributeParser.js';
|
|
2
|
-
import { TemplateResult } from './TemplateResult';
|
|
3
|
-
import { convertStringToHTML } from '../util/DOMHelper';
|
|
2
|
+
import { TemplateResult } from './TemplateResult.js';
|
|
3
|
+
import { convertStringToHTML, convertStringToTemplate } from '../util/DOMHelper.js';
|
|
4
|
+
import { defineDirective, Directive } from '../util/Directive.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Maps a list of classes to an element from an object.
|
|
@@ -55,30 +56,54 @@ const choose = (value, cases, defaultCase) => {
|
|
|
55
56
|
/**
|
|
56
57
|
* Renders a given string as HTML instead of text
|
|
57
58
|
* @param {string} string
|
|
58
|
-
* @returns {function(): Node}
|
|
59
|
+
* @returns {function(): Node | string}
|
|
59
60
|
*/
|
|
60
61
|
const unsafeHTML = (string) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (!globalThis.DOMParser) return () => string;
|
|
63
|
+
const node = convertStringToHTML(string).firstChild;
|
|
64
|
+
const importedNode = globalThis.document?.importNode(node, true);
|
|
65
|
+
return () => importedNode;
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
68
|
-
* @param {Object.<string, any>} attributes
|
|
69
|
-
* @returns {function(): string}
|
|
70
70
|
*/
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
export class SpreadAttributesDirective extends Directive {
|
|
72
|
+
/**
|
|
73
|
+
* @param {Object.<string, any>} attributes
|
|
74
|
+
*/
|
|
75
|
+
update(attributes) {
|
|
76
|
+
// TODO: what if the attribute was already there..?!
|
|
77
|
+
// TODO: what if the attribute was there from a previous render/update, but not anymore?!
|
|
78
|
+
for (const name of Object.keys(attributes)) {
|
|
79
|
+
const value = attributes[name];
|
|
80
|
+
this.node.setAttribute(
|
|
81
|
+
camelToDash(name),
|
|
82
|
+
encodeAttribute(typeof value !== 'string' ? JSON.stringify(value) : value),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {Object.<string, any>} attributes
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
stringify(attributes) {
|
|
73
92
|
return Object.keys(attributes)
|
|
74
|
-
.map((
|
|
75
|
-
let value = attributes[
|
|
76
|
-
return `${camelToDash(
|
|
93
|
+
.map((name) => {
|
|
94
|
+
let value = attributes[name];
|
|
95
|
+
return `${camelToDash(name)}='${encodeAttribute(
|
|
77
96
|
typeof value !== 'string' ? JSON.stringify(value) : value,
|
|
78
97
|
)}'`;
|
|
79
98
|
})
|
|
80
99
|
.join(' ');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
105
|
+
* @param {Object.<string, any>} attributes
|
|
106
|
+
*/
|
|
107
|
+
const spreadAttributes = defineDirective(SpreadAttributesDirective);
|
|
83
108
|
|
|
84
109
|
export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
|
|
@@ -1,23 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests if a value is of type `object`
|
|
3
|
+
* @param {any} value
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
1
6
|
export function isObjectLike(value) {
|
|
2
7
|
return typeof value == 'object' && value !== null;
|
|
3
8
|
}
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Tests if a value can be parsed as JSON
|
|
12
|
+
* @param {any} value
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function isJSON(value) {
|
|
6
16
|
try {
|
|
7
|
-
return JSON.parse(
|
|
17
|
+
return JSON.parse(value) && !!value;
|
|
8
18
|
} catch (e) {
|
|
9
19
|
return false;
|
|
10
20
|
}
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Tests if a value is `boolean`
|
|
25
|
+
* @param {any} value
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
13
28
|
export function isBoolean(value) {
|
|
14
29
|
return value === 'true' || value === 'false';
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Parses a value to `boolean`
|
|
34
|
+
* @param {any} value
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
17
37
|
export function parseBoolean(value) {
|
|
18
38
|
return value === 'true';
|
|
19
39
|
}
|
|
20
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Tests if a value is of type `string`
|
|
43
|
+
* @param {any} value
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
21
46
|
export function isString(value) {
|
|
22
47
|
return (
|
|
23
48
|
typeof value === 'string' ||
|
|
@@ -25,26 +50,40 @@ export function isString(value) {
|
|
|
25
50
|
);
|
|
26
51
|
}
|
|
27
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Tests if a value is of type `number`
|
|
55
|
+
* @param {any} value
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
28
58
|
export function isNumber(value) {
|
|
29
59
|
return new RegExp('^-?(0|0\\.\\d+|[1-9]\\d*(\\.\\d+)?)$').test(value);
|
|
30
60
|
}
|
|
31
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Tests if a value is of type `NaN`
|
|
64
|
+
* @param {any} value
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
32
67
|
export function isNaN(value) {
|
|
33
68
|
return Number.isNaN(value);
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param
|
|
40
|
-
* @param valueB
|
|
72
|
+
* Compare two values (deep). It compares primitive or complex values with JSON stringify.
|
|
73
|
+
* @param {any} valueA
|
|
74
|
+
* @param {any} valueB
|
|
41
75
|
* @returns {boolean}
|
|
42
76
|
*/
|
|
43
|
-
|
|
44
77
|
export function deepEquals(valueA, valueB) {
|
|
45
78
|
return JSON.stringify(valueA) === JSON.stringify(valueB);
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Parses an attribute value that comes in as a `string` to its corresponding type
|
|
83
|
+
* @param {string} value
|
|
84
|
+
* @param {PropertyOptions} options
|
|
85
|
+
* @returns {string | number | boolean | object | array}
|
|
86
|
+
*/
|
|
48
87
|
export function parseAttribute(value, options = {}) {
|
|
49
88
|
if (options.parse === false || !isString(value)) {
|
|
50
89
|
// no-op
|
|
@@ -67,7 +106,8 @@ export function parseAttribute(value, options = {}) {
|
|
|
67
106
|
|
|
68
107
|
/**
|
|
69
108
|
* Replaces dashed-expression (i.e. some-value) to a camel-cased expression (i.e. someValue)
|
|
70
|
-
* @
|
|
109
|
+
* @param {string} string
|
|
110
|
+
* @returns {string}
|
|
71
111
|
*/
|
|
72
112
|
export function dashToCamel(string) {
|
|
73
113
|
if (string.indexOf('-') === -1) return string;
|
|
@@ -77,17 +117,16 @@ export function dashToCamel(string) {
|
|
|
77
117
|
|
|
78
118
|
/**
|
|
79
119
|
* Replaces camel-cased expression (i.e. someValue) to a dashed-expression (i.e. some-value)
|
|
80
|
-
* @
|
|
120
|
+
* @param {string} string
|
|
121
|
+
* @returns {string}
|
|
81
122
|
*/
|
|
82
123
|
export function camelToDash(string) {
|
|
83
124
|
return string.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
84
125
|
}
|
|
85
126
|
|
|
86
127
|
/**
|
|
87
|
-
* Decodes an attribute.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @param attribute
|
|
128
|
+
* Decodes an attribute.
|
|
129
|
+
* @param {string} attribute
|
|
91
130
|
* @returns {string}
|
|
92
131
|
*/
|
|
93
132
|
export function decodeAttribute(attribute) {
|
|
@@ -99,14 +138,8 @@ export function decodeAttribute(attribute) {
|
|
|
99
138
|
}
|
|
100
139
|
|
|
101
140
|
/**
|
|
102
|
-
* Encodes an attribute.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* This does not use the standard way to encode an attribute, as the dom parser
|
|
106
|
-
* that renders the html would understand and automatically decode it. Which results
|
|
107
|
-
* in non sanitized html output.
|
|
108
|
-
*
|
|
109
|
-
* @param attribute
|
|
141
|
+
* Encodes an attribute.
|
|
142
|
+
* @param {string} attribute
|
|
110
143
|
* @returns {string}
|
|
111
144
|
*/
|
|
112
145
|
export function encodeAttribute(attribute) {
|