@webtides/element-js 0.6.2 → 0.7.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.
package/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { BaseElement } from './src/BaseElement';
2
- import { StyledElement } from './src/StyledElement';
3
- import { TemplateElement, html } from './src/TemplateElement';
4
- import { toString } from './src/util/toString';
5
- import { defineElement } from './src/util/defineElement';
6
- import { i18n } from './src/util/i18n';
7
- import { Store } from './src/util/Store';
1
+ import { BaseElement } from './src/BaseElement.js';
2
+ import { StyledElement } from './src/StyledElement.js';
3
+ import { TemplateElement, html } from './src/TemplateElement.js';
4
+ import { toString } from './src/util/toString.js';
5
+ import { defineElement } from './src/util/defineElement.js';
6
+ import { i18n } from './src/util/i18n.js';
7
+ import { Store } from './src/util/Store.js';
8
8
 
9
9
  export { BaseElement, StyledElement, TemplateElement, Store, html, toString, defineElement, i18n };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "module": "index.js",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -45,7 +45,7 @@
45
45
  "@web/dev-server": "^0.1.30",
46
46
  "@web/test-runner": "^0.13.26",
47
47
  "babel-eslint": "^10.1.0",
48
- "babel-plugin-module-resolver": "^4.0.0",
48
+ "babel-plugin-module-resolver": "^5.0.0",
49
49
  "deepmerge": "^4.2.2",
50
50
  "esbuild": "^0.8.57",
51
51
  "eslint": "^7.0.0",
@@ -2,8 +2,8 @@ import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from '.
2
2
  import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
3
3
  import { Store } from './util/Store.js';
4
4
 
5
- export { defineElement } from './util/defineElement';
6
- export { toString } from './util/toString';
5
+ export { defineElement } from './util/defineElement.js';
6
+ export { toString } from './util/toString.js';
7
7
 
8
8
  class BaseElement extends HTMLElement {
9
9
  constructor(options = {}) {
@@ -45,6 +45,9 @@ class BaseElement extends HTMLElement {
45
45
  // define all computed properties to "this"
46
46
  this.defineComputedProperties();
47
47
 
48
+ // define context
49
+ this.definePropertyInjection();
50
+
48
51
  // define everything that should be observed
49
52
  this.defineObserver();
50
53
 
@@ -96,6 +99,10 @@ class BaseElement extends HTMLElement {
96
99
  // remove events for elements in the component
97
100
  this.removeEvents();
98
101
 
102
+ if (Object.keys(this.provideProperties()).length > 0) {
103
+ this.removeEventListener('request-context', this.onRequestContext);
104
+ }
105
+
99
106
  // remove observers
100
107
  if (this._mutationObserver) this._mutationObserver.disconnect();
101
108
 
@@ -189,7 +196,7 @@ class BaseElement extends HTMLElement {
189
196
  * Will trigger update() when a property was changed
190
197
  */
191
198
  defineProperties() {
192
- Object.keys(this.properties()).forEach((prop) => {
199
+ Object.keys({ ...this.injectProperties(), ...this.properties() }).forEach((prop) => {
193
200
  // when mixing shadow dom elements with light dom elements and nesting custom elements
194
201
  // it might occur that properties where set on an element before it was
195
202
  // registered or connected. To avoid such timing issues we check
@@ -234,29 +241,17 @@ class BaseElement extends HTMLElement {
234
241
  set: (newValue) => {
235
242
  const oldValue = this._state[property];
236
243
  const newValueString = JSON.stringify(newValue);
237
-
238
244
  if (JSON.stringify(oldValue) !== newValueString) {
239
245
  this._state[property] = newValue;
240
246
 
247
+ if (newValue instanceof Store) {
248
+ newValue.subscribe(this);
249
+ }
250
+
241
251
  if (reflectAttribute || this._options.propertyOptions[property]?.reflect) {
242
252
  this.reflectProperty({ property, newValue, newValueString });
243
253
  }
244
254
 
245
- const informWatchedPropertiesAndDispatchChangeEvent = () => {
246
- // notify watched properties (after update())
247
- if (property in this.watch()) {
248
- this.watch()[property](newValue, oldValue);
249
- }
250
-
251
- // dispatch change event
252
- if (
253
- property in this._options['propertyOptions'] &&
254
- this._options['propertyOptions'][property]['notify'] === true
255
- ) {
256
- this.dispatch(`${camelToDash(property)}-changed`, newValue, true);
257
- }
258
- };
259
-
260
255
  if (this._options.autoUpdate) {
261
256
  this.requestUpdate({
262
257
  notify: true,
@@ -265,18 +260,51 @@ class BaseElement extends HTMLElement {
265
260
  newValueString: newValueString,
266
261
  oldValue: oldValue,
267
262
  }).finally(() => {
268
- informWatchedPropertiesAndDispatchChangeEvent();
263
+ this.callPropertyWatcher(property, newValue, oldValue);
264
+ this.notifyPropertyChange(property, newValue);
269
265
  });
270
266
  } else {
271
- informWatchedPropertiesAndDispatchChangeEvent();
267
+ this.callPropertyWatcher(property, newValue, oldValue);
268
+ this.notifyPropertyChange(property, newValue);
272
269
  }
273
270
  }
274
-
275
271
  return this;
276
272
  },
277
273
  });
278
274
  }
279
275
 
276
+ /**
277
+ * call the watch callbacks on property changes
278
+ *
279
+ * @param context is the elements instance
280
+ * @param property property that changes
281
+ * @param newValue
282
+ * @param oldValue
283
+ */
284
+ callPropertyWatcher(property, newValue, oldValue) {
285
+ // notify watched properties (after update())
286
+ const watch = this.watch();
287
+ if (property in watch) {
288
+ watch[property](newValue, oldValue);
289
+ }
290
+ }
291
+ /**
292
+ * notify property observer via change event
293
+ *
294
+ * @param context is the elements instance
295
+ * @param property property that changes
296
+ * @param newValue
297
+ */
298
+ notifyPropertyChange(property, newValue) {
299
+ // dispatch change event
300
+ if (
301
+ property in this._options['propertyOptions'] &&
302
+ this._options['propertyOptions'][property]['notify'] === true
303
+ ) {
304
+ this.dispatch(`${camelToDash(property)}-changed`, newValue, true);
305
+ }
306
+ }
307
+
280
308
  reflectProperty(options) {
281
309
  const { property, newValue } = options;
282
310
  const newValueString = options.newValueString || JSON.stringify(newValue);
@@ -294,6 +322,72 @@ class BaseElement extends HTMLElement {
294
322
  }
295
323
  }
296
324
 
325
+ /**
326
+ * Context Properties to issue Context Requests and to pull contextual properties into the elements scope
327
+ * @return {{}}
328
+ */
329
+ injectProperties() {
330
+ return {};
331
+ }
332
+
333
+ /**
334
+ * Define which properties that can be provided to children
335
+ * @return {{}}
336
+ */
337
+ provideProperties() {
338
+ return {};
339
+ }
340
+
341
+ /**
342
+ * Defines context on the element based on keys from this.injectProperties()
343
+ */
344
+ definePropertyInjection() {
345
+ Object.entries(this.injectProperties()).forEach(([key, value]) => {
346
+ this.requestContext(key, value);
347
+ });
348
+ // define context provider
349
+ if (Object.keys(this.provideProperties()).length > 0) {
350
+ this.addEventListener('request-context', this.onRequestContext);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Helper Function to initialize the context requests
356
+ *
357
+ * @param propertyName
358
+ * @param valueOrCallback
359
+ */
360
+ requestContext(propertyName, valueOrCallback) {
361
+ this.dispatch('request-context', { [propertyName]: valueOrCallback }, true);
362
+ }
363
+
364
+ /**
365
+ * Internal Context Request Handler
366
+ * This will check if the current/receiving instance actually provides a value under the requested name.
367
+ * If there is something to provide it will either assign the value to the requesting element or call the callback.
368
+ *
369
+ * @param event
370
+ */
371
+ onRequestContext(event) {
372
+ const properties = this.properties();
373
+ const provideProperties = this.provideProperties();
374
+
375
+ Object.entries(event.detail ?? {}).forEach(([key, valueOrCallback]) => {
376
+ if (key in provideProperties) {
377
+ // found it, provide it
378
+ const providedValue = provideProperties[key];
379
+ event.stopPropagation();
380
+ if (typeof valueOrCallback === 'function') {
381
+ // call function with context value
382
+ valueOrCallback(providedValue);
383
+ } else {
384
+ // assign to prop
385
+ event.target[key] = providedValue;
386
+ }
387
+ }
388
+ });
389
+ }
390
+
297
391
  // Deprecated
298
392
  hooks() {
299
393
  return {};
@@ -1,6 +1,6 @@
1
- import { BaseElement } from './BaseElement';
2
- import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper';
3
- export { i18n } from './util/i18n';
1
+ import { BaseElement } from './BaseElement.js';
2
+ import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper.js';
3
+ export { i18n } from './util/i18n.js';
4
4
 
5
5
  class StyledElement extends BaseElement {
6
6
  static globalStyles = null;
@@ -1,7 +1,7 @@
1
- import { StyledElement } from './StyledElement';
1
+ import { StyledElement } from './StyledElement.js';
2
2
  import { html } from 'lit-html';
3
3
  import { render } from 'lit-html/lib/shady-render.js';
4
- export { i18n } from './util/i18n';
4
+ export { i18n } from './util/i18n.js';
5
5
 
6
6
  class TemplateElement extends StyledElement {
7
7
  constructor(options) {
@@ -1,11 +1,9 @@
1
- import { StyledElement } from '../../StyledElement';
2
- import { html, spreadAttributes, unsafeHTML } from './util/html';
3
- import { render } from './util/render';
4
- export { i18n } from '../../util/i18n';
1
+ import { StyledElement } from '../../StyledElement.js';
2
+ import { html, spreadAttributes, unsafeHTML } from './util/html.js';
3
+ import { render } from './util/render.js';
4
+ export { i18n } from '../../util/i18n.js';
5
5
 
6
6
  class TemplateElement extends StyledElement {
7
- static _$templateElement$ = true;
8
-
9
7
  constructor(options) {
10
8
  super({
11
9
  deferUpdate: false,
@@ -1,9 +1,9 @@
1
- import { BaseElement } from '../../BaseElement';
2
- import { StyledElement } from '../../StyledElement';
3
- import { TemplateElement, html, unsafeHTML, spreadAttributes } from './TemplateElement';
4
- import { toString } from '../../util/toString';
5
- import { defineElement } from '../../util/defineElement';
6
- import { i18n } from '../../util/i18n';
1
+ import { BaseElement } from '../../BaseElement.js';
2
+ import { StyledElement } from '../../StyledElement.js';
3
+ import { TemplateElement, html, unsafeHTML, spreadAttributes } from './TemplateElement.js';
4
+ import { toString } from '../../util/toString.js';
5
+ import { defineElement } from '../../util/defineElement.js';
6
+ import { i18n } from '../../util/i18n.js';
7
7
 
8
8
  export {
9
9
  BaseElement,
@@ -1,4 +1,4 @@
1
- import { encodeAttribute } from '../../../util/AttributeParser';
1
+ import { encodeAttribute } from '../../../util/AttributeParser.js';
2
2
  import { unsafeHTML, spreadAttributes } from './directives.js';
3
3
 
4
4
  class Part {
@@ -1,4 +1,4 @@
1
- const _cachedTemplateElements = {};
1
+ import { TemplateElement } from '../TemplateElement.js';
2
2
 
3
3
  /**
4
4
  * Parses the given template string and returns a real DOM element.
@@ -45,22 +45,10 @@ const diffAttributes = function (templateElement, domElement) {
45
45
  */
46
46
  const isTemplateElement = (element) => {
47
47
  const tagName = element.tagName?.toLowerCase() ?? false;
48
- if (!tagName || !tagName.includes('-')) {
48
+ if (!tagName || !tagName.includes('-') || element.shadowRoot) {
49
49
  return false;
50
50
  }
51
-
52
- if (_cachedTemplateElements[tagName]) {
53
- return true;
54
- }
55
-
56
- const elementClass = customElements.get(tagName);
57
- const isTemplateElement = elementClass && elementClass._$templateElement$ === true;
58
-
59
- if (isTemplateElement) {
60
- _cachedTemplateElements[tagName] = elementClass;
61
- }
62
-
63
- return isTemplateElement;
51
+ return element instanceof TemplateElement;
64
52
  };
65
53
 
66
54
  /**
@@ -138,7 +126,10 @@ const diffNodes = function (templateNode, domNode) {
138
126
 
139
127
  // If there are child nodes, diff them recursively
140
128
  if (templateChildNode.hasChildNodes()) {
141
- if (!isTemplateElement(templateChildNode)) {
129
+ // https://stackoverflow.com/questions/46898739/why-domparser-doesnt-use-the-customelements-registered
130
+ // we have to use the domChildNode here because the templateChildNode is not upgraded because it was not
131
+ // imported into a document yet and therefore instanceOf will not work...
132
+ if (!isTemplateElement(domChildNode)) {
142
133
  diffNodes(templateChildNode, domChildNode);
143
134
  }
144
135
  }
@@ -33,6 +33,18 @@ export function isNaN(value) {
33
33
  return Number.isNaN(value);
34
34
  }
35
35
 
36
+ /**
37
+ * deep compare two values primitive or complex
38
+ *
39
+ * @param valueA
40
+ * @param valueB
41
+ * @returns {boolean}
42
+ */
43
+
44
+ export function deepEquals(valueA, valueB) {
45
+ return JSON.stringify(valueA) === JSON.stringify(valueB);
46
+ }
47
+
36
48
  export function parseAttribute(value, options = {}) {
37
49
  if (options.parse === false || !isString(value)) {
38
50
  // no-op
package/src/util/Store.js CHANGED
@@ -1,4 +1,5 @@
1
- import { isObjectLike } from './AttributeParser';
1
+ import { isObjectLike, deepEquals } from './AttributeParser.js';
2
+ import { BaseElement } from '../BaseElement';
2
3
 
3
4
  export class Store {
4
5
  _observer = new Set();
@@ -17,13 +18,17 @@ export class Store {
17
18
  return this._state[key];
18
19
  },
19
20
  set: (newValue) => {
21
+ const oldState = structuredClone(this._state);
20
22
  const oldValue = this._state[key];
21
23
  this._state[key] = newValue;
22
- const watch = this.watch();
23
- if (watch.hasOwnProperty(key) && typeof watch[key] === 'function') {
24
- watch[key](newValue, oldValue);
24
+
25
+ if (!deepEquals(newValue, oldValue)) {
26
+ const watch = this.watch();
27
+ if (watch.hasOwnProperty(key) && typeof watch[key] === 'function') {
28
+ watch[key](newValue, oldValue);
29
+ }
30
+ this.requestUpdate(oldState);
25
31
  }
26
- this.requestUpdate();
27
32
  },
28
33
  });
29
34
  });
@@ -64,9 +69,27 @@ export class Store {
64
69
  this._observer.delete(observer);
65
70
  }
66
71
 
67
- requestUpdate() {
68
- this._observer.forEach((observer) =>
69
- typeof observer.requestUpdate === 'function' ? observer.requestUpdate() : observer(),
70
- );
72
+ requestUpdate(oldState) {
73
+ this._observer.forEach(async (observer) => {
74
+ if (observer instanceof BaseElement) {
75
+ await observer.requestUpdate();
76
+ // check if store is watched by an observer
77
+ if (Object.keys(observer.watch() ?? {}).length > 0) {
78
+ // observer actually has watched properties
79
+ const properties = observer.properties();
80
+ Object.keys(observer.watch() ?? {}).forEach((key) => {
81
+ if (properties[key] === this) {
82
+ // observer is actually watching store changes provide new and old values
83
+ observer.callPropertyWatcher(key, this._state, oldState);
84
+ observer.notifyPropertyChange(key, this._state);
85
+ }
86
+ });
87
+ }
88
+ } else if (typeof observer === 'function') {
89
+ observer();
90
+ } else {
91
+ console.error('Store: observer type is not supported.', observer);
92
+ }
93
+ });
71
94
  }
72
95
  }