@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,169 @@
1
+ ### Component Styles / CSS
2
+
3
+ Every _element-js_ element has the ability to load one or more style sheets. Either by adding them to the `styles`option in the `propertyOptions` via the constructor or by implementing a `styles()` method and returning a list of `string` values.
4
+
5
+ The value of these "style sheets" must always be a `string`.
6
+
7
+ ```javascript
8
+ import { TemplateElement, defineElement, html } from '@webtides/element-js';
9
+
10
+ const style = 'my-element .element { background: blue; }';
11
+
12
+ export class StyledElement extends TemplateElement {
13
+ constructor() {
14
+ super({ styles: [style] });
15
+ }
16
+
17
+ styles() {
18
+ return ['my-element .element { color: red; }'];
19
+ }
20
+
21
+ template() {
22
+ return html` <span class="element">I'm styled!</span> `;
23
+ }
24
+ }
25
+
26
+ defineElement('styled-element', StyledElement);
27
+ ```
28
+
29
+ For a better developer experience it is also possible to import real `CSS` files and load them from a separate file instead of cluttering the element with style text.
30
+
31
+ ```javascript
32
+ import { StyleElement, defineElement } from '@webtides/element-js';
33
+ import style from './styled-element.css';
34
+
35
+ export class StyledElement extends StyleElement {
36
+ constructor() {
37
+ super({ styles: [style] });
38
+ }
39
+ }
40
+
41
+ defineElement('styled-element', StyledElement);
42
+ ```
43
+
44
+ > Importing `CSS` files or rather `modules` is not currently supported by browsers. It is being worked on - but for now we have to rely on build tools to handle this kind of imports. For a detailed guide on how to build/bundle your elements nicely see [Bundling/Publishing](../README.md#bundlingpublishing).
45
+
46
+ #### Shadow DOM vs. Light DOM
47
+
48
+ By default, _element-js_ will render in light DOM. In this case the styling will not be encapsulated, and your element styles could potentially leak out and style any global selectors.
49
+
50
+ To avoid these problems we highly encourage you to always start your selectors with the element tag name.
51
+
52
+ ```css
53
+ my-element {
54
+ border: 1px solid blue;
55
+ }
56
+
57
+ my-element .element {
58
+ color: red;
59
+ }
60
+ ```
61
+
62
+ When you render an element in Shadow DOM instead you don't have to prefix your selectors with the tag name since Shadow DOM automatically scopes the shadow tree and does not leak any styles to the outside document.
63
+
64
+ ```css
65
+ .element {
66
+ color: red;
67
+ }
68
+ ```
69
+
70
+ To style the element itself in the parent document you can use the special `:host` selector.
71
+
72
+ ```css
73
+ :host {
74
+ border: 1px solid blue;
75
+ }
76
+ ```
77
+
78
+ It is also possible to combine the `:host` selector with other matching selectors.
79
+
80
+ ```css
81
+ :host {
82
+ display: none;
83
+ }
84
+
85
+ :host([selected]) {
86
+ display: block;
87
+ }
88
+ ```
89
+
90
+ #### CSS custom properties
91
+
92
+ Custom properties allow for reusing CSS values throughout a document. To read all about it, see [Using CSS custom properties (variables) - CSS: Cascading Style Sheets | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)
93
+
94
+ The nice thing about custom properties is that they will also take effect inside Shadow DOM elements. They enable therefore a nice and easy way to style certain parts of your element from outside.
95
+
96
+ ```javascript
97
+ import { TemplateElement, defineElement, html } from '@webties/element-js';
98
+
99
+ export class StyledElement extends TemplateElement {
100
+ constructor() {
101
+ super({ shadowRender: true });
102
+ }
103
+
104
+ styles() {
105
+ return ['.element { color: var(—highlight-color, red); }'];
106
+ }
107
+
108
+ template() {
109
+ return html` <span class="“element”">I'm highlighted!</span> `;
110
+ }
111
+ }
112
+
113
+ defineElement('styled-element', StyledElement);
114
+ ```
115
+
116
+ Users of this element can set the value of `—highlight-color`, using the `styled-element` tag as a CSS selector
117
+
118
+ ```css
119
+ styled-element {
120
+ -—highlight-color: blue;
121
+ }
122
+ ```
123
+
124
+ #### PostCSS & Plugins
125
+
126
+ _element-js_ expects the provided CSS to be valid CSS. In recent years developers have been using preprocessing tools like `SCSS` or `LESS` to write more reusable CSS. Pre-processers can however be quite slow during development. Wouldn't it be nice if we could write “normal” CSS again. But with superpowers?
127
+
128
+ Since we would recommend to always use `PostCSS` and `autoprefixer` anyway - we suggest that you add a few more plugins.
129
+
130
+ - postcss-import: [GitHub - postcss/postcss-import: PostCSS plugin to inline @import rules content](https://github.com/postcss/postcss-import)
131
+ - postcss-nested: [GitHub - postcss/postcss-nested: PostCSS plugin to unwrap nested rules like how Sass does it.](https://github.com/postcss/postcss-nested)
132
+
133
+ With these plugins, and also CSS custom properties, you will be able to write composable and reusable CSS.
134
+
135
+ ```css
136
+ :root {
137
+ -—highlight-color: red;
138
+ }
139
+ ```
140
+
141
+ ```postcss
142
+ .btn {
143
+ background: white;
144
+ border: 1px solid grey;
145
+ text: var(—highlight-color);
146
+
147
+ &.is-active {
148
+ border: 2px solid black;
149
+ }
150
+ }
151
+ ```
152
+
153
+ ```postcss
154
+ @import './root.css';
155
+ @import './buttons.css';
156
+
157
+ my-element {
158
+ -—highlight-color: blue;
159
+
160
+ .btn {
161
+ &:hover {
162
+ background: var(—highlight-color);
163
+ text: white;
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ To see how you can integrate PostCSS into your build tools see: [Guides and Tooling](../README.md#bundlingpublishing)
@@ -0,0 +1,308 @@
1
+ ### Rendering / TemplateElement
2
+
3
+ The TemplateElement from _element-js_ can render templates using `lit-html`.
4
+
5
+ For detailed information see the documentation on [lit-html.polymer-project-org](https://lit-html.polymer-project.org)
6
+
7
+ > lit-html lets you write HTML templates in JavaScript, then efficiently render and re-render those templates together with data to create and update DOM
8
+
9
+ To render a template from your element just define a `template()` method and return a `tagged template literal`.
10
+
11
+ ```javascript
12
+ export class MyElement extends TemplateElement {
13
+ properties() {
14
+ return {
15
+ name: 'John'
16
+ };
17
+ }
18
+
19
+ template() {
20
+ return html` <span>Hello ${this.name}</span> `;
21
+ }
22
+ }
23
+ ```
24
+
25
+ #### Shadow DOM vs. Light DOM
26
+
27
+ By default, _element-js_ will render templates in light DOM. When rendering in light DOM all global styles will be applied to your element as well.
28
+
29
+ As mentioned before in the constructor options you can set `shadowRender` to `true` . The element will render templates then in the shadow DOM and encapsulate the element from the rest of the document.
30
+
31
+ #### Data binding
32
+
33
+ When writing templates vor elements you will often need to access dynamic data as in properties from the element. `tagged template literals` are just enhanced `string literals`. To access variables just use `${}` anywhere inside the string.
34
+
35
+ #### Template Directives
36
+
37
+ In `element-js`, directives are powerful utility functions that enhance and simplify the process of writing templates. Here's a detailed breakdown of the available directives:
38
+
39
+ ##### Built-in directives
40
+
41
+ **classMap(map)**
42
+
43
+ Maps object properties to class names based on their truthy values.
44
+
45
+ Parameters:
46
+
47
+ map: Object.<string, string | boolean | number> - An object where the keys represent the class names, and the truthy values determine whether the class should be applied.
48
+
49
+ Returns:
50
+
51
+ A string of class names separated by spaces.
52
+
53
+ Example:
54
+
55
+ ```javascript
56
+ const myMap = { active: true, hidden: false, themeDark: true };
57
+ const classes = classMap(myMap);
58
+ // Returns: "active themeDark"
59
+ ```
60
+
61
+ **styleMap(map)**
62
+
63
+ Maps object properties to inline styles.
64
+
65
+ Parameters:
66
+
67
+ map: Object.<string, string | undefined | null> - An object where the keys represent the CSS properties and the values represent the CSS values.
68
+
69
+ Returns:
70
+
71
+ A string representation of inline styles.
72
+
73
+ Example:
74
+
75
+ ```javascript
76
+ const myStyles = { backgroundColor: 'red', display: null };
77
+ const styles = styleMap(myStyles);
78
+ // Returns: "backgroundColor:red;"
79
+ ```
80
+
81
+ **when(condition, trueCase, falseCase)**
82
+
83
+ Renders one of two template parts based on a condition.
84
+
85
+ Parameters:
86
+
87
+ condition: boolean - The condition to test.
88
+ trueCase: TemplateResult | string - The value to return if the condition is true.
89
+ falseCase: TemplateResult | string (optional) - The value to return if the condition is false.
90
+
91
+ Returns:
92
+
93
+ Either the trueCase or falseCase based on the condition.
94
+
95
+ Example:
96
+
97
+ ```javascript
98
+ const result = when(5 > 3, 'True', 'False');
99
+ // Returns: "True"
100
+ ```
101
+
102
+ **choose(value, cases, defaultCase)**
103
+
104
+ Selects a template based on a provided value from a map of cases.
105
+
106
+ Parameters:
107
+
108
+ value: string - The value to match against the cases.
109
+ cases: Object.<string, TemplateResult | string> - A map of values to templates.
110
+ defaultCase: TemplateResult | string (optional) - The default template to return if no cases match.
111
+
112
+ Returns:
113
+
114
+ The matching case's template or the defaultCase if none match.
115
+
116
+ Example:
117
+
118
+ ```javascript
119
+ const myCases = { one: 'First', two: 'Second' };
120
+ const chosen = choose('two', myCases, 'Unknown');
121
+ // Returns: "Second"
122
+ ```
123
+
124
+ **unsafeHTML(string)**
125
+
126
+ Renders a provided string as raw HTML instead of sanitized text. Note: Use with caution, as this can expose vulnerabilities to cross-site scripting (XSS) attacks.
127
+
128
+ Parameters:
129
+
130
+ string: string - The raw HTML string to render.
131
+
132
+ Returns:
133
+
134
+ A function that, when invoked, returns the HTML content.
135
+
136
+ **spreadAttributes(attributes)**
137
+
138
+ Renders multiple attributes as key="value" pairs from a map of attributes.
139
+
140
+ Parameters:
141
+
142
+ attributes: Object.<string, any> - The attributes map.
143
+
144
+ Returns:
145
+
146
+ A function that, when invoked, returns a string of attributes.
147
+
148
+ Example:
149
+
150
+ ```javascript
151
+ const attrs = { id: 'myElement', ariaLabel: 'Description' };
152
+ const attributeString = spreadAttributes(attrs);
153
+ // When invoked, returns: "id='myElement' aria-label='Description'"
154
+ ```
155
+
156
+ ##### Writing custom directives
157
+
158
+ Directives in `element-js` are just functions. They accept parameters and return values that manipulate or produce templates or strings.
159
+
160
+ To write a custom directive:
161
+
162
+ Define a Function: This function will describe what your directive does. The function should return either a template, string, or another function based on its parameters.
163
+
164
+ ```javascript
165
+ const visibility = (isVisible) => {
166
+ return isVisible ? '' : 'style="display: none;"';
167
+ };
168
+ ```
169
+
170
+ Use your custom directive in your templates.
171
+
172
+ ```javascript
173
+ function template() {
174
+ return html` <div ${visibility(false)}>This div will be hidden.</div> `;
175
+ }
176
+ ```
177
+
178
+ Remember, the power of directives is in their simplicity. They allow you to encapsulate logic in a function and reuse that logic across multiple templates.
179
+
180
+ #### Slots
181
+
182
+ Slots are a very powerful feature when using shadow DOM. By providing slots your elements can accept child elements from the light DOM and have them placed in a specified location inside your elements tree. The slotted content will still belong to the document and therefore also be styled by the document level styles.
183
+
184
+ With this pattern you can build things like Modals, Dialogs, etc. where can encapsulate all the behaviour in the Shadow DOM and let the user pass any content inside the element.
185
+
186
+ ```javascript
187
+ export class ModalElement extends TemplateElement {
188
+ template() {
189
+ return html`
190
+ <div class="backdrop">
191
+ <div class="header">
192
+ <slot name="header"></slot>
193
+ </div>
194
+ <div class="content">
195
+ <slot></slot>
196
+ </div>
197
+ <div class="footer">
198
+ <slot name="footer"></slot>
199
+ </div>
200
+ </div>
201
+ `;
202
+ }
203
+ }
204
+ ```
205
+
206
+ When the user (the using developer of the element) passes any content to the element it will be placed instead of the default slot inside the `div.content`.
207
+
208
+ ```html
209
+ <modal-element>
210
+ <p>I am modal content!</p>
211
+ </modal-element>
212
+ ```
213
+
214
+ Slots can also be named so that you can have multiple different slots.
215
+
216
+ ```html
217
+ <modal-element>
218
+ <p>I am modal content!</p>
219
+ <button slot="footer">OK</button>
220
+ </modal-element>
221
+ ```
222
+
223
+ Any elements that don't have a slot name will be slotted into the default slot.
224
+
225
+ #### Complex Template structures
226
+
227
+ In the examples above we have only seen very simple templates. In real elements you will likely have much more complicated views and your `template()` will start to get messy and hard to read/parse.
228
+
229
+ Lets look at a slightly more advanced element
230
+
231
+ ```javascript
232
+ export class FormControl extends TemplateElement {
233
+ properties() {
234
+ return {
235
+ label: '',
236
+ name: '',
237
+ helpMessage: ''
238
+ };
239
+ }
240
+
241
+ template() {
242
+ return html`
243
+ ${this.label ? html` <label for="input-${this.name}"> ${this.label} </label> ` : ''}
244
+ <div class="control">
245
+ <input id="input-${this.name}" name="${this.name}" aria-describedby="${this.name}-help" />
246
+ </div>
247
+ ${this.helpMessage ? html` <span id="${this.name}-help"> ${this.helpMessage} </span> ` : ''}
248
+ `;
249
+ }
250
+ }
251
+ ```
252
+
253
+ Although we still left out some very important things the template starts to get complicated. To clean this up we can improve the readability by implementing parts of the template in dedicated template functions
254
+
255
+ ```javascript
256
+ export class FormControl extends TemplateElement {
257
+ properties() {
258
+ return {
259
+ label: '',
260
+ name: '',
261
+ helpMessage: ''
262
+ };
263
+ }
264
+
265
+ template() {
266
+ return html`
267
+ ${this.labelTemplate()}
268
+ <div class="control">${this.fieldTemplate()}</div>
269
+ ${this.helpTemplate()}}
270
+ `;
271
+ }
272
+
273
+ labelTemplate() {
274
+ if (!this.label) return null;
275
+ return html` <label for="input-${this.name}"> ${this.label} </label> `;
276
+ }
277
+
278
+ fieldTemplate() {
279
+ return html` <input id="input-${this.name}" name="${this.name}" aria-describedby="${this.name}-help" /> `;
280
+ }
281
+
282
+ helpTemplate() {
283
+ if (!this.helpMessage) return null;
284
+ return html` <span id="${this.name}-help"> ${this.helpMessage} </span> `;
285
+ }
286
+ }
287
+ ```
288
+
289
+ #### SSR (Server side rendering)
290
+
291
+ The templates you write for _element-js_ can easily be rendered on the server or any other environment different from the browser (like Service Workers, Node, etc.). _element-js_ uses special markers (as comments) in the HTML to track positions of control flow and interpolations. When elements get connected in the browser, they don't have to render the templates again. They only need to hydrate.
292
+
293
+ ##### Hydration
294
+
295
+ When you render templates on the server, _element-js_ can efficiently hydrate elements. It uses the existing rendered templates and only adds event listeners and other things that only work in browser environments. One important piece of hydration is the state of the elements. It is not possible to embed the state into the server rendered templates. _element-js_ therefore uses a global JSON object where all elements (and stores) will serialize their state. If such an object is found upon connection of an element, it will restore its state.
296
+
297
+ > The state serialization is not enabled by default. Currently most _element-js_ elements only render in the browser (client side). This might change in the future as more and more applications and elements will get rendered server side.
298
+
299
+ To enable state serialization you can add the following (inline, sync) script block to yor HTML.
300
+
301
+ ```html
302
+ <script>
303
+ /** @type {ElementJsConfig} */
304
+ globalThis.elementJsConfig = {
305
+ serializeState: true
306
+ };
307
+ </script>
308
+ ```
@@ -0,0 +1,54 @@
1
+
2
+ ### Watch Changes
3
+
4
+ Alongside defining reactive properties in the `properties()` map, you can define a watcher for every property by
5
+ implementing a `watch()` map in the same manner.
6
+
7
+ ```javascript
8
+ export class MyElement extends BaseElement {
9
+ properties() {
10
+ return {
11
+ property: 'I am public & reactive'
12
+ };
13
+ }
14
+
15
+ watch() {
16
+ return {
17
+ property: (newValue, oldValue) => {
18
+ console.log('property changed', {newValue, oldValue});
19
+ }
20
+ };
21
+ }
22
+ }
23
+ ```
24
+
25
+ Whenever a property is changed (either from within the element or from outside) it will trigger the callback defined in
26
+ the `watch()` map if present. The callback will be invoked with the old value and the new value as parameters.
27
+
28
+ _element-js_ compares values by stringifying them rather than by reference and will only trigger update/render cycles if
29
+ the value actually changed.
30
+
31
+ > `array` or `object` data will not trigger changes if nested elements/keys are changed. Also methods like `push()`
32
+ > and `shift()` won't work.
33
+
34
+ Non-mutable operations like `map()` and `shift()` and the spread operator will however change the value and trigger an
35
+ update cycle.
36
+
37
+ ```javascript
38
+ export class MyElement extends BaseElement {
39
+ properties() {
40
+ return {
41
+ items: ['one', 'two']
42
+ };
43
+ }
44
+
45
+ count() {
46
+ this.items = [...this.items, 'three'];
47
+ }
48
+ }
49
+ ```
50
+
51
+ The spread operator works for objects and arrays.
52
+
53
+ If you want to make sure that an update will be triggered you can always request it manually by calling
54
+ the `requestUpdate()` method.