@webtides/element-js 0.7.3 → 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/index.js CHANGED
@@ -5,5 +5,6 @@ import { toString } from './src/util/toString.js';
5
5
  import { defineElement } from './src/util/defineElement.js';
6
6
  import { i18n } from './src/util/i18n.js';
7
7
  import { Store } from './src/util/Store.js';
8
+ import { Directive } from './src/util/Directive.js';
8
9
 
9
- export { BaseElement, StyledElement, TemplateElement, Store, html, toString, defineElement, i18n };
10
+ export { BaseElement, StyledElement, TemplateElement, Store, Directive, html, toString, defineElement, i18n };
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "0.7.3",
3
+ "version": "1.0.0-alpha.2",
4
4
  "module": "index.js",
5
5
  "main": "dist/index.js",
6
+ "type": "module",
6
7
  "files": [
7
8
  "/dist",
8
9
  "/src",
@@ -16,12 +17,7 @@
16
17
  },
17
18
  "./src/*": "./src/*.js",
18
19
  "./src/util/*": "./src/util/*.js",
19
- "./src/renderer/vanilla": "./src/renderer/vanilla/index.js",
20
- "./src/renderer/vanilla/TemplateElement": "./src/renderer/vanilla/TemplateElement.js",
21
- "./src/renderer/vanilla/util/*": "./src/renderer/vanilla/util/*.js",
22
- "./src/renderer/vanilla/util/directives.js": "./src/renderer/vanilla/util/directives.js",
23
- "./src/renderer/vanilla/util/html.js": "./src/renderer/vanilla/util/html.js",
24
- "./src/renderer/vanilla/util/render.js": "./src/renderer/vanilla/util/render.js"
20
+ "./src/dom-parts/*": "./src/dom-parts/*.js"
25
21
  },
26
22
  "repository": "https://github.com/webtides/element-js.git",
27
23
  "author": "@webtides",
@@ -37,23 +33,23 @@
37
33
  "element-js",
38
34
  "shadow-dom"
39
35
  ],
40
- "dependencies": {
41
- "lit-html": "^1.2.1"
42
- },
36
+ "dependencies": {},
43
37
  "devDependencies": {
44
- "@open-wc/testing": "^3.0.3",
38
+ "@babel/eslint-parser": "^7.18.9",
39
+ "@open-wc/testing": "^3.1.6",
45
40
  "@web/dev-server": "^0.1.30",
46
- "@web/test-runner": "^0.13.26",
41
+ "@web/test-runner": "^0.14.0",
47
42
  "babel-eslint": "^10.1.0",
48
43
  "babel-plugin-module-resolver": "^5.0.0",
49
44
  "deepmerge": "^4.2.2",
50
- "esbuild": "^0.8.57",
51
- "eslint": "^7.0.0",
52
- "eslint-config-prettier": "^6.11.0",
53
- "eslint-plugin-prettier": "^3.1.3",
54
- "husky": "^7.0.4",
55
- "lint-staged": "^12.1.2",
56
- "prettier": "^2.5.1"
45
+ "esbuild": "^0.15.5",
46
+ "eslint": "8.22.0",
47
+ "eslint-config-prettier": "^7.2.0",
48
+ "eslint-plugin-import": "^2.26.0",
49
+ "eslint-plugin-prettier": "^4.2.1",
50
+ "husky": "^8.0.1",
51
+ "lint-staged": "^13.0.3",
52
+ "prettier": "^2.7.1"
57
53
  },
58
54
  "lint-staged": {
59
55
  "*.{js,json}": [
@@ -62,7 +58,7 @@
62
58
  ]
63
59
  },
64
60
  "scripts": {
65
- "lint": "eslint src/**/*.js",
61
+ "lint": "eslint index.js 'src/**/*.js'",
66
62
  "prepare": "esbuild index.js --outfile=dist/index.js --bundle --format=esm --sourcemap --target=chrome79,firefox68,safari12,edge79",
67
63
  "test": "web-test-runner test/**/*.test.{html,js} --node-resolve",
68
64
  "test:coverage": "web-test-runner test/**/*.test.{html,js} --node-resolve --coverage",
@@ -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 = {};
@@ -26,15 +63,13 @@ class BaseElement extends HTMLElement {
26
63
  propertyOptions: {},
27
64
  ...options,
28
65
  };
29
-
30
- if (options.childListUpdate !== undefined && options.childListUpdate !== null) {
31
- this._options.mutationObserverOptions.childList = options.childListUpdate;
32
- console.warn(
33
- `[${this.localName}] Using the "childListUpdate" option is deprecated and will be removed before 1.0! Please use the "mutationObserverOptions" dictionary instead. See the docs for more info`,
34
- );
35
- }
36
66
  }
37
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
+ */
38
73
  connectedCallback() {
39
74
  // define all attributes to "this" as properties
40
75
  this.defineAttributesAsProperties();
@@ -42,9 +77,6 @@ class BaseElement extends HTMLElement {
42
77
  // define all properties to "this"
43
78
  this.defineProperties();
44
79
 
45
- // define all computed properties to "this"
46
- this.defineComputedProperties();
47
-
48
80
  // define context
49
81
  this.definePropertyInjection();
50
82
 
@@ -52,17 +84,21 @@ class BaseElement extends HTMLElement {
52
84
  this.defineObserver();
53
85
 
54
86
  if (this.hasAttribute('defer-update') || this._options.deferUpdate) {
55
- // don't updates/render, but register refs and events
87
+ // don't update/render, but register refs and events
56
88
  this.registerEventsAndRefs();
57
89
 
58
90
  this.triggerHook('connected');
59
91
  } else {
60
92
  this.requestUpdate({ notify: false }).then(() => {
61
93
  this.triggerHook('connected');
94
+ this.triggerHook('afterUpdate');
62
95
  });
63
96
  }
64
97
  }
65
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
+ */
66
102
  defineObserver() {
67
103
  // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
68
104
  this._mutationObserver = new MutationObserver((mutations) => {
@@ -95,6 +131,9 @@ class BaseElement extends HTMLElement {
95
131
  });
96
132
  }
97
133
 
134
+ /**
135
+ * Native `disconnectedCallback` of the HTMLElement
136
+ */
98
137
  disconnectedCallback() {
99
138
  // remove events for elements in the component
100
139
  this.removeEvents();
@@ -116,6 +155,11 @@ class BaseElement extends HTMLElement {
116
155
  this.triggerHook('disconnected');
117
156
  }
118
157
 
158
+ /**
159
+ * Request and batch an asynchronous update cycle
160
+ * @param {PropertyUpdateOptions} options
161
+ * @returns {Promise<unknown>}
162
+ */
119
163
  requestUpdate(options = { notify: true }) {
120
164
  if (options.notify === true) {
121
165
  this.triggerHook('beforeUpdate');
@@ -145,6 +189,7 @@ class BaseElement extends HTMLElement {
145
189
  * This should be called by the template component AFTER adding the template to
146
190
  * the DOM.
147
191
  * Here we will register the events and the refs for the element.
192
+ * @param {PropertyUpdateOptions} options
148
193
  */
149
194
  update(options = { notify: true }) {
150
195
  this.registerEventsAndRefs();
@@ -211,6 +256,9 @@ class BaseElement extends HTMLElement {
211
256
  * Define a property on the element
212
257
  * Will trigger watch() function when a property was changed
213
258
  * Will trigger update() when a property was changed
259
+ * @param {string} property
260
+ * @param {any} value
261
+ * @param {boolean} reflectAttribute
214
262
  */
215
263
  defineProperty(property, value, reflectAttribute = false) {
216
264
  if (value instanceof Store) {
@@ -274,12 +322,10 @@ class BaseElement extends HTMLElement {
274
322
  }
275
323
 
276
324
  /**
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
325
+ * Call the watch callbacks on property changes
326
+ * @param {string} property property that changes
327
+ * @param {any} newValue
328
+ * @param {any} oldValue
283
329
  */
284
330
  callPropertyWatcher(property, newValue, oldValue) {
285
331
  // notify watched properties (after update())
@@ -288,12 +334,11 @@ class BaseElement extends HTMLElement {
288
334
  watch[property](newValue, oldValue);
289
335
  }
290
336
  }
337
+
291
338
  /**
292
- * notify property observer via change event
293
- *
294
- * @param context is the elements instance
295
- * @param property property that changes
296
- * @param newValue
339
+ * Notify property observer via change event
340
+ * @param {string} property property that changes
341
+ * @param {any} newValue
297
342
  */
298
343
  notifyPropertyChange(property, newValue) {
299
344
  // dispatch change event
@@ -305,6 +350,10 @@ class BaseElement extends HTMLElement {
305
350
  }
306
351
  }
307
352
 
353
+ /**
354
+ * Reflects a property as attribute on the element
355
+ * @param {PropertyUpdateOptions} options
356
+ */
308
357
  reflectProperty(options) {
309
358
  const { property, newValue } = options;
310
359
  const newValueString = options.newValueString || JSON.stringify(newValue);
@@ -353,9 +402,8 @@ class BaseElement extends HTMLElement {
353
402
 
354
403
  /**
355
404
  * Helper Function to initialize the context requests
356
- *
357
- * @param propertyName
358
- * @param valueOrCallback
405
+ * @param {string} propertyName
406
+ * @param {any | function} valueOrCallback
359
407
  */
360
408
  requestContext(propertyName, valueOrCallback) {
361
409
  this.dispatch('request-context', { [propertyName]: valueOrCallback }, true);
@@ -365,8 +413,7 @@ class BaseElement extends HTMLElement {
365
413
  * Internal Context Request Handler
366
414
  * This will check if the current/receiving instance actually provides a value under the requested name.
367
415
  * If there is something to provide it will either assign the value to the requesting element or call the callback.
368
- *
369
- * @param event
416
+ * @param {CustomEvent} event
370
417
  */
371
418
  onRequestContext(event) {
372
419
  const properties = this.properties();
@@ -388,33 +435,31 @@ class BaseElement extends HTMLElement {
388
435
  });
389
436
  }
390
437
 
391
- // Deprecated
392
- hooks() {
393
- return {};
394
- }
395
-
396
- // Connected lifecycle hook
438
+ /**
439
+ * Connected lifecycle hook
440
+ */
397
441
  connected() {}
398
442
 
399
- // BeforeUpdate lifecycle hook
443
+ /**
444
+ * BeforeUpdate lifecycle hook
445
+ */
400
446
  beforeUpdate() {}
401
447
 
402
- // AfterUpdate lifecycle hook
448
+ /**
449
+ * AfterUpdate lifecycle hook
450
+ */
403
451
  afterUpdate() {}
404
452
 
405
- // Disconnected lifecycle hook
453
+ /**
454
+ * Disconnected lifecycle hook
455
+ */
406
456
  disconnected() {}
407
457
 
408
- // Triggers a lifecycle hook based on the name
458
+ /**
459
+ * Triggers a lifecycle hook based on the name
460
+ * @param {string} name
461
+ */
409
462
  triggerHook(name) {
410
- if (this.hooks && name in this.hooks()) {
411
- console.warn(
412
- `[${this.localName}] Using the hooks() map for lifecycle hooks is deprecated! Please overwrite the existing lifecycle hook functions. See the docs for more info`,
413
- );
414
- this.hooks()[name]();
415
- return;
416
- }
417
-
418
463
  if (name in this) {
419
464
  this[name]();
420
465
  }
@@ -431,25 +476,6 @@ class BaseElement extends HTMLElement {
431
476
  return {};
432
477
  }
433
478
 
434
- // Deprecated
435
- computed() {
436
- return {};
437
- }
438
-
439
- // Deprecated
440
- defineComputedProperties() {
441
- Object.keys(this.computed()).forEach((prop) => {
442
- if (!this.hasOwnProperty(prop)) {
443
- console.warn(
444
- `[${this.localName}] Using the computed() map for computed properties is deprecated! Please use regular JS getters and return the computed value. See the docs for more info`,
445
- );
446
- Object.defineProperty(this, prop, {
447
- get: () => this.computed()[prop](),
448
- });
449
- }
450
- });
451
- }
452
-
453
479
  /**
454
480
  * Return a map of events that should be registered on the element.
455
481
  * eg. { '.link': {
@@ -542,7 +568,14 @@ class BaseElement extends HTMLElement {
542
568
  this.$refs = refsMap;
543
569
  }
544
570
 
545
- // Helper function for dispatching custom events
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
+ */
546
579
  dispatch(name, data, bubble = false, cancelable = false, composed = false) {
547
580
  const event = new CustomEvent(name, {
548
581
  bubbles: bubble,
@@ -553,7 +586,10 @@ class BaseElement extends HTMLElement {
553
586
  this.dispatchEvent(event);
554
587
  }
555
588
 
556
- // Get the root element
589
+ /**
590
+ * Get the root element
591
+ * @returns {HTMLElement}
592
+ */
557
593
  getRoot() {
558
594
  return this;
559
595
  }
@@ -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.getElementById('globalStyles');
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,32 +39,36 @@ 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
 
33
48
  if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
34
49
  // adopting does only make sense in shadow dom. Fall back to append for light elements
35
50
  this.adoptStyleSheets();
36
- } else if (this._options.shadowRender && window.ShadyCSS !== undefined) {
37
- // if shadowRoot is polyfilled we use ShadyCSS to copy scoped styles to <head>
38
- window.ShadyCSS.ScopingShim.prepareAdoptedCssText(this._styles, this.localName);
39
- }
40
-
41
- // if shadowRoot is polyfilled - scope element template
42
- if (window.ShadyCSS !== undefined) {
43
- window.ShadyCSS.styleElement(this);
44
51
  }
45
52
  }
46
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
+ */
47
59
  styles() {
48
60
  return [];
49
61
  }
50
62
 
63
+ /**
64
+ * Overrides the `update` method to adopt optional styles
65
+ * @param {PropertyUpdateOptions} options
66
+ */
51
67
  update(options) {
52
68
  if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
53
69
  // append stylesheets to template if not already adopted
54
70
  const appendableStyles = [...this._styles];
55
- if (this._options.shadowRender && this._options.adoptGlobalStyles && !window.ShadyCSS) {
71
+ if (this._options.shadowRender && this._options.adoptGlobalStyles) {
56
72
  appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
57
73
  }
58
74
  this.appendStyleSheets(appendableStyles);
@@ -60,6 +76,9 @@ class StyledElement extends BaseElement {
60
76
  super.update(options);
61
77
  }
62
78
 
79
+ /**
80
+ * Adopt stylesheets
81
+ */
63
82
  adoptStyleSheets() {
64
83
  if (!this.constructor['cssStyleSheets']) {
65
84
  this.constructor['cssStyleSheets'] = this._styles.map((style) => {
@@ -87,7 +106,10 @@ class StyledElement extends BaseElement {
87
106
  ];
88
107
  }
89
108
 
90
- // custom polyfill for constructable stylesheets by appending styles to the end of an element
109
+ /**
110
+ * Custom polyfill for constructable stylesheets by appending styles to the end of an element
111
+ * @param {string[]} styles
112
+ */
91
113
  appendStyleSheets(styles) {
92
114
  const parentDocument = getShadowParentOrBody(this.getRoot());
93
115
  styles.forEach((style, index) => {
@@ -95,7 +117,7 @@ class StyledElement extends BaseElement {
95
117
 
96
118
  // only append stylesheet if not already appended to shadowRoot or document
97
119
  if (!parentDocument.querySelector(`#${identifier}`)) {
98
- const styleElement = document.createElement('style');
120
+ const styleElement = globalThis.document?.createElement('style');
99
121
  styleElement.id = identifier;
100
122
  styleElement.style.display = 'none';
101
123
  styleElement.textContent = style;
@@ -1,9 +1,18 @@
1
1
  import { StyledElement } from './StyledElement.js';
2
- import { html } from 'lit-html';
3
- import { render } from 'lit-html/lib/shady-render.js';
2
+ import { html } from './dom-parts/html.js';
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,29 +29,40 @@ 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') {
35
55
  // just a plain string literal. no lit-html required
36
56
  this.getRoot().innerHTML = `${template}`;
37
57
  } else {
38
- // render via lit-html
39
- render(html` ${template} `, this.getRoot(), {
40
- scopeName: this.localName,
41
- eventContext: this,
42
- });
58
+ render(template, this.getRoot());
43
59
  }
44
60
  }
45
61
 
62
+ /**
63
+ * Get the root element - either the element or the shadow root
64
+ * @returns {ShadowRoot | HTMLElement}
65
+ */
46
66
  getRoot() {
47
67
  return this.shadowRoot !== null ? this.shadowRoot : this;
48
68
  }