@webtides/element-js 1.0.0-alpha.4 → 1.0.0-alpha.5

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.
@@ -0,0 +1,243 @@
1
+ # Upgrade Guide
2
+
3
+ Upgrading from `element-js` v0.x to v1.0
4
+
5
+ ---
6
+
7
+ `element-js` v1.0 is the first major version since it was released in June 2020. The biggest change is a new and custom DOM Parts renderer that allows us to get rid of `lit-html` making it a dependency free library finally.
8
+
9
+ We only have a couple of breaking changes and a few removed deprecations. The deprecations have been logged to the console for a while now and we hope that most of them should not be there anymore. And the breaking changes should be mostly changed import paths. So it should only be a matter of copy and paste.
10
+
11
+ ---
12
+
13
+ ## Install element-js v1.0
14
+
15
+ Update `element-js` using npm:
16
+
17
+ ```
18
+ npm install element-js@latest
19
+ ```
20
+
21
+ ## Support for IE 11 has been dropped
22
+
23
+ Prior to v1.0, we tried our best to make sure features we included in `element-js` worked in IE 11 whenever possible. If you need to support IE 11, we recommend continuing to use `element-js` v0.x until you no longer need to support IE.
24
+
25
+ ## Added `afterUpdate` lifecycle hook upon connecting the elements
26
+
27
+ `element-js` will now also trigger the `afterUpdate` hook right after the `connected` hook. This is to reduce the cases where you had to do the same things in `connected` and `afterUpdate`. You can now remove those duplicate calls from the `connected` hook.
28
+
29
+ ## Change import paths for main exports
30
+
31
+ > This only applies if you used the vanilla renderer. For the default `lit-html` renderer you do not need to change the import paths.
32
+
33
+ Change everything that was imported from:
34
+
35
+ ```javascript
36
+ import { TemplateElement, html } from '@webtides/element-js/src/renderer/vanilla';
37
+ ```
38
+
39
+ to:
40
+
41
+ ```javascript
42
+ import { TemplateElement, html } from '@webtides/element-js';
43
+ ```
44
+
45
+ This must be done for the following classes and functions:
46
+
47
+ - BaseElement
48
+ - StyledElement
49
+ - TemplateElement
50
+ - Store
51
+ - html
52
+ - toString
53
+ - defineElement
54
+ - i18n
55
+
56
+ ## Change import paths for directives
57
+
58
+ If you used any directives you must change their import paths as well.
59
+
60
+ For the vanilla renderer we previously had them here:
61
+
62
+ ```javascript
63
+ import { unsafeHTML } from '@webtides/element-js/src/renderer/vanilla/util/directives.js';
64
+ ```
65
+
66
+ And for the `lit-html` renderer you had to import the directives from `lit`:
67
+
68
+ ```javascript
69
+ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
70
+ ```
71
+
72
+ In both cases you must now change the imports to our new directives:
73
+
74
+ ```javascript
75
+ import { unsafeHTML } from '@webtides/element-js/src/dom-parts/directives';
76
+ ```
77
+
78
+ ## Removed childListUpdate option from constructor options
79
+
80
+ We removed the deprecated `BaseElement` constructor option `childListUpdate`. Please use the "mutationObserverOptions" dictionary instead.
81
+
82
+ Change:
83
+
84
+ ```javascript
85
+ class MyElement extends BaseElement {
86
+ constructor() {
87
+ super({ childListUpdate: true });
88
+ }
89
+ }
90
+ ```
91
+
92
+ to:
93
+
94
+ ```javascript
95
+ class MyElement extends BaseElement {
96
+ constructor() {
97
+ super({ mutationObserverOptions: { childList: true } });
98
+ }
99
+ }
100
+ ```
101
+
102
+ For more information on how to configure `mutationObserverOptions` see the docs.
103
+
104
+ ## Removed the `hooks` map
105
+
106
+ We removed the `hooks` map in the `BaseElement`. Using the hooks() map for lifecycle hooks is not possible anymore! Please overwrite the existing lifecycle hook functions. See the docs for more info.
107
+
108
+ Change:
109
+
110
+ ```javascript
111
+ class MyElement extends BaseElement {
112
+ hooks() {
113
+ return {
114
+ connected: () => {},
115
+ beforeUpdate: () => {},
116
+ afterUpdate: () => {}
117
+ };
118
+ }
119
+ }
120
+ ```
121
+
122
+ to:
123
+
124
+ ```javascript
125
+ class MyElement extends BaseElement {
126
+ connected() {}
127
+ beforeUpdate() {}
128
+ afterUpdate() {}
129
+ }
130
+ ```
131
+
132
+ ## Removed the `computed` map
133
+
134
+ We removed the `computed` map in the `BaseElement`. Using the computed() map for computed properties is is not possible anymore! Please use regular JS getters and return the computed value. See the docs for more info.
135
+
136
+ Change:
137
+
138
+ ```javascript
139
+ class MyElement extends BaseElement {
140
+ firstName = 'John';
141
+ lastName = 'Doe';
142
+
143
+ computed() {
144
+ return {
145
+ name: () => `${this.firstName} ${this.lastName}`
146
+ };
147
+ }
148
+ }
149
+ ```
150
+
151
+ to:
152
+
153
+ ```javascript
154
+ class MyElement extends BaseElement {
155
+ firstName = 'John';
156
+ lastName = 'Doe';
157
+
158
+ get name() {
159
+ return `${this.firstName} ${this.lastName}`;
160
+ }
161
+ }
162
+ ```
163
+
164
+ ## Removed the `i18n` helper function
165
+
166
+ We removed the `i18n` helper function. It was a very simple and naive implementation for translating strings in templates. We would recommend to use a more sophisticated i18n solution.
167
+
168
+ If you still need the same functionality, you can simply add the following function to your code and change the imports accordingly:
169
+
170
+ ```javascript
171
+ /**
172
+ * Retrieves a translated key from a dictionary on the window object
173
+ * Example: ${i18n('CustomElement.buttonLabel', 'Label')}
174
+ * @param {string} key - to be translated
175
+ * @param {string} fallback - to be used if key is not defined
176
+ * @return {string} - String of the translated key or fallback or original key
177
+ */
178
+ export function i18n(key, fallback) {
179
+ try {
180
+ const translations = window.elementjs.i18n();
181
+ if (translations[key] === undefined) {
182
+ throw 'Translation Missing';
183
+ }
184
+
185
+ return translations[key];
186
+ } catch (err) {
187
+ if (fallback) return fallback;
188
+ else return key;
189
+ }
190
+ }
191
+ ```
192
+
193
+ ## Changed default options for the `dispatch` helper function
194
+
195
+ We changed the default options when using the `dispatch` helper function. `bubbles`, `cancelable` and `composed` will now be `true` by default.
196
+
197
+ Also, we changed the parameter signature. Instead of expecting the tree boolean values as parameters after the data value, we now expect an `options` object that is in line with the `CustomEventInit` options for the `CustomEvent` class. This is not a breaking change, and we will still evaluate the old parameter list for now. It will finally be removed in v2.0.
198
+
199
+ We would recommend that you start using the new syntax. Change this:
200
+
201
+ ```javascript
202
+ this.dispatch('custom-event', 'foo', true, true, true);
203
+ ```
204
+
205
+ to (if you wanted every parameter to be `true` before):
206
+
207
+ ```javascript
208
+ this.dispatch('custom-event', 'foo');
209
+ ```
210
+
211
+ or this (if you had different option values):
212
+
213
+ ```javascript
214
+ this.dispatch('custom-event', 'foo', {
215
+ bubbles: true,
216
+ cancelable: false,
217
+ composed: true
218
+ });
219
+ ```
220
+
221
+ ## Changed adoptGlobalStyles
222
+
223
+ We changed adopting global styles to include ALL global styles now. Previously only one inline <style> with the ID of '#globalStyles' would be adopted. For more information see the docs. We think that this is the behaviour that one would actually want.
224
+
225
+ If you still need it to be the way it was before, please change the `adoptGlobalStyles` option value from:
226
+
227
+ ```javascript
228
+ export class ShadowElement extends TemplateElement {
229
+ constructor() {
230
+ super({ adoptGlobalStyles: true });
231
+ }
232
+ }
233
+ ```
234
+
235
+ to:
236
+
237
+ ```javascript
238
+ export class ShadowElement extends TemplateElement {
239
+ constructor() {
240
+ super({ adoptGlobalStyles: '#globalStyles' });
241
+ }
242
+ }
243
+ ```
@@ -0,0 +1,112 @@
1
+
2
+ ## Guides/Tooling
3
+
4
+ ### Frontend Stack
5
+
6
+ ### Bundling/Publishing
7
+
8
+ ### Design System
9
+
10
+ ### Style Guide / Best Practices
11
+
12
+ This is an element style guide created and enforced for the purpose of standardizing elements and file structures.
13
+
14
+ #### File structure
15
+
16
+ - One element per file.
17
+ - One element per directory. Though it may make sense to group similar elements into the same directory, we've found it'
18
+ s easier to consume and document elements when each one has its own directory.
19
+ - Implementation (.js) and styles (.css) of an element should live in the same directory.
20
+
21
+ Example:
22
+
23
+ ```
24
+ ├── card-element
25
+ │ ├── card-element.css
26
+ │ ├── card-element.js
27
+ │ └── test
28
+ │ ├── card-element.e2e.js
29
+ │ └── card-element.spec.js
30
+ ├── card-content
31
+ │ ├── card-content.css
32
+ │ └── card-content.js
33
+ ├── card-title
34
+ │ ├── card-title.css
35
+ │ └── card-title.js
36
+ ```
37
+
38
+ #### Naming
39
+
40
+ ##### Name
41
+
42
+ Elements are not actions, they are conceptually "things". It is better to use nouns, instead of verbs, such us: "
43
+ animation" instead of "animating". "input", "tab", "nav", "menu" are some examples.
44
+
45
+ ##### -element postfix
46
+
47
+ The naming has a major role when you are creating a collection of elements intended to be used across different
48
+ projects. Web Components are not scoped because they are globally declared within the page, which means a "unique" name
49
+ is needed to prevent collisions.
50
+ Additionally, web components are required to contain a "-" dash within the tag name. When using the first section to
51
+ namespace your components - everything will look the same, and it will be hard to distinguish elements.
52
+
53
+ DO NOT do this:
54
+
55
+ ```
56
+ company-card
57
+ company-card-header
58
+ company-card-title
59
+ company-card-content
60
+ company-card-footer
61
+ ```
62
+
63
+ Instead, use -element for elements with a single noun.
64
+
65
+ ```
66
+ card-element
67
+ card-header
68
+ card-title
69
+ card-content
70
+ card-footer
71
+ ```
72
+
73
+ ##### Modifiers
74
+
75
+ When several elements are related and/or coupled, it is a good idea to share the name, and then add different modifiers,
76
+ for example:
77
+
78
+ ```
79
+ menu-element
80
+ menu-controller
81
+ ```
82
+
83
+ ```
84
+ card-element
85
+ card-header
86
+ card-content
87
+ ```
88
+
89
+ ##### Element (JS class)
90
+
91
+ The name of the ES6 class of the element should reflect the file name, and the html tag name.
92
+
93
+ ```js
94
+ export class ButtonElement extends BaseElement {
95
+ }
96
+
97
+ customElements.define('button-element', ButtonElement);
98
+
99
+ export class MenuElement extends BaseElement {
100
+ }
101
+
102
+ customElements.define('menu-element', MenuElement);
103
+ ```
104
+
105
+ #### Code organization
106
+
107
+ ##### Newspaper Metaphor from The Robert C. Martin's _Clean Code_
108
+
109
+ > The source file should be organized like a newspaper article, with the highest level summary at the top, and more and
110
+ > more details further down. Functions called from the top function come directly below it, and so on down to the lowest
111
+ > level, and most detailed functions at the bottom. This is a good way to organize the source code, even though IDE:s make
112
+ > the location of functions less important, since it is so easy to navigate in and out of them.
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.5",
4
4
  "module": "index.js",
5
- "main": "dist/index.js",
5
+ "main": "index.js",
6
6
  "type": "module",
7
7
  "files": [
8
- "/dist",
8
+ "/docs",
9
9
  "/src",
10
10
  "index.js"
11
11
  ],
12
12
  "exports": {
13
13
  ".": {
14
- "browser": "./dist/index.js",
14
+ "browser": "./index.js",
15
15
  "import": "./index.js",
16
16
  "default": "./index.js"
17
17
  },
@@ -35,21 +35,18 @@
35
35
  ],
36
36
  "dependencies": {},
37
37
  "devDependencies": {
38
- "@babel/eslint-parser": "^7.18.9",
39
- "@open-wc/testing": "^3.1.6",
40
- "@web/dev-server": "^0.1.30",
41
- "@web/test-runner": "^0.14.0",
38
+ "@babel/eslint-parser": "^7.23.3",
39
+ "@open-wc/testing": "^4.0.0",
40
+ "@web/dev-server": "^0.4.1",
41
+ "@web/test-runner": "^0.14.1",
42
42
  "babel-eslint": "^10.1.0",
43
- "babel-plugin-module-resolver": "^5.0.0",
44
- "deepmerge": "^4.2.2",
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"
43
+ "eslint": "8.55.0",
44
+ "eslint-config-prettier": "^9.1.0",
45
+ "eslint-plugin-import": "^2.29.0",
46
+ "eslint-plugin-prettier": "^5.0.1",
47
+ "husky": "^8.0.3",
48
+ "lint-staged": "^15.1.0",
49
+ "prettier": "^3.1.0"
53
50
  },
54
51
  "lint-staged": {
55
52
  "*.{js,json}": [
@@ -59,7 +56,6 @@
59
56
  },
60
57
  "scripts": {
61
58
  "lint": "eslint index.js 'src/**/*.js'",
62
- "prepare": "esbuild index.js --outfile=dist/index.js --bundle --format=esm --sourcemap --target=chrome79,firefox68,safari12,edge79",
63
59
  "test": "web-test-runner test/**/*.test.{html,js} --node-resolve",
64
60
  "test:coverage": "web-test-runner test/**/*.test.{html,js} --node-resolve --coverage",
65
61
  "test:watch": "web-test-runner test/**/*.test.{html,js} --node-resolve --watch",
@@ -380,7 +380,7 @@ class BaseElement extends HTMLElement {
380
380
  property in this._options['propertyOptions'] &&
381
381
  this._options['propertyOptions'][property]['notify'] === true
382
382
  ) {
383
- this.dispatch(`${camelToDash(property)}-changed`, newValue, true);
383
+ this.dispatch(`${camelToDash(property)}-changed`, newValue);
384
384
  }
385
385
  }
386
386
 
@@ -453,7 +453,7 @@ class BaseElement extends HTMLElement {
453
453
  * @param {any | function} valueOrCallback
454
454
  */
455
455
  requestContext(propertyName, valueOrCallback) {
456
- this.dispatch('request-context', { [propertyName]: valueOrCallback }, true, true, true);
456
+ this.dispatch('request-context', { [propertyName]: valueOrCallback });
457
457
  }
458
458
 
459
459
  /**
@@ -619,10 +619,10 @@ class BaseElement extends HTMLElement {
619
619
  /**
620
620
  * Helper function for dispatching custom events
621
621
  * @param {string} name
622
- * @param {Object} data
623
- * @param {CustomEventInit} options
624
- * @param {boolean} cancelable - DEPRECATED
625
- * @param {boolean} composed - DEPRECATED
622
+ * @param {Object} [data]
623
+ * @param {CustomEventInit} [options]
624
+ * @param {boolean} [cancelable] - DEPRECATED
625
+ * @param {boolean} [composed] - DEPRECATED
626
626
  */
627
627
  dispatch(name, data, options, cancelable = false, composed = false) {
628
628
  let eventInit = {};
@@ -120,6 +120,7 @@ export class AttributePart extends Part {
120
120
  */
121
121
  constructor(node, name, initialValue) {
122
122
  super();
123
+ node.__part = this; // add Part to comment node for debugging in the browser
123
124
  this.name = name;
124
125
  this.initialValue = initialValue;
125
126
  this.processor = processAttributePart(node.nextElementSibling, this.name);
@@ -212,6 +212,8 @@ export class ChildNodePart extends Part {
212
212
 
213
213
  let serverSideRendered = false;
214
214
  if (startNode) {
215
+ startNode.__part = this; // add Part to comment node for debugging in the browser
216
+
215
217
  const placeholder = startNode.data;
216
218
  const childNodes = [startNode];
217
219
  let childNode = startNode.nextSibling;
@@ -22,6 +22,7 @@ export class NodePart extends Part {
22
22
  );
23
23
  }
24
24
  super();
25
+ node.__part = this; // add Part to comment node for debugging in the browser
25
26
  this.node = node.nextElementSibling;
26
27
  const { directiveClass, values } = initialValue;
27
28
  this.directive = new directiveClass(node.nextElementSibling);
@@ -99,6 +99,8 @@ const getValue = (value) => {
99
99
  return value.map(getValue).join('');
100
100
  case value instanceof TemplateResult:
101
101
  return value.toString();
102
+ case value.__unsafeHTML:
103
+ return value.string;
102
104
  }
103
105
  break;
104
106
  case 'function':
@@ -54,12 +54,14 @@ const choose = (value, cases, defaultCase) => {
54
54
  };
55
55
 
56
56
  /**
57
- * Renders a given string as HTML instead of text
57
+ * Renders a given string as HTML instead of text or as unescaped string in SSR mode
58
58
  * @param {string} string
59
- * @returns {function(): Node | string}
59
+ * @returns {(function(): Node) | { __unsafeHTML: true, string: string }}
60
60
  */
61
61
  const unsafeHTML = (string) => {
62
- if (!globalThis.DOMParser) return () => string;
62
+ if (!globalThis.DOMParser) {
63
+ return { __unsafeHTML: true, string };
64
+ }
63
65
  const node = convertStringToHTML(string).firstChild;
64
66
  const importedNode = globalThis.document?.importNode(node, true);
65
67
  return () => importedNode;