@webtides/element-js 1.0.0-alpha.1 → 1.0.0-alpha.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.3",
4
4
  "module": "index.js",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,11 +1,53 @@
1
1
  import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
2
- import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
2
+ import { getAllElementChildren, getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
3
3
  import { Store } from './util/Store.js';
4
-
4
+ import { serializeState, deserializeState, hasSerializedState } from './util/SerializeStateHelper.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
+
42
+ /**
43
+ * @implements {Serializable}
44
+ */
8
45
  class BaseElement extends HTMLElement {
46
+ _serializationKey;
47
+
48
+ /**
49
+ * @param {BaseElementOptions} options
50
+ */
9
51
  constructor(options = {}) {
10
52
  super();
11
53
  this.$refs = {};
@@ -17,6 +59,7 @@ class BaseElement extends HTMLElement {
17
59
  this._options = {
18
60
  autoUpdate: true,
19
61
  deferUpdate: true,
62
+ deferConnected: false,
20
63
  mutationObserverOptions: {
21
64
  attributes: true,
22
65
  childList: true,
@@ -26,9 +69,36 @@ class BaseElement extends HTMLElement {
26
69
  propertyOptions: {},
27
70
  ...options,
28
71
  };
72
+
73
+ if (this.hasAttribute('defer-update')) {
74
+ this._options.deferUpdate = true;
75
+ }
76
+ if (this.hasAttribute('defer-connected')) {
77
+ this._options.deferConnected = true;
78
+ }
29
79
  }
30
80
 
81
+ /**
82
+ * Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
83
+ * This will define the attributes and properties as reactive getters and setter,
84
+ * register observers, refs and events.
85
+ */
31
86
  connectedCallback() {
87
+ if (this._options.deferConnected) {
88
+ this._options.deferConnected = false;
89
+ return;
90
+ }
91
+
92
+ if (this.hasAttribute('ejs:key')) {
93
+ this._serializationKey = this.getAttribute('ejs:key');
94
+ deserializeState(this);
95
+ } else {
96
+ this._serializationKey = globalThis.crypto.randomUUID();
97
+ if (globalThis.elementJsConfig?.serializeState) {
98
+ this.setAttribute('ejs:key', this._serializationKey);
99
+ }
100
+ }
101
+
32
102
  // define all attributes to "this" as properties
33
103
  this.defineAttributesAsProperties();
34
104
 
@@ -54,6 +124,9 @@ class BaseElement extends HTMLElement {
54
124
  }
55
125
  }
56
126
 
127
+ /**
128
+ * Define a MutationObserver on the element to get notified on attribute changes and childList updates according to the options specified via the constructor options.
129
+ */
57
130
  defineObserver() {
58
131
  // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
59
132
  this._mutationObserver = new MutationObserver((mutations) => {
@@ -86,6 +159,9 @@ class BaseElement extends HTMLElement {
86
159
  });
87
160
  }
88
161
 
162
+ /**
163
+ * Native `disconnectedCallback` of the HTMLElement
164
+ */
89
165
  disconnectedCallback() {
90
166
  // remove events for elements in the component
91
167
  this.removeEvents();
@@ -107,6 +183,11 @@ class BaseElement extends HTMLElement {
107
183
  this.triggerHook('disconnected');
108
184
  }
109
185
 
186
+ /**
187
+ * Request and batch an asynchronous update cycle
188
+ * @param {PropertyUpdateOptions} options
189
+ * @returns {Promise<unknown>}
190
+ */
110
191
  requestUpdate(options = { notify: true }) {
111
192
  if (options.notify === true) {
112
193
  this.triggerHook('beforeUpdate');
@@ -136,6 +217,7 @@ class BaseElement extends HTMLElement {
136
217
  * This should be called by the template component AFTER adding the template to
137
218
  * the DOM.
138
219
  * Here we will register the events and the refs for the element.
220
+ * @param {PropertyUpdateOptions} options
139
221
  */
140
222
  update(options = { notify: true }) {
141
223
  this.registerEventsAndRefs();
@@ -202,12 +284,11 @@ class BaseElement extends HTMLElement {
202
284
  * Define a property on the element
203
285
  * Will trigger watch() function when a property was changed
204
286
  * Will trigger update() when a property was changed
287
+ * @param {string} property
288
+ * @param {any} value
289
+ * @param {boolean} reflectAttribute
205
290
  */
206
291
  defineProperty(property, value, reflectAttribute = false) {
207
- if (value instanceof Store) {
208
- value.subscribe(this);
209
- }
210
-
211
292
  if (this._state.hasOwnProperty(property)) {
212
293
  // property has already been defined as an attribute nothing to do here
213
294
  return;
@@ -223,8 +304,16 @@ class BaseElement extends HTMLElement {
223
304
  this.removeAttribute(camelToDash(property));
224
305
  }
225
306
 
307
+ if (hasSerializedState(this._serializationKey) && Object.keys(this.serializeState())?.includes(property)) {
308
+ value = this[property];
309
+ }
310
+
226
311
  this._state[property] = value;
227
312
 
313
+ if (this._state[property] instanceof Store) {
314
+ this._state[property].subscribe(this);
315
+ }
316
+
228
317
  Object.defineProperty(this, property, {
229
318
  get: () => {
230
319
  return this._state[property];
@@ -235,6 +324,8 @@ class BaseElement extends HTMLElement {
235
324
  if (JSON.stringify(oldValue) !== newValueString) {
236
325
  this._state[property] = newValue;
237
326
 
327
+ serializeState(this);
328
+
238
329
  if (newValue instanceof Store) {
239
330
  newValue.subscribe(this);
240
331
  }
@@ -265,12 +356,10 @@ class BaseElement extends HTMLElement {
265
356
  }
266
357
 
267
358
  /**
268
- * call the watch callbacks on property changes
269
- *
270
- * @param context is the elements instance
271
- * @param property property that changes
272
- * @param newValue
273
- * @param oldValue
359
+ * Call the watch callbacks on property changes
360
+ * @param {string} property property that changes
361
+ * @param {any} newValue
362
+ * @param {any} oldValue
274
363
  */
275
364
  callPropertyWatcher(property, newValue, oldValue) {
276
365
  // notify watched properties (after update())
@@ -279,12 +368,11 @@ class BaseElement extends HTMLElement {
279
368
  watch[property](newValue, oldValue);
280
369
  }
281
370
  }
371
+
282
372
  /**
283
- * notify property observer via change event
284
- *
285
- * @param context is the elements instance
286
- * @param property property that changes
287
- * @param newValue
373
+ * Notify property observer via change event
374
+ * @param {string} property property that changes
375
+ * @param {any} newValue
288
376
  */
289
377
  notifyPropertyChange(property, newValue) {
290
378
  // dispatch change event
@@ -296,6 +384,10 @@ class BaseElement extends HTMLElement {
296
384
  }
297
385
  }
298
386
 
387
+ /**
388
+ * Reflects a property as attribute on the element
389
+ * @param {PropertyUpdateOptions} options
390
+ */
299
391
  reflectProperty(options) {
300
392
  const { property, newValue } = options;
301
393
  const newValueString = options.newValueString || JSON.stringify(newValue);
@@ -337,30 +429,40 @@ class BaseElement extends HTMLElement {
337
429
  this.requestContext(key, value);
338
430
  });
339
431
  // define context provider
340
- if (Object.keys(this.provideProperties()).length > 0) {
432
+ const providedKeys = Object.keys(this.provideProperties());
433
+ if (providedKeys.length > 0) {
341
434
  this.addEventListener('request-context', this.onRequestContext);
435
+ // check if there are already connected elements in child dom and restart requests
436
+ getAllElementChildren(this.getRoot(), BaseElement).forEach((customChild) => {
437
+ // if injectProperties?.() is defined it means that the child got connected BEFORE the parent (Runtime Issue)
438
+ const requestedProperties = customChild.injectProperties?.() ?? {};
439
+ Object.entries(requestedProperties).forEach(([key, value]) => {
440
+ // iterate over requested properties to find missed context requests
441
+ if (providedKeys.includes(key)) {
442
+ // owns the provided key -> replay request
443
+ customChild.requestContext(key, value);
444
+ }
445
+ });
446
+ });
342
447
  }
343
448
  }
344
449
 
345
450
  /**
346
451
  * Helper Function to initialize the context requests
347
- *
348
- * @param propertyName
349
- * @param valueOrCallback
452
+ * @param {string} propertyName
453
+ * @param {any | function} valueOrCallback
350
454
  */
351
455
  requestContext(propertyName, valueOrCallback) {
352
- this.dispatch('request-context', { [propertyName]: valueOrCallback }, true);
456
+ this.dispatch('request-context', { [propertyName]: valueOrCallback }, true, true, true);
353
457
  }
354
458
 
355
459
  /**
356
460
  * Internal Context Request Handler
357
461
  * This will check if the current/receiving instance actually provides a value under the requested name.
358
462
  * If there is something to provide it will either assign the value to the requesting element or call the callback.
359
- *
360
- * @param event
463
+ * @param {CustomEvent} event
361
464
  */
362
465
  onRequestContext(event) {
363
- const properties = this.properties();
364
466
  const provideProperties = this.provideProperties();
365
467
 
366
468
  Object.entries(event.detail ?? {}).forEach(([key, valueOrCallback]) => {
@@ -373,25 +475,38 @@ class BaseElement extends HTMLElement {
373
475
  valueOrCallback(providedValue);
374
476
  } else {
375
477
  // assign to prop
376
- event.target[key] = providedValue;
478
+ const eventPath = event.composedPath();
479
+ const target = eventPath[0] || event.target;
480
+ target[key] = providedValue;
377
481
  }
378
482
  }
379
483
  });
380
484
  }
381
485
 
382
- // Connected lifecycle hook
486
+ /**
487
+ * Connected lifecycle hook
488
+ */
383
489
  connected() {}
384
490
 
385
- // BeforeUpdate lifecycle hook
491
+ /**
492
+ * BeforeUpdate lifecycle hook
493
+ */
386
494
  beforeUpdate() {}
387
495
 
388
- // AfterUpdate lifecycle hook
496
+ /**
497
+ * AfterUpdate lifecycle hook
498
+ */
389
499
  afterUpdate() {}
390
500
 
391
- // Disconnected lifecycle hook
501
+ /**
502
+ * Disconnected lifecycle hook
503
+ */
392
504
  disconnected() {}
393
505
 
394
- // Triggers a lifecycle hook based on the name
506
+ /**
507
+ * Triggers a lifecycle hook based on the name
508
+ * @param {string} name
509
+ */
395
510
  triggerHook(name) {
396
511
  if (name in this) {
397
512
  this[name]();
@@ -501,21 +616,66 @@ class BaseElement extends HTMLElement {
501
616
  this.$refs = refsMap;
502
617
  }
503
618
 
504
- // Helper function for dispatching custom events
505
- dispatch(name, data, bubble = false, cancelable = false, composed = false) {
506
- const event = new CustomEvent(name, {
507
- bubbles: bubble,
508
- cancelable: cancelable,
509
- composed: composed,
510
- detail: data,
511
- });
619
+ /**
620
+ * Helper function for dispatching custom events
621
+ * @param {string} name
622
+ * @param {Object} data
623
+ * @param {CustomEventInit} options
624
+ * @param {boolean} cancelable - DEPRECATED
625
+ * @param {boolean} composed - DEPRECATED
626
+ */
627
+ dispatch(name, data, options, cancelable = false, composed = false) {
628
+ let eventInit = {};
629
+
630
+ if (options !== undefined && typeof options === 'boolean') {
631
+ eventInit['bubbles'] = options;
632
+ eventInit['cancelable'] = cancelable;
633
+ eventInit['composed'] = composed;
634
+ eventInit['detail'] = data;
635
+ console.warn(
636
+ `[${this.localName}] Using the old parameter options for the event is deprecated and will be removed in v2.0! Please use the new options object that is in line with the "CustomEventInit" options`,
637
+ );
638
+ } else {
639
+ eventInit = {
640
+ bubbles: true,
641
+ cancelable: true,
642
+ composed: true,
643
+ detail: data,
644
+ ...options,
645
+ };
646
+ }
647
+
648
+ const event = new CustomEvent(name, eventInit);
512
649
  this.dispatchEvent(event);
513
650
  }
514
651
 
515
- // Get the root element
652
+ /**
653
+ * Get the root element
654
+ * @returns {HTMLElement}
655
+ */
516
656
  getRoot() {
517
657
  return this;
518
658
  }
659
+
660
+ /**
661
+ * This method will be used to serialize this elements state.
662
+ * By default, it will generate an object with all properties from this.properties().
663
+ * You can override this and return a different object with more/less/different properties.
664
+ * @return {{[string: any]: *}}
665
+ */
666
+ serializeState() {
667
+ const keys = Object.keys(this.properties());
668
+ return Object.fromEntries(keys.map((key) => [key, this[key]]));
669
+ }
670
+
671
+ /**
672
+ * This method will be used to restore properties on the element from a previously serialized state.
673
+ * You can override this and handle the deserialization differently if the default is not enough.
674
+ * @param {{[string: any]: *}} serializedState
675
+ */
676
+ restoreState(serializedState) {
677
+ Object.assign(this, serializedState);
678
+ }
519
679
  }
520
680
 
521
681
  export { BaseElement, getClosestParentCustomElementNode, isOfSameNodeType };
@@ -1,21 +1,20 @@
1
1
  import { BaseElement } from './BaseElement.js';
2
2
  import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper.js';
3
- export { i18n } from './util/i18n.js';
3
+ import { globalStylesStore } from './util/GlobalStylesStore.js';
4
4
 
5
- class StyledElement extends BaseElement {
6
- static globalStyles = null;
7
-
8
- static updateGlobalStyles() {
9
- // this is a runtime dependency so that every shadow dom can make use of global css
10
- // we assume these styles to be inlined into the document
11
- StyledElement.globalStyles = document.getElementById('globalStyles');
12
-
13
- if (StyledElement.globalStyles && StyledElement['globalStyleSheet']) {
14
- //updates already adopted global styles
15
- StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
16
- }
17
- }
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 | string | string[]} [adoptGlobalStyles] - When set to true element-js will look for all style elements in the global document with and apply them before any custom/element styles inside the shadow DOM. Default is `true`.
12
+ */
18
13
 
14
+ class StyledElement extends BaseElement {
15
+ /**
16
+ * @param {StyledElementOptions} options
17
+ */
19
18
  constructor(options) {
20
19
  super({
21
20
  deferUpdate: false,
@@ -27,76 +26,94 @@ class StyledElement extends BaseElement {
27
26
  this._styles = [...this._options.styles, ...this.styles()];
28
27
  }
29
28
 
29
+ /**
30
+ * Overrides the `connectedCallback` to adopt optional styles when the element is connected
31
+ */
30
32
  connectedCallback() {
31
33
  super.connectedCallback();
32
34
 
35
+ if (!this.constructor['elementStyleSheets']) {
36
+ this.constructor['elementStyleSheets'] = this._styles.map((style) => {
37
+ const cssStyleSheet = new CSSStyleSheet();
38
+ cssStyleSheet.replaceSync(style);
39
+ return cssStyleSheet;
40
+ });
41
+ }
42
+
33
43
  if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
34
44
  // adopting does only make sense in shadow dom. Fall back to append for light elements
35
45
  this.adoptStyleSheets();
46
+
47
+ if (this._options.adoptGlobalStyles !== false) {
48
+ globalStylesStore.subscribe(() => {
49
+ this.adoptStyleSheets();
50
+ });
51
+ }
36
52
  }
37
53
  }
38
54
 
55
+ /**
56
+ * The styles method is another way to return a list of styles to be adopted when the element is connected.
57
+ * You can either provide a list in the constructor options or return it here.
58
+ * @returns {string[]}
59
+ */
39
60
  styles() {
40
61
  return [];
41
62
  }
42
63
 
64
+ /**
65
+ * Overrides the `update` method to adopt optional styles
66
+ * @param {PropertyUpdateOptions} options
67
+ */
43
68
  update(options) {
69
+ // We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
44
70
  if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
45
- // append stylesheets to template if not already adopted
46
- const appendableStyles = [...this._styles];
47
- if (this._options.shadowRender && this._options.adoptGlobalStyles) {
48
- appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
49
- }
50
- this.appendStyleSheets(appendableStyles);
71
+ this.appendStyleSheets();
51
72
  }
52
73
  super.update(options);
53
74
  }
54
75
 
76
+ /**
77
+ * Adopt stylesheets
78
+ */
55
79
  adoptStyleSheets() {
56
- if (!this.constructor['cssStyleSheets']) {
57
- this.constructor['cssStyleSheets'] = this._styles.map((style) => {
58
- const sheet = new CSSStyleSheet();
59
- if (sheet && sheet.cssRules.length === 0) {
60
- sheet.replaceSync(style);
61
- }
62
- return sheet;
63
- });
64
- }
65
-
66
- if (StyledElement.globalStyles && !StyledElement['globalStyleSheet']) {
67
- StyledElement['globalStyleSheet'] = new CSSStyleSheet();
68
- StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
69
- }
80
+ const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
70
81
 
71
- // adopt styles
72
- // uses proposed solution for constructable stylesheets
73
- // see: https://wicg.github.io/construct-stylesheets/#proposed-solution
74
82
  this.getRoot().adoptedStyleSheets = [
75
- ...(this._options.shadowRender && this._options.adoptGlobalStyles && StyledElement['globalStyleSheet']
76
- ? [StyledElement['globalStyleSheet']]
77
- : []),
78
- ...this.constructor['cssStyleSheets'],
83
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
+ ...this.constructor['elementStyleSheets'],
79
85
  ];
80
86
  }
81
87
 
82
- // custom polyfill for constructable stylesheets by appending styles to the end of an element
83
- appendStyleSheets(styles) {
88
+ /**
89
+ * Custom polyfill for constructable stylesheets by appending styles to the end of an element
90
+ */
91
+ appendStyleSheets() {
84
92
  const parentDocument = getShadowParentOrBody(this.getRoot());
85
- styles.forEach((style, index) => {
93
+
94
+ const adoptGlobalStyleSheets =
95
+ this._options.shadowRender && this._options.adoptGlobalStyles !== false && parentDocument !== document.body;
96
+
97
+ const appendableStyles = [
98
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
99
+ ...this.constructor['elementStyleSheets'],
100
+ ];
101
+
102
+ appendableStyles.forEach((styleSheet, index) => {
86
103
  const identifier = this.tagName + index;
87
104
 
88
105
  // only append stylesheet if not already appended to shadowRoot or document
89
106
  if (!parentDocument.querySelector(`#${identifier}`)) {
90
- const styleElement = document.createElement('style');
107
+ const styleElement = globalThis.document?.createElement('style');
91
108
  styleElement.id = identifier;
92
109
  styleElement.style.display = 'none';
93
- styleElement.textContent = style;
110
+ styleElement.textContent = Array.from(styleSheet.cssRules)
111
+ .map((rule) => rule.cssText)
112
+ .join('');
94
113
  parentDocument.appendChild(styleElement);
95
114
  }
96
115
  });
97
116
  }
98
117
  }
99
118
 
100
- StyledElement.updateGlobalStyles();
101
-
102
119
  export { StyledElement };
@@ -1,9 +1,17 @@
1
1
  import { StyledElement } from './StyledElement.js';
2
2
  import { html } from './dom-parts/html.js';
3
3
  import { render } from './dom-parts/render.js';
4
- export { i18n } from './util/i18n.js';
4
+
5
+ /**
6
+ * Options object for the TemplateElement
7
+ * @typedef {Object} TemplateElementOptions
8
+ * @extends StyledElementOptions
9
+ */
5
10
 
6
11
  class TemplateElement extends StyledElement {
12
+ /**
13
+ * @param {TemplateElementOptions} options
14
+ */
7
15
  constructor(options) {
8
16
  super({
9
17
  deferUpdate: false,
@@ -20,15 +28,26 @@ class TemplateElement extends StyledElement {
20
28
  if (this._options.shadowRender) this.attachShadow({ mode: 'open' });
21
29
  }
22
30
 
31
+ /**
32
+ * The template method should be overridden in extending elements and return the template to be rendered to the root
33
+ * @returns {TemplateResult}
34
+ */
23
35
  template() {
24
36
  return html``;
25
37
  }
26
38
 
39
+ /**
40
+ * Override update method to render the template to the root
41
+ * @param {PropertyUpdateOptions} options
42
+ */
27
43
  update(options) {
28
44
  this.renderTemplate();
29
45
  super.update(options);
30
46
  }
31
47
 
48
+ /**
49
+ * Render the template to the root
50
+ */
32
51
  renderTemplate() {
33
52
  const template = this._template || this.template();
34
53
  if (typeof template === 'string') {
@@ -39,6 +58,10 @@ class TemplateElement extends StyledElement {
39
58
  }
40
59
  }
41
60
 
61
+ /**
62
+ * Get the root element - either the element or the shadow root
63
+ * @returns {ShadowRoot | HTMLElement}
64
+ */
42
65
  getRoot() {
43
66
  return this.shadowRoot !== null ? this.shadowRoot : this;
44
67
  }
@@ -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
- // result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
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
  }