@elenajs/core 0.14.0 → 0.15.0
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 +2 -2
- package/README.md +0 -1249
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elenajs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Elena is a simple, tiny library for building Progressive Web Components.",
|
|
5
5
|
"author": "Elena <hi@elenajs.com>",
|
|
6
6
|
"homepage": "https://elenajs.com/",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"typescript": "5.9.3",
|
|
37
37
|
"vitest": "4.0.18"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "b4c41483e5196b542a1b87361f7d37222737fccc"
|
|
40
40
|
}
|
package/README.md
DELETED
|
@@ -1,1249 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/img/elena-dark.png" alt="Elena" width="201" height="230">
|
|
4
|
-
</source>
|
|
5
|
-
<source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/img/elena.png" alt="Elena" width="201" height="230">
|
|
6
|
-
</source>
|
|
7
|
-
<img src="https://elenajs.com/img/elena.png" alt="Elena" width="201" height="230">
|
|
8
|
-
</picture>
|
|
9
|
-
|
|
10
|
-
### Simple, tiny library for building Progressive Web Components.
|
|
11
|
-
|
|
12
|
-
<br/>
|
|
13
|
-
|
|
14
|
-
<a href="https://arielsalminen.com"><img src="https://img.shields.io/badge/creator-@arielle-F95B1F" alt="Creator @arielle"/></a>
|
|
15
|
-
<a href="https://www.npmjs.com/org/elenajs"><img src="https://img.shields.io/npm/v/@elenajs/core.svg" alt="Latest version on npm" /></a>
|
|
16
|
-
<a href="https://github.com/getelena/elena/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="Elena is released under the MIT license." /></a>
|
|
17
|
-
<a href="https://github.com/getelena/elena/actions/workflows/tests.yml"><img src="https://img.shields.io/badge/coverage-100%25-green" alt="Coverage 100%" /></a>
|
|
18
|
-
<a href="https://www.npmjs.com/package/@elenajs/core"><img src="https://img.shields.io/npm/dt/@elenajs/core.svg" alt="Total Downloads"></a>
|
|
19
|
-
<a href="https://github.com/getelena/elena/actions/workflows/tests.yml"><img src="https://github.com/getelena/elena/actions/workflows/tests.yml/badge.svg" alt="Tests status" /></a>
|
|
20
|
-
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<br/>
|
|
24
|
-
|
|
25
|
-
<p align="center"><a href="https://elenajs.com">Elena</a> is a simple, tiny library (2kB) for building <a href="#what-is-a-progressive-web-component">Progressive Web Components</a>. With Elena, you can immediately render the component’s base HTML & CSS, then progressively enhance the experience with JavaScript rather than relying on it from the start. This approach provides great support for <a href="#server-side-rendering">Server Side Rendering</a> <em>(and e.g. React Server Components)</em> without additional configuration or tooling.</p>
|
|
26
|
-
|
|
27
|
-
<br/>
|
|
28
|
-
|
|
29
|
-
## Elena features
|
|
30
|
-
|
|
31
|
-
- 🔋 **Extremely lightweight:** Only 2kB minified & gzipped with zero runtime overhead.
|
|
32
|
-
- 📈 **Progressively enhanced:** Renders HTML & CSS first, then hydrates with JavaScript.
|
|
33
|
-
- 🫶 **Accessible by default:** Semantic HTML foundation with no Shadow DOM barriers.
|
|
34
|
-
- 🌍 **Standards based:** Built entirely on native custom elements & web standards.
|
|
35
|
-
- ⚡ **Reactive props:** Prop changes sync to attributes and trigger updates automatically.
|
|
36
|
-
- 🎨 **Scoped styles:** Simple & clean CSS encapsulation without complex workarounds.
|
|
37
|
-
- 🖥️ **SSR friendly:** Works out of the box, with optional server-side utilities if needed.
|
|
38
|
-
- 🧩 **Zero dependencies:** No runtime dependencies, runs entirely on the web platform.
|
|
39
|
-
- 🔓 **Zero lock-in:** Works with every major framework, or no framework at all.
|
|
40
|
-
|
|
41
|
-
<br/>
|
|
42
|
-
|
|
43
|
-
## Table of contents
|
|
44
|
-
|
|
45
|
-
- **[Design principles](#design-principles)**
|
|
46
|
-
- **[What is a Progressive Web Component?](#what-is-a-progressive-web-component)**
|
|
47
|
-
- **[Getting started](#getting-started)**
|
|
48
|
-
- **[Quick start](#quick-start)**
|
|
49
|
-
- **[Installation](#installation)**
|
|
50
|
-
- **[Creating a component](#create-a-composite-component)**
|
|
51
|
-
- **[Options](#options)**
|
|
52
|
-
- **[Props](#props)**
|
|
53
|
-
- **[Reflecting props to attributes](#reflecting-props-to-attributes)**
|
|
54
|
-
- **[Documenting props](#documenting-props)**
|
|
55
|
-
- **[Prop types](#prop-types)**
|
|
56
|
-
- **[Events](#events)**
|
|
57
|
-
- **[Methods](#methods)**
|
|
58
|
-
- **[Utility methods](#utility-methods)**
|
|
59
|
-
- **[Custom methods](#custom-methods)**
|
|
60
|
-
- **[Templates](#templates)**
|
|
61
|
-
- **[`nothing`](#nothing-1)**
|
|
62
|
-
- **[`unsafeHTML`](#unsafehtml-1)**
|
|
63
|
-
- **[Element ref](#element-ref)**
|
|
64
|
-
- **[Text content](#text-content)**
|
|
65
|
-
- **[Advanced template example](#advanced-template-example)**
|
|
66
|
-
- **[Live demos](#live-demos)**
|
|
67
|
-
- **[Usage examples](#usage-examples)**
|
|
68
|
-
- **[Project examples](#project-examples)**
|
|
69
|
-
- **[Component examples](#component-examples)**
|
|
70
|
-
- **[Server Side Rendering](#server-side-rendering)**
|
|
71
|
-
- **[Avoiding layout shifts](#avoiding-layout-shifts)**
|
|
72
|
-
- **[Rendering Primitive Components to HTML strings](#rendering-primitive-components-to-html-strings)**
|
|
73
|
-
- **[Framework examples](#framework-examples)**
|
|
74
|
-
- **[TypeScript](#typescript)**
|
|
75
|
-
- **[Generating types for components](#generating-types-for-components)**
|
|
76
|
-
- **[Using the generated types](#using-the-generated-types)**
|
|
77
|
-
- **[TypeScript examples](#typescript-examples)**
|
|
78
|
-
- **[Authoring components with TypeScript](#authoring-components-with-typescript)**
|
|
79
|
-
- **[CSS styles](#css-styles)**
|
|
80
|
-
- **[Writing scoped styles](#writing-scoped-styles)**
|
|
81
|
-
- **[Elena CSS Encapsulation Pattern](#elena-css-encapsulation-pattern)**
|
|
82
|
-
- **[Pre-hydration state and styles](#pre-hydration-state-and-styles)**
|
|
83
|
-
- **[Styling Composite Components](#styling-composite-components)**
|
|
84
|
-
- **[Documenting public CSS properties](#documenting-public-css-properties)**
|
|
85
|
-
- **[Misc](#misc)**
|
|
86
|
-
- **[Load event](#load-event)**
|
|
87
|
-
- **[Hide until loaded](#hide-until-loaded)**
|
|
88
|
-
- **[Known issues](#known-issues)**
|
|
89
|
-
- **[Browser compatibility](#browser-compatibility)**
|
|
90
|
-
- **[JavaScript frameworks](#javascript-frameworks)**
|
|
91
|
-
- **[Packages in this monorepo](#packages)**
|
|
92
|
-
- **[Development](#development)**
|
|
93
|
-
|
|
94
|
-
<br/>
|
|
95
|
-
|
|
96
|
-
## Design principles
|
|
97
|
-
|
|
98
|
-
- **Progressive:** Renders HTML and CSS first, hydrates it with JavaScript after.
|
|
99
|
-
- **Reliable:** Predictable lifecycle and property syncing with no hidden magic.
|
|
100
|
-
- **Interoperable:** Built on web standards; no proprietary abstractions.
|
|
101
|
-
- **Modular:** Small, composable pieces you can use independently.
|
|
102
|
-
- **Universal:** Works across frameworks, tools, and environments.
|
|
103
|
-
- **Lightweight:** 2kB minified & gzipped, zero runtime dependencies.
|
|
104
|
-
- **Accessible:** Built on semantic HTML, assistive technologies supported by default.
|
|
105
|
-
|
|
106
|
-
<br/>
|
|
107
|
-
|
|
108
|
-
## What is a Progressive Web Component?
|
|
109
|
-
|
|
110
|
-
A _“Progressive Web Component”_ is a native Custom Element designed in two layers: a base layer of HTML and CSS that renders immediately, without JavaScript, and an enhancement layer of JavaScript that adds reactivity, event handling, and dynamic updates once it loads.
|
|
111
|
-
|
|
112
|
-
This mirrors the classic principle of [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement): start from a functional baseline that works everywhere, then improve the experience for users who have JavaScript available. The result is components that render immediately, are more resilient to script failures, are naturally SSR-friendly, and also compatible with any framework.
|
|
113
|
-
|
|
114
|
-
There are two types of Progressive Web Components:
|
|
115
|
-
|
|
116
|
-
### Primitive Components
|
|
117
|
-
|
|
118
|
-
- Self-contained components that own and render their own HTML markup.
|
|
119
|
-
- All content is controlled through `props`, nothing is composed into them except text content.
|
|
120
|
-
- Examples: `button`, `input`, `checkbox`, `radio`, `textarea`, `icon`, `spinner`, `switch`.
|
|
121
|
-
|
|
122
|
-
### Composite Components
|
|
123
|
-
|
|
124
|
-
- Components that wrap and enhance the HTML composed inside them, including other components.
|
|
125
|
-
- Provide styling, layout, and behavior around the composed content.
|
|
126
|
-
- Examples: `stack`, `table`, `layout`, `card`, `banner`, `visually-hidden`, `fieldset`.
|
|
127
|
-
|
|
128
|
-
<br/>
|
|
129
|
-
|
|
130
|
-
## Getting started
|
|
131
|
-
|
|
132
|
-
### Quick start
|
|
133
|
-
|
|
134
|
-
If you just want to quickly test Elena in a web browser, the fastest way is to include the following directly into your page with a `<script>` tag:
|
|
135
|
-
|
|
136
|
-
```html
|
|
137
|
-
<script type="module">
|
|
138
|
-
import { Elena } from "https://unpkg.com/@elenajs/core@0.13.0";
|
|
139
|
-
|
|
140
|
-
export default class MyComponent extends Elena(HTMLElement, {
|
|
141
|
-
tagName: "my-component"
|
|
142
|
-
}) {
|
|
143
|
-
// Do something, or leave empty.
|
|
144
|
-
// This is a valid Elena (composite) component as is.
|
|
145
|
-
}
|
|
146
|
-
MyComponent.define();
|
|
147
|
-
</script>
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Once created, add scoped `<styles>` for your component as well:
|
|
151
|
-
|
|
152
|
-
```html
|
|
153
|
-
<style>
|
|
154
|
-
@scope (my-component) {
|
|
155
|
-
:scope {
|
|
156
|
-
display: inline-block;
|
|
157
|
-
background: pink;
|
|
158
|
-
color: black;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
</style>
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Now you can use your component anywhere on the page:
|
|
165
|
-
|
|
166
|
-
```html
|
|
167
|
-
<my-component>Hello Elena!</my-component>
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
> [!TIP]
|
|
171
|
-
> Whilst this is the fastest way to get started, we don’t recommend it for production since you would be relying entirely on unpkg CDN. Instead, we recommend using the [@elenajs/bundler](#elenajsbundler) for production, for optimal performance.
|
|
172
|
-
|
|
173
|
-
### Installation
|
|
174
|
-
|
|
175
|
-
To install Elena as a dependency in your project, run:
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
npm install @elenajs/core
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Once Elena is installed, you can import it from the package:
|
|
182
|
-
|
|
183
|
-
```js
|
|
184
|
-
import { Elena } from "@elenajs/core";
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Create a Composite Component
|
|
188
|
-
|
|
189
|
-
```js
|
|
190
|
-
// ░ [ELENA]: Composite Component
|
|
191
|
-
export default class Stack extends Elena(HTMLElement, {
|
|
192
|
-
tagName: "elena-stack",
|
|
193
|
-
props: ["direction"],
|
|
194
|
-
}) {
|
|
195
|
-
constructor() {
|
|
196
|
-
super();
|
|
197
|
-
this.direction = "column";
|
|
198
|
-
}
|
|
199
|
-
// Note that Composite Components do not call render()
|
|
200
|
-
}
|
|
201
|
-
Stack.define();
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
#### Usage:
|
|
205
|
-
|
|
206
|
-
```html
|
|
207
|
-
<elena-stack>
|
|
208
|
-
<elena-input label="Name" type="text"></elena-input>
|
|
209
|
-
<elena-input label="Email" type="email"></elena-input>
|
|
210
|
-
<elena-textarea label="Message"></elena-textarea>
|
|
211
|
-
<elena-button type="submit">Submit</elena-button>
|
|
212
|
-
</elena-stack>
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### …Or, create a Primitive Component
|
|
216
|
-
|
|
217
|
-
```js
|
|
218
|
-
import { Elena, html } from "@elenajs/core";
|
|
219
|
-
|
|
220
|
-
// ░ [ELENA]: Primitive Component
|
|
221
|
-
export default class Button extends Elena(HTMLElement, {
|
|
222
|
-
tagName: "elena-button",
|
|
223
|
-
props: ["variant"],
|
|
224
|
-
}) {
|
|
225
|
-
constructor() {
|
|
226
|
-
super();
|
|
227
|
-
this.variant = "default";
|
|
228
|
-
}
|
|
229
|
-
// Primitive Components return their `html` in render()
|
|
230
|
-
render() {
|
|
231
|
-
return html`<button>${this.text}</button>`;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
Button.define();
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
#### Usage:
|
|
238
|
-
|
|
239
|
-
```html
|
|
240
|
-
<elena-button variant="primary">Save</elena-button>
|
|
241
|
-
<elena-button>Cancel</elena-button>
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
<br/>
|
|
245
|
-
|
|
246
|
-
## Options
|
|
247
|
-
|
|
248
|
-
Elena provides an options object where you can set the following:
|
|
249
|
-
|
|
250
|
-
```js
|
|
251
|
-
export default class Button extends Elena(HTMLElement, {
|
|
252
|
-
// Custom element tag name to register:
|
|
253
|
-
tagName: "elena-button",
|
|
254
|
-
|
|
255
|
-
// Props to observe and sync as attributes:
|
|
256
|
-
props: ["label", "disabled"],
|
|
257
|
-
|
|
258
|
-
// Events to delegate from the inner element:
|
|
259
|
-
events: ["click", "focus", "blur"],
|
|
260
|
-
|
|
261
|
-
// CSS selector for the inner element to be used as Ref:
|
|
262
|
-
element: ".my-button",
|
|
263
|
-
})
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
All of Elena’s options are optional. `tagName` is required only if you want Elena to handle the web component registration for you. Otherwise call `customElements.define()` yourself:
|
|
267
|
-
|
|
268
|
-
```js
|
|
269
|
-
export default class Button extends Elena(HTMLElement) {
|
|
270
|
-
// do something...
|
|
271
|
-
}
|
|
272
|
-
customElements.define("elena-button", Button);
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
Please note though that doing this means that your web component can no longer be used in a server context.
|
|
276
|
-
|
|
277
|
-
> [!TIP]
|
|
278
|
-
> When working with Primitive Components, leaving out `element` option means that Elena will try use `firstElementChild` instead, if available. In cases when your template markup is simple, this is actually more performant when you have hundreds or even thousands of Elena components on a page.
|
|
279
|
-
|
|
280
|
-
<br/>
|
|
281
|
-
|
|
282
|
-
## Props
|
|
283
|
-
|
|
284
|
-
Elena allows you to define prop declarations in its options object. This makes Elena aware of what external props passed to the element should be observed and synced as attributes between the web component host and the inner template element (passed as an `element` in options).
|
|
285
|
-
|
|
286
|
-
Props are declared in the `props` array in the options object, with default values set inside the `constructor`:
|
|
287
|
-
|
|
288
|
-
```js
|
|
289
|
-
export default class Button extends Elena(HTMLElement, {
|
|
290
|
-
props: ["variant", "disabled", "value", "type"],
|
|
291
|
-
}) {
|
|
292
|
-
constructor() {
|
|
293
|
-
super();
|
|
294
|
-
|
|
295
|
-
this.variant = "default";
|
|
296
|
-
this.disabled = false;
|
|
297
|
-
this.value = "";
|
|
298
|
-
this.type = "button";
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
> [!TIP]
|
|
304
|
-
> When naming properties, keep them simple, easy to understand, and a maximum of 1 word (e.g. `variant`).
|
|
305
|
-
|
|
306
|
-
### Reflecting props to attributes
|
|
307
|
-
|
|
308
|
-
By default, Elena reflects all properties to the host element as HTML attributes. If you want to disable this feature for a specific property, use `reflect: false`:
|
|
309
|
-
|
|
310
|
-
```js
|
|
311
|
-
const options = {
|
|
312
|
-
props: [
|
|
313
|
-
"variant",
|
|
314
|
-
"size",
|
|
315
|
-
{ name: "icon", reflect: false },
|
|
316
|
-
],
|
|
317
|
-
};
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Documenting props
|
|
321
|
-
|
|
322
|
-
In addition to declaring props, you can (and should!) document them using a [JSDoc style syntax](https://jsdoc.app):
|
|
323
|
-
|
|
324
|
-
```js
|
|
325
|
-
/**
|
|
326
|
-
* The style variant of the button.
|
|
327
|
-
* @attribute
|
|
328
|
-
* @type {"default" | "primary" | "danger"}
|
|
329
|
-
*/
|
|
330
|
-
this.variant = "default";
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Makes the component disabled.
|
|
334
|
-
* @attribute
|
|
335
|
-
* @type {Boolean}
|
|
336
|
-
*/
|
|
337
|
-
this.disabled = false;
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* The value used to identify the button in forms.
|
|
341
|
-
* @attribute
|
|
342
|
-
* @type {string}
|
|
343
|
-
*/
|
|
344
|
-
this.value = "";
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* The type of the button.
|
|
348
|
-
* @attribute
|
|
349
|
-
* @type {"submit" | "reset" | "button"}
|
|
350
|
-
*/
|
|
351
|
-
this.type = "button";
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
> [!TIP]
|
|
355
|
-
> **`@elenajs/bundler`** transforms the above JSDocs automatically to TypeScript types and Custom Elements Manifest which allows tooling and IDEs to give rich information about the Elena elements.
|
|
356
|
-
|
|
357
|
-
### Prop types
|
|
358
|
-
|
|
359
|
-
The `@type` can be one of the following native constructors:
|
|
360
|
-
|
|
361
|
-
```js
|
|
362
|
-
/** @type {string} */
|
|
363
|
-
/** @type {Number} */
|
|
364
|
-
/** @type {Array} */
|
|
365
|
-
/** @type {Boolean} */
|
|
366
|
-
/** @type {Object} */
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
Additionally, you can provide possible prop values using the following syntax:
|
|
370
|
-
|
|
371
|
-
```js
|
|
372
|
-
/** @type {"default" | "primary" | "danger"} */
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
<br/>
|
|
376
|
-
|
|
377
|
-
## Events
|
|
378
|
-
|
|
379
|
-
Elena allows you to define event declarations in its options object. The `events` array is used for determining which events the element should listen to and delegate from the inner template element:
|
|
380
|
-
|
|
381
|
-
```js
|
|
382
|
-
export default class Button extends Elena(HTMLElement, {
|
|
383
|
-
events: ["click", "focus", "blur"],
|
|
384
|
-
})
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
Once declared, Elena will set up the necessary event listeners and dispatching logic and take care of cleanup when the element is removed from the DOM.
|
|
388
|
-
|
|
389
|
-
> [!TIP]
|
|
390
|
-
> You can alternatively build your own custom logic inside the web component for events and not rely on the built-in functionality in Elena.
|
|
391
|
-
|
|
392
|
-
<br/>
|
|
393
|
-
|
|
394
|
-
## Methods
|
|
395
|
-
|
|
396
|
-
Elena ships with the following built-in lifecycle methods:
|
|
397
|
-
|
|
398
|
-
- **`connectedCallback()`:** Called each time the element is added to the DOM.
|
|
399
|
-
- **`disconnectedCallback()`:** Called each time the element is removed from the DOM.
|
|
400
|
-
- **`attributeChangedCallback()`:** Called when Elena’s props are changed, added, removed or replaced.
|
|
401
|
-
- **`render()`:** Called whenever there’s an update that needs rendering.
|
|
402
|
-
- **`updated()`:** Performs a post-update and adds the `hydrated` attribute to the Host element.
|
|
403
|
-
|
|
404
|
-
### Utility methods
|
|
405
|
-
|
|
406
|
-
Additionally, Elena provides the following utility methods:
|
|
407
|
-
|
|
408
|
-
#### `ClassName.define()`
|
|
409
|
-
|
|
410
|
-
Register the web component with SSR guards. Call this on your subclass after the class body is defined. The tag name is read from the `tagName` option set when calling `Elena()`.
|
|
411
|
-
|
|
412
|
-
```js
|
|
413
|
-
MyElement.define();
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
#### `html`
|
|
417
|
-
|
|
418
|
-
Tagged template for defining an Elena web component’s HTML structure. Return it from `render()`. Dynamic values are auto-escaped, and nested `html` sub-templates pass through as trusted HTML without double-escaping:
|
|
419
|
-
|
|
420
|
-
```js
|
|
421
|
-
import { Elena, html } from "@elenajs/core";
|
|
422
|
-
|
|
423
|
-
// ...later:
|
|
424
|
-
render() {
|
|
425
|
-
return html`
|
|
426
|
-
<button class="elena-button">
|
|
427
|
-
${this.text}
|
|
428
|
-
</button>
|
|
429
|
-
`;
|
|
430
|
-
}
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
#### `nothing`
|
|
434
|
-
|
|
435
|
-
A placeholder you can use in conditional template expressions when there is nothing to render. It always produces an empty string and signals to the template engine that no processing is needed.
|
|
436
|
-
|
|
437
|
-
```js
|
|
438
|
-
import { Elena, html, nothing } from "@elenajs/core";
|
|
439
|
-
|
|
440
|
-
// ...later:
|
|
441
|
-
render() {
|
|
442
|
-
return html`
|
|
443
|
-
<button>
|
|
444
|
-
${this.icon ? html`<span class="icon">${this.icon}</span>` : nothing}
|
|
445
|
-
${this.text}
|
|
446
|
-
</button>
|
|
447
|
-
`;
|
|
448
|
-
}
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
#### `unsafeHTML`
|
|
452
|
-
|
|
453
|
-
Values interpolated into Elena’s `html` tagged template are auto-escaped to prevent XSS. `unsafeHTML` allows you to bypass this and render a trusted HTML string without escaping, for example an SVG icon or markup from a database.
|
|
454
|
-
|
|
455
|
-
```js
|
|
456
|
-
import { Elena, html, unsafeHTML, nothing } from "@elenajs/core";
|
|
457
|
-
|
|
458
|
-
// ...later:
|
|
459
|
-
render() {
|
|
460
|
-
const icon = this.icon ? unsafeHTML(`<span>${this.icon}</span>`) : nothing;
|
|
461
|
-
const text = this.text ? html`<span>${this.text}</span>` : nothing;
|
|
462
|
-
|
|
463
|
-
return html`
|
|
464
|
-
<button>
|
|
465
|
-
${text}
|
|
466
|
-
${icon}
|
|
467
|
-
</button>
|
|
468
|
-
`;
|
|
469
|
-
}
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### Custom methods
|
|
473
|
-
|
|
474
|
-
You can also define your own custom methods:
|
|
475
|
-
|
|
476
|
-
```js
|
|
477
|
-
export default class Button extends Elena(HTMLElement) {
|
|
478
|
-
/**
|
|
479
|
-
* Renders a link: <a href="#">.
|
|
480
|
-
* @internal
|
|
481
|
-
*/
|
|
482
|
-
renderLink(template) {
|
|
483
|
-
return html`
|
|
484
|
-
<a
|
|
485
|
-
class="elena-button"
|
|
486
|
-
href="${this.href}"
|
|
487
|
-
target="${this.target}"
|
|
488
|
-
${this.download ? "download" : nothing}
|
|
489
|
-
${this.label ? html`aria-label="${this.label}"` : nothing}>
|
|
490
|
-
${template}
|
|
491
|
-
</a>
|
|
492
|
-
`;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
Elena also allows you to extend the lifecycle methods by calling `super`:
|
|
498
|
-
|
|
499
|
-
```js
|
|
500
|
-
export default class Button extends Elena(HTMLElement) {
|
|
501
|
-
|
|
502
|
-
connectedCallback() {
|
|
503
|
-
super.connectedCallback();
|
|
504
|
-
console.log("Element was added to the DOM.");
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
disconnectedCallback() {
|
|
508
|
-
super.disconnectedCallback();
|
|
509
|
-
console.log("Element was removed from the DOM.");
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
<br/>
|
|
515
|
-
|
|
516
|
-
## Templates
|
|
517
|
-
|
|
518
|
-
Elena uses an HTML-based template syntax built on JavaScript [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). Return an `html` tagged template from `render()`:
|
|
519
|
-
|
|
520
|
-
```js
|
|
521
|
-
import { Elena, html } from "@elenajs/core";
|
|
522
|
-
|
|
523
|
-
// ...later:
|
|
524
|
-
render() {
|
|
525
|
-
return html`
|
|
526
|
-
<button variant="${this.variant || "default"}">
|
|
527
|
-
${this.text}
|
|
528
|
-
</button>
|
|
529
|
-
`;
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
The content of the `html` method is passed as tagged template literals, which Elena then compiles on the fly.
|
|
534
|
-
|
|
535
|
-
### `nothing`
|
|
536
|
-
|
|
537
|
-
A placeholder you can use in conditional template expressions when there is nothing to render. It always produces an empty string and signals to the template engine that no processing is needed.
|
|
538
|
-
|
|
539
|
-
```js
|
|
540
|
-
import { Elena, html, nothing } from "@elenajs/core";
|
|
541
|
-
|
|
542
|
-
// ...later:
|
|
543
|
-
render() {
|
|
544
|
-
return html`
|
|
545
|
-
<button>
|
|
546
|
-
${this.icon ? html`<span class="icon">${this.icon}</span>` : nothing}
|
|
547
|
-
${this.text}
|
|
548
|
-
</button>
|
|
549
|
-
`;
|
|
550
|
-
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### `unsafeHTML`
|
|
554
|
-
|
|
555
|
-
Values interpolated into Elena’s `html` tagged template are auto-escaped to prevent XSS. `unsafeHTML` allows you to bypass this and render a trusted HTML string without escaping, for example an SVG icon or markup from a database.
|
|
556
|
-
|
|
557
|
-
```js
|
|
558
|
-
import { Elena, html, unsafeHTML, nothing } from "@elenajs/core";
|
|
559
|
-
|
|
560
|
-
// ...later:
|
|
561
|
-
render() {
|
|
562
|
-
const icon = this.icon ? unsafeHTML(`<span>${this.icon}</span>`) : nothing;
|
|
563
|
-
const text = this.text ? html`<span>${this.text}</span>` : nothing;
|
|
564
|
-
|
|
565
|
-
return html`
|
|
566
|
-
<button>
|
|
567
|
-
${text}
|
|
568
|
-
${icon}
|
|
569
|
-
</button>
|
|
570
|
-
`;
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
### Element ref
|
|
575
|
-
|
|
576
|
-
Elena provides a special **Ref** to the `element` you pass as a DOM selector:
|
|
577
|
-
|
|
578
|
-
```js
|
|
579
|
-
export default class Button extends Elena(HTMLElement, {
|
|
580
|
-
element: ".my-button",
|
|
581
|
-
})
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
This allows you a direct access to the underlying DOM element:
|
|
585
|
-
|
|
586
|
-
```js
|
|
587
|
-
console.log(this.element);
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### Text content
|
|
591
|
-
|
|
592
|
-
Every Elena element has a built-in reactive `text` property. On first connect, Elena automatically captures the element’s `textContent` from the light DOM before rendering. This lets you pass text content naturally as children:
|
|
593
|
-
|
|
594
|
-
```html
|
|
595
|
-
<elena-button>Click me</elena-button>
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
Use `this.text` in your component's `render()` method to reference the captured text:
|
|
599
|
-
|
|
600
|
-
```js
|
|
601
|
-
render() {
|
|
602
|
-
return html`<button class="elena-button">${this.text}</button>`;
|
|
603
|
-
}
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
The `text` property is reactive, setting it programmatically triggers a re-render:
|
|
607
|
-
|
|
608
|
-
```js
|
|
609
|
-
const button = document.querySelector("elena-button");
|
|
610
|
-
button.text = "Save changes";
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
When used with JavaScript frameworks, passing text as children works for static text:
|
|
614
|
-
|
|
615
|
-
```jsx
|
|
616
|
-
// Works for static text
|
|
617
|
-
<elena-button>Click me</elena-button>
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
For dynamic text that changes over time, use the `text` property instead, since **Primitive Components** own their internal DOM and frameworks cannot update children after Elena has hydrated the element:
|
|
621
|
-
|
|
622
|
-
```jsx
|
|
623
|
-
// React
|
|
624
|
-
<elena-button text={buttonText} />
|
|
625
|
-
|
|
626
|
-
// Angular
|
|
627
|
-
<elena-button [text]="buttonText"></elena-button>
|
|
628
|
-
|
|
629
|
-
// Vue
|
|
630
|
-
<elena-button :text="buttonText"></elena-button>
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
> [!TIP]
|
|
634
|
-
> **Composite Components** don’t need the above, they preserve children naturally since they have no `render()` method. This feature is for **Primitive Components** only which own their internal DOM and would otherwise destroy any children passed to them.
|
|
635
|
-
|
|
636
|
-
### Advanced template example
|
|
637
|
-
|
|
638
|
-
```js
|
|
639
|
-
import { Elena, html, nothing } from "@elenajs/core";
|
|
640
|
-
|
|
641
|
-
// ...later:
|
|
642
|
-
render() {
|
|
643
|
-
return html`
|
|
644
|
-
<label for="${this.identifier}">${this.label}</label>
|
|
645
|
-
<div class="elena-input-wrapper">
|
|
646
|
-
${this.start ? html`<div class="elena-input-start">${this.start}</div>` : nothing}
|
|
647
|
-
<input
|
|
648
|
-
id="${this.identifier}"
|
|
649
|
-
class="elena-input ${this.start ? "elena-input-has-start" : nothing}"
|
|
650
|
-
/>
|
|
651
|
-
</div>
|
|
652
|
-
${this.error ? html`<div class="elena-input-error">${this.error}</div>` : nothing}
|
|
653
|
-
`;
|
|
654
|
-
}
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
<br/>
|
|
658
|
-
|
|
659
|
-
## Live demos
|
|
660
|
-
|
|
661
|
-
- **[Client, partial SSR](https://arielsalminen.com/elena/)**
|
|
662
|
-
- **[Server, full SSR](https://arielsalminen.com/elena/server.html)**
|
|
663
|
-
|
|
664
|
-
<br/>
|
|
665
|
-
|
|
666
|
-
## Usage examples
|
|
667
|
-
|
|
668
|
-
### Project examples
|
|
669
|
-
|
|
670
|
-
- **[Usage with Angular](https://github.com/getelena/angular-example-project)**
|
|
671
|
-
- **[Usage with Eleventy](https://github.com/getelena/eleventy-example-project)**
|
|
672
|
-
- **[Usage with HTML](https://github.com/getelena/html-example-project)**
|
|
673
|
-
- **[Usage with Next.js](https://github.com/getelena/next-example-project)**
|
|
674
|
-
- **[Usage with React](https://github.com/getelena/react-example-project)**
|
|
675
|
-
- **[Usage with Svelte](https://github.com/getelena/svelte-example-project)**
|
|
676
|
-
- **[Usage with Vue](https://github.com/getelena/vue-example-project)**
|
|
677
|
-
|
|
678
|
-
### Component examples
|
|
679
|
-
|
|
680
|
-
#### Composite Component
|
|
681
|
-
|
|
682
|
-
Below is an example of a **Composite Component** which includes `props` and documentation:
|
|
683
|
-
|
|
684
|
-
```js
|
|
685
|
-
// ░ [ELENA]: Composite Component
|
|
686
|
-
import { Elena } from "@elenajs/core";
|
|
687
|
-
|
|
688
|
-
const options = {
|
|
689
|
-
tagName: "elena-stack",
|
|
690
|
-
props: ["direction"],
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Stack component manages layout of immediate children
|
|
695
|
-
* with optional spacing between each child.
|
|
696
|
-
*
|
|
697
|
-
* @displayName Stack
|
|
698
|
-
* @slot - The stacked content
|
|
699
|
-
* @status alpha
|
|
700
|
-
*/
|
|
701
|
-
export default class Stack extends Elena(HTMLElement, options) {
|
|
702
|
-
constructor() {
|
|
703
|
-
super();
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* The direction of the stack.
|
|
707
|
-
* @attribute
|
|
708
|
-
* @type {"column" | "row"}
|
|
709
|
-
*/
|
|
710
|
-
this.direction = "column";
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
Stack.define();
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
#### Primitive Component
|
|
717
|
-
|
|
718
|
-
Below is an example of a **Primitive Component** which includes `props`, `events`, `methods` and documentation:
|
|
719
|
-
|
|
720
|
-
```js
|
|
721
|
-
// ░ [ELENA]: Primitive Component
|
|
722
|
-
import { Elena, html } from "@elenajs/core";
|
|
723
|
-
|
|
724
|
-
const options = {
|
|
725
|
-
tagName: "elena-button",
|
|
726
|
-
props: ["variant", "disabled", "type"],
|
|
727
|
-
events: ["click", "focus", "blur"],
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* The description of the component goes here.
|
|
732
|
-
*
|
|
733
|
-
* @displayName Button
|
|
734
|
-
* @status alpha
|
|
735
|
-
*
|
|
736
|
-
* @event click - Programmatically fire click on the component.
|
|
737
|
-
* @event focus - Programmatically move focus to the component.
|
|
738
|
-
* @event blur - Programmatically remove focus from the component.
|
|
739
|
-
*
|
|
740
|
-
* @cssprop [--elena-button-text] - Overrides the default text color.
|
|
741
|
-
* @cssprop [--elena-button-bg] - Overrides the default background color.
|
|
742
|
-
* @cssprop [--elena-button-font] - Overrides the default font-family.
|
|
743
|
-
*/
|
|
744
|
-
export default class Button extends Elena(HTMLElement, options) {
|
|
745
|
-
constructor() {
|
|
746
|
-
super();
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* The style variant of the button.
|
|
750
|
-
* @attribute
|
|
751
|
-
* @type {"default" | "primary" | "danger"}
|
|
752
|
-
*/
|
|
753
|
-
this.variant = "default";
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* Makes the component disabled.
|
|
757
|
-
* @attribute
|
|
758
|
-
* @type {Boolean}
|
|
759
|
-
*/
|
|
760
|
-
this.disabled = false;
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* The type of the button.
|
|
764
|
-
* @attribute
|
|
765
|
-
* @type {"submit" | "reset" | "button"}
|
|
766
|
-
*/
|
|
767
|
-
this.type = "button";
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* An example custom method.
|
|
772
|
-
*/
|
|
773
|
-
myMethod() {
|
|
774
|
-
console.log(this.element);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Renders the button component template.
|
|
779
|
-
* @internal
|
|
780
|
-
*/
|
|
781
|
-
render() {
|
|
782
|
-
return html`
|
|
783
|
-
<button>${this.text}</button>
|
|
784
|
-
`;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
Button.define();
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
<br/>
|
|
791
|
-
|
|
792
|
-
## Server Side Rendering
|
|
793
|
-
|
|
794
|
-
Elena’s recommended approach to Server Side Rendering (SSR) is simple & straightforward. Since [Progressive Web Components](#what-is-a-progressive-web-component) are primarily HTML & CSS, you don’t need any special logic on the server to render them. The **[Composite Components](#2-composite-components)** provide a full support for SSR by default, while the **[Primitive Components](#1-primitive-components)** provide a partial support and do the rest of the hydration on the client side.
|
|
795
|
-
|
|
796
|
-
Partial SSR support for the **Primitive Components** means that the component’s base HTML & CSS lives in the `Light DOM`. The JavaScript lifecycle is then used to progressively enhance the functionality and markup once the element is registered.
|
|
797
|
-
|
|
798
|
-
The benefit of Elena’s approach is that it doesn’t need any extra logic on the server while still allowing you to ship all your layout components _(the “Composite Components"!)_ with full SSR support.
|
|
799
|
-
|
|
800
|
-
### Avoiding layout shifts
|
|
801
|
-
|
|
802
|
-
For the **Primitive Components** specifically, our recommendation is to ship them with CSS styles that visually matches the `loading` and `hydrated` states without causing layout shift, FOUC, or FOIC _(Flash Of Unstyled Content, Flash Of Invisible Content)._ This can be achieved utilizing the provided `hydrated` attribute in your component styles:
|
|
803
|
-
|
|
804
|
-
```css
|
|
805
|
-
/* Elena SSR Pattern to avoid layout shift */
|
|
806
|
-
:scope:not([hydrated]),
|
|
807
|
-
.inner-element {
|
|
808
|
-
color: var(--elena-button-text);
|
|
809
|
-
}
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
Since **Primitive Components** are self-contained and render their own HTML markup, you may sometimes need access to more than just the initial text content pre-hydration for better SSR support to avoid layout shifts. This can be achieved with pseudo elements in CSS by referencing the attributes set on the element itself:
|
|
813
|
-
|
|
814
|
-
```css
|
|
815
|
-
:scope:not([hydrated])::before {
|
|
816
|
-
content: attr(label);
|
|
817
|
-
/* etc */
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
:scope:not([hydrated])::after {
|
|
821
|
-
content: attr(placeholder);
|
|
822
|
-
/* etc */
|
|
823
|
-
}
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
> [!TIP]
|
|
827
|
-
> You can skip this section entirely for Composite Components, when you plan to [hide components until loaded](#hide-until-loaded), or when the rest of your app renders client side only.
|
|
828
|
-
|
|
829
|
-
### Rendering Primitive Components to HTML strings
|
|
830
|
-
|
|
831
|
-
When you don’t want to handle the pre-hydration state with CSS, you can expand the **Primitive Component** templates inline by using the provided utility package called [@elenajs/ssr](https://github.com/getelena/elena/tree/main/packages/ssr) that renders the Elena Primitive Components to HTML strings for full SSR support.
|
|
832
|
-
|
|
833
|
-
Please see the [SSR package’s readme](https://github.com/getelena/elena/tree/main/packages/ssr) for full usage guidelines.
|
|
834
|
-
|
|
835
|
-
> [!WARNING]
|
|
836
|
-
> Please note that `@elenajs/ssr` is an experimental package and not yet ready for production use. APIs may change without notice.
|
|
837
|
-
|
|
838
|
-
### Framework examples
|
|
839
|
-
|
|
840
|
-
Elena currently provides SSR examples for the following frameworks:
|
|
841
|
-
|
|
842
|
-
- **[Eleventy](https://github.com/getelena/eleventy-example-project)**
|
|
843
|
-
- **[Plain HTML](https://github.com/getelena/html-example-project)**
|
|
844
|
-
- **[Next.js](https://github.com/getelena/next-example-project)** _(Elena can even be used inside React Server Components, see [src/app/page.tsx](https://github.com/getelena/next-example-project/blob/main/src/app/page.tsx))_
|
|
845
|
-
|
|
846
|
-
<br/>
|
|
847
|
-
|
|
848
|
-
## TypeScript
|
|
849
|
-
|
|
850
|
-
Elena is written in vanilla JavaScript with JSDoc annotations. The **`@elenajs/core`** library ships its own type declarations (`dist/elena.d.ts`) which are generated automatically by `tsc` from the JSDoc so that you get full IntelliSense and type checking.
|
|
851
|
-
|
|
852
|
-
```ts
|
|
853
|
-
import { Elena, html, nothing } from "@elenajs/core";
|
|
854
|
-
// Elena, ElenaOptions, html, nothing are all typed
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
### Generating types for components
|
|
858
|
-
|
|
859
|
-
When you build your own Elena components, **`@elenajs/bundler`** can generate TypeScript declarations for each one. Running `elena build` (or calling the bundler programmatically) produces:
|
|
860
|
-
|
|
861
|
-
- **Per-component `.d.ts` files**: A declaration file for each component (e.g. `button.d.ts`) with typed props and event handlers, derived from your JSDoc annotations. This lets TypeScript resolve sub-path imports like `@my-lib/components/dist/button.js`.
|
|
862
|
-
- **`custom-elements.json`**: The [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/), a machine-readable description of your components used by IDEs and documentation tools.
|
|
863
|
-
- **`custom-elements.d.ts`**: JSX integration types that map your custom element tag names to their prop types. This enables autocomplete and type checking for `<elena-button variant="primary" />` in JSX/TSX files.
|
|
864
|
-
|
|
865
|
-
### Using the generated types
|
|
866
|
-
|
|
867
|
-
The generated `custom-elements.d.ts` exports a `CustomElements` type map and a `ScopedElements` helper. To get type checking in JSX (this works with Next.js, see further down for more examples):
|
|
868
|
-
|
|
869
|
-
```ts
|
|
870
|
-
// types.d.ts (in your consuming project)
|
|
871
|
-
import type { CustomElements } from "@my-lib/components";
|
|
872
|
-
|
|
873
|
-
type ElenaIntrinsicElements = {
|
|
874
|
-
[K in keyof CustomElements]: CustomElements[K] & {
|
|
875
|
-
onClick?: (e: MouseEvent) => void;
|
|
876
|
-
onFocus?: (e: FocusEvent) => void;
|
|
877
|
-
onBlur?: (e: FocusEvent) => void;
|
|
878
|
-
children?: React.ReactNode;
|
|
879
|
-
};
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
declare module "react" {
|
|
883
|
-
namespace JSX {
|
|
884
|
-
interface IntrinsicElements extends ElenaIntrinsicElements {}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
### TypeScript examples
|
|
890
|
-
|
|
891
|
-
Elena provides TypeScript examples for the following JavaScript frameworks:
|
|
892
|
-
|
|
893
|
-
- **[Next.js](https://github.com/getelena/next-example-project)**
|
|
894
|
-
- **[React](https://github.com/getelena/react-example-project)**
|
|
895
|
-
- **[Svelte](https://github.com/getelena/svelte-example-project)**
|
|
896
|
-
- **[Vue](https://github.com/getelena/vue-example-project)**
|
|
897
|
-
|
|
898
|
-
### Authoring components with TypeScript
|
|
899
|
-
|
|
900
|
-
When using TypeScript (instead of JavaScript) to author the Elena components, you can simplify the code (like omitting the `constructor` part) and have your type definitions inline:
|
|
901
|
-
|
|
902
|
-
```ts
|
|
903
|
-
// ░ [ELENA]: Primitive Component
|
|
904
|
-
import { Elena, html } from "@elenajs/core";
|
|
905
|
-
|
|
906
|
-
export default class Button extends Elena(HTMLElement, {
|
|
907
|
-
tagName: "elena-button",
|
|
908
|
-
props: ["variant"],
|
|
909
|
-
}) {
|
|
910
|
-
/**
|
|
911
|
-
* The style variant of the component.
|
|
912
|
-
* @attribute
|
|
913
|
-
*/
|
|
914
|
-
variant: "default" | "primary" | "danger" = "default";
|
|
915
|
-
|
|
916
|
-
/**
|
|
917
|
-
* Renders the html template.
|
|
918
|
-
* @internal
|
|
919
|
-
*/
|
|
920
|
-
render() {
|
|
921
|
-
return html`
|
|
922
|
-
<button>${this.text}</button>
|
|
923
|
-
`;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
Button.define();
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
<br/>
|
|
930
|
-
|
|
931
|
-
## CSS styles
|
|
932
|
-
|
|
933
|
-
These guidelines cover the approaches that we recommend when styling Progressive Web Components to make them work reliably across the lifecycle of a component. You’re obviously able to craft the CSS the best way you see fit for your purpose, but there are some things to take into account that we’ve tried to cover below.
|
|
934
|
-
|
|
935
|
-
### Writing scoped styles
|
|
936
|
-
|
|
937
|
-
Elena recommends using the [@scope](https://caniuse.com/css-cascade-scope) at-rule which prevents the component styles from leaking to the outer page. This makes it possible to have entirely isolated styles without sacrificing inheritance or cascading:
|
|
938
|
-
|
|
939
|
-
```css
|
|
940
|
-
@scope (elena-button) {
|
|
941
|
-
/**
|
|
942
|
-
* Scoped styles for the elena-button. These won’t leak
|
|
943
|
-
* out or affect any other elements in your app.
|
|
944
|
-
*/
|
|
945
|
-
}
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
To style the host `elena-button` itself, you can use `:scope`:
|
|
949
|
-
|
|
950
|
-
```css
|
|
951
|
-
@scope (elena-button) {
|
|
952
|
-
|
|
953
|
-
/* Targets the host element (elena-button) */
|
|
954
|
-
:scope {
|
|
955
|
-
all: unset;
|
|
956
|
-
display: inline-block;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
The full baseline pattern for authoring encapsulated component styles looks like this:
|
|
962
|
-
|
|
963
|
-
```css
|
|
964
|
-
/* Scope makes sure styles don’t leak out */
|
|
965
|
-
@scope (elena-button) {
|
|
966
|
-
|
|
967
|
-
/* Unset makes sure styles don’t leak in */
|
|
968
|
-
:scope, *, *::before, *::after {
|
|
969
|
-
all: unset;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/* Targets the host element (elena-button) */
|
|
973
|
-
:scope {
|
|
974
|
-
|
|
975
|
-
/* Public CSS properties */
|
|
976
|
-
--elena-button-font: sans-serif;
|
|
977
|
-
--elena-button-text: white;
|
|
978
|
-
--elena-button-bg: blue;
|
|
979
|
-
|
|
980
|
-
/* Display mode for the host element */
|
|
981
|
-
display: inline-block;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
/* Elena SSR Pattern to avoid layout shift */
|
|
985
|
-
:scope:not([hydrated]),
|
|
986
|
-
button {
|
|
987
|
-
font-family: var(--elena-button-font);
|
|
988
|
-
color: var(--elena-button-text);
|
|
989
|
-
background: var(--elena-button-bg);
|
|
990
|
-
display: inline-block;
|
|
991
|
-
appearance: none;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
/* Rest of your component styles */
|
|
995
|
-
button {
|
|
996
|
-
display: inline-flex;
|
|
997
|
-
}
|
|
998
|
-
:scope[variant="primary"] {
|
|
999
|
-
--elena-button-bg: red;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
```
|
|
1003
|
-
|
|
1004
|
-
The above patterns work great for **Primitive Components** that are self-contained and own and render their own HTML markup.
|
|
1005
|
-
|
|
1006
|
-
### Elena CSS Encapsulation Pattern
|
|
1007
|
-
|
|
1008
|
-
While the [scoped styles](#writing-scoped-styles) defined earlier prevent the component styles from leaking out, it does not prevent global styles from leaking in. For this, you can use this pattern that does both and then add your own component styles below:
|
|
1009
|
-
|
|
1010
|
-
```css
|
|
1011
|
-
/* Scope makes sure styles don’t leak out */
|
|
1012
|
-
@scope (elena-button) {
|
|
1013
|
-
|
|
1014
|
-
/* Unset makes sure styles don’t leak in */
|
|
1015
|
-
:scope, *, *::before, *::after {
|
|
1016
|
-
all: unset; /* Or all: initial */
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
/* Rest of your component styles */
|
|
1020
|
-
}
|
|
1021
|
-
```
|
|
1022
|
-
|
|
1023
|
-
### Pre-hydration state and styles
|
|
1024
|
-
|
|
1025
|
-
Since **Primitive Components** are self-contained and render their own HTML markup, you may sometimes need access to more than just the initial text content pre-hydration for better SSR support to avoid layout shifts.
|
|
1026
|
-
|
|
1027
|
-
This can be achieved with pseudo elements in CSS by referencing the attributes set on the element itself:
|
|
1028
|
-
|
|
1029
|
-
```css
|
|
1030
|
-
:scope:not([hydrated])::before {
|
|
1031
|
-
content: attr(label);
|
|
1032
|
-
/* etc */
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
:scope:not([hydrated])::after {
|
|
1036
|
-
content: attr(placeholder);
|
|
1037
|
-
/* etc */
|
|
1038
|
-
}
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
For more detailed guidelines, see the [Server Side Rendering](#server-side-rendering) section.
|
|
1042
|
-
|
|
1043
|
-
> [!TIP]
|
|
1044
|
-
> You can skip this section entirely for Composite Components, when you plan to [hide components until loaded](#hide-until-loaded), or when the rest of your app renders client side only.
|
|
1045
|
-
|
|
1046
|
-
### Styling Composite Components
|
|
1047
|
-
|
|
1048
|
-
When styling **Composite Components** which wrap and enhance the HTML composed inside them, you would commonly style the host element and then provide customization with the props set on the component:
|
|
1049
|
-
|
|
1050
|
-
```css
|
|
1051
|
-
/* Scope makes sure styles don’t leak out */
|
|
1052
|
-
@scope (elena-stack) {
|
|
1053
|
-
|
|
1054
|
-
/* Targets the host element (elena-stack) */
|
|
1055
|
-
:scope {
|
|
1056
|
-
display: flex;
|
|
1057
|
-
justify-content: flex-start;
|
|
1058
|
-
align-items: flex-start;
|
|
1059
|
-
flex-flow: column wrap;
|
|
1060
|
-
flex-direction: column;
|
|
1061
|
-
gap: 0.5rem;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/* Attributes provide customization */
|
|
1065
|
-
:scope[direction="row"] {
|
|
1066
|
-
flex-direction: row;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
```
|
|
1070
|
-
|
|
1071
|
-
Notice above that you don’t have to worry about the pre-hydrated/hydrated states when styling **Composite Components** as all of their HTML lives in the `Light DOM`.
|
|
1072
|
-
|
|
1073
|
-
### Documenting public CSS properties
|
|
1074
|
-
|
|
1075
|
-
The documentation for the component’s public CSS properties lives in the component itself:
|
|
1076
|
-
|
|
1077
|
-
```js
|
|
1078
|
-
/**
|
|
1079
|
-
* The description of the component goes here.
|
|
1080
|
-
*
|
|
1081
|
-
* @cssprop [--elena-button-text] - Overrides the default text color.
|
|
1082
|
-
* @cssprop [--elena-button-bg] - Overrides the default background color.
|
|
1083
|
-
* @cssprop [--elena-button-font] - Overrides the default font-family.
|
|
1084
|
-
*/
|
|
1085
|
-
export default class Button extends Elena(HTMLElement) { /*...*/ }
|
|
1086
|
-
```
|
|
1087
|
-
|
|
1088
|
-
> [!TIP]
|
|
1089
|
-
> **`@elenajs/bundler`** transforms the above JSDocs automatically to Custom Elements Manifest which allows you to generate documentation that surfaces the component’s public CSS properties.
|
|
1090
|
-
|
|
1091
|
-
<br/>
|
|
1092
|
-
|
|
1093
|
-
## Misc
|
|
1094
|
-
|
|
1095
|
-
### Load event
|
|
1096
|
-
|
|
1097
|
-
Elena web components are self-contained and can be loaded and defined asynchronously. Therefore an element may not be interactive immediately.
|
|
1098
|
-
|
|
1099
|
-
If you set a property on an Elena web component before it has been fully initialized, it will be applied correctly and will use the values once it has finished client side hydration. However, you cannot call a method on an element before the JavaScript has been loaded.
|
|
1100
|
-
|
|
1101
|
-
Most of the time this is not an issue, as you will be calling methods through event handlers. In cases where you want to call a method as soon as possible, for example during a page load, you need to wait for the Elena web component to be defined, using `customElements.whenDefined`:
|
|
1102
|
-
|
|
1103
|
-
```html
|
|
1104
|
-
<script type="module">
|
|
1105
|
-
const button = document.querySelector("elena-button");
|
|
1106
|
-
|
|
1107
|
-
// It's fine to set props while an Elena Element is loading
|
|
1108
|
-
button.variant = "primary";
|
|
1109
|
-
|
|
1110
|
-
// But if you want to immediately call a method, you should
|
|
1111
|
-
// wait for the Elena Element to be defined
|
|
1112
|
-
await customElements.whenDefined("elena-button");
|
|
1113
|
-
button.click();
|
|
1114
|
-
</script>
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
### Hide until loaded
|
|
1118
|
-
|
|
1119
|
-
Sometimes you may want to hide your web components until they’re hydrated and interactive. You can achieve that with this small code snippet from [Scott Jehl](https://scottjehl.com/posts/web-component-self-destruct-css/):
|
|
1120
|
-
|
|
1121
|
-
```css
|
|
1122
|
-
@keyframes hideElena {
|
|
1123
|
-
0%,
|
|
1124
|
-
100% {
|
|
1125
|
-
visibility: hidden;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
:not(:defined) {
|
|
1129
|
-
animation: hideElena 2s;
|
|
1130
|
-
}
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
> [!TIP]
|
|
1134
|
-
> This CSS snippet will take care that as soon as your elements get defined, the hiding will instantly and automatically unapply. But it will also unapply itself after two seconds no matter what, should the JavaScript take that long to do its thing, or fail to run at all.
|
|
1135
|
-
|
|
1136
|
-
<br/>
|
|
1137
|
-
|
|
1138
|
-
## Known issues
|
|
1139
|
-
|
|
1140
|
-
### Browser compatibility
|
|
1141
|
-
|
|
1142
|
-
- Firefox 148 has an open issue regarding CSS `@scope` and `attr[value]` selector that we’ve [documented here](https://codepen.io/arielsalminen/full/raMazZV). This is already fixed in the pre-release build though and that should be out soon.
|
|
1143
|
-
|
|
1144
|
-
### JavaScript frameworks
|
|
1145
|
-
|
|
1146
|
-
Rules that apply to **Primitive Components** when used with a framework:
|
|
1147
|
-
|
|
1148
|
-
- Never render a framework component _inside_ a Primitive Component (e.g. via `ReactDOM.createRoot(elenaElement)`). Elena calls `replaceChildren()` on render, which would destroy the framework’s fiber tree and cause DOM corruption.
|
|
1149
|
-
- Avoid a JavaScript framework and Elena both mutating the same attribute on a Primitive Component. A framework’s reconciler would overwrite Elena’s changes on next reconcile, triggering many re-renders. Treat framework-controlled props as read-only inputs inside your Elena element’s `render()`:
|
|
1150
|
-
|
|
1151
|
-
```js
|
|
1152
|
-
// Good: framework passes text, Elena renders it
|
|
1153
|
-
<elena-button text={state.text} />
|
|
1154
|
-
|
|
1155
|
-
render() {
|
|
1156
|
-
// Good: only reads, never writes back
|
|
1157
|
-
return html`<button>${this.text}</button>`;
|
|
1158
|
-
}
|
|
1159
|
-
// Good: Elena communicates back via events, framework updates state
|
|
1160
|
-
<elena-button text={state.text} onclick={e => setState(...)} />
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
```js
|
|
1164
|
-
// Bad: Elena writes back to a framework-controlled prop
|
|
1165
|
-
render() {
|
|
1166
|
-
this.setAttribute("label", this.label.toUpperCase()); // ← don't do this
|
|
1167
|
-
}
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
- You can’t pass dynamic text content as children. Instead use the `text` property, since **Primitive Components** own their internal DOM and frameworks cannot update children after the initial Elena render:
|
|
1171
|
-
|
|
1172
|
-
```jsx
|
|
1173
|
-
// React
|
|
1174
|
-
<elena-button text={buttonText} />
|
|
1175
|
-
|
|
1176
|
-
// Angular
|
|
1177
|
-
<elena-button [text]="buttonText"></elena-button>
|
|
1178
|
-
|
|
1179
|
-
// Vue
|
|
1180
|
-
<elena-button :text="buttonText"></elena-button>
|
|
1181
|
-
```
|
|
1182
|
-
|
|
1183
|
-
> [!WARNING]
|
|
1184
|
-
> React 17 does not pass `Array` or `Object` type props or event handlers to web components correctly. Use React 18+ for proper Elena support, or pass all props as string attributes.
|
|
1185
|
-
|
|
1186
|
-
<br/>
|
|
1187
|
-
|
|
1188
|
-
## Packages
|
|
1189
|
-
|
|
1190
|
-
Elena is a monorepo containing several packages published to npm under the `@elenajs` scope:
|
|
1191
|
-
|
|
1192
|
-
- **[`@elenajs/core`](https://github.com/getelena/elena/tree/main/packages/core)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#release-candidate)
|
|
1193
|
-
- **[`@elenajs/cli`](https://github.com/getelena/elena/tree/main/packages/cli)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#release-candidate)
|
|
1194
|
-
- **[`@elenajs/bundler`](https://github.com/getelena/elena/tree/main/packages/bundler)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
|
|
1195
|
-
- **[`@elenajs/plugin-cem-define`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-define)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
|
|
1196
|
-
- **[`@elenajs/plugin-cem-tag`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-tag)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
|
|
1197
|
-
- **[`@elenajs/plugin-cem-typescript`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-typescript)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
|
|
1198
|
-
- **[`@elenajs/plugin-rollup-css`](https://github.com/getelena/elena/tree/main/packages/plugin-rollup-css)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
|
|
1199
|
-
- **[`@elenajs/components`](https://github.com/getelena/elena/tree/main/packages/components)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#alpha)
|
|
1200
|
-
- **[`@elenajs/ssr`](https://github.com/getelena/elena/tree/main/packages/ssr)** [](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#experimental)
|
|
1201
|
-
|
|
1202
|
-
<!-- https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md -->
|
|
1203
|
-
|
|
1204
|
-
<br/>
|
|
1205
|
-
|
|
1206
|
-
## Development
|
|
1207
|
-
|
|
1208
|
-
### Commands
|
|
1209
|
-
|
|
1210
|
-
All commands run from the monorepo root (`elena/`):
|
|
1211
|
-
|
|
1212
|
-
```bash
|
|
1213
|
-
pnpm install # Install dependencies
|
|
1214
|
-
pnpm build # Build all packages
|
|
1215
|
-
pnpm test # Run all tests
|
|
1216
|
-
pnpm lint # Lint with ESLint
|
|
1217
|
-
```
|
|
1218
|
-
|
|
1219
|
-
Core package commands (from `packages/core/`):
|
|
1220
|
-
|
|
1221
|
-
```bash
|
|
1222
|
-
pnpm start # Rollup watch
|
|
1223
|
-
pnpm build # Rollup build
|
|
1224
|
-
pnpm test # Vitest with coverage
|
|
1225
|
-
pnpm test:visual # Playwright visual regression tests
|
|
1226
|
-
pnpm test:visual:update # Update visual test baselines
|
|
1227
|
-
pnpm bench # Run performance benchmarks
|
|
1228
|
-
npx vitest run test/props.test.js # Run a single test file
|
|
1229
|
-
```
|
|
1230
|
-
|
|
1231
|
-
Elements dev server (from `packages/components/`):
|
|
1232
|
-
|
|
1233
|
-
```bash
|
|
1234
|
-
pnpm start # web-dev-server with live reload
|
|
1235
|
-
```
|
|
1236
|
-
|
|
1237
|
-
For more details about pull requests, commit conventions and code style, please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
1238
|
-
|
|
1239
|
-
<br/>
|
|
1240
|
-
|
|
1241
|
-
## License
|
|
1242
|
-
|
|
1243
|
-
MIT
|
|
1244
|
-
|
|
1245
|
-
<br/>
|
|
1246
|
-
|
|
1247
|
-
## Copyright
|
|
1248
|
-
|
|
1249
|
-
Copyright © 2026 [Ariel Salminen](https://arielsalminen.com)
|