atoms-element v5.0.0
git clone https://git.pyrossh.dev/atoms-element
A simple web component library for defining your custom elements. It works on both client and server.
readme.md
| b8566f5 | 1 | # atoms-element |
| b8566f5 | 2 | |
| 318927e | 3 | A simple web component library for defining your custom elements. It works on both client and server. It supports hooks and follows the same principles of react. |
| 318927e | 4 | Data props are attributes on the custom element by default so its easier to debug and functions/handlers are attached to the element. |
| b8566f5 | 5 | |
| 1269af9 | 6 | I initially started researching if it was possible to server render web components but found out not one framework supported it. I liked using |
| 9283413 | 7 | [haunted](https://github.com/matthewp/haunted) as it was react-like with hooks but was lost on how to implement server rendering. Libraries like |
| 1269af9 | 8 | JSDOM couldn't be of use since it didn't support web components and I didn't want to use puppeteer for something like this. |
| 1269af9 | 9 | |
| 1269af9 | 10 | After a year of thinking about it and researching it I found out this awesome framework [Tonic](https://github.com/optoolco/tonic). |
| 1269af9 | 11 | That was the turning point I figured out how they implemented it using a simple html parser. |
| 1269af9 | 12 | |
| 1269af9 | 13 | After going through all these libraries, |
| b8566f5 | 14 | |
| 20e01ad | 15 | 1. [lit-html](https://github.com/lit/lit) |
| 1269af9 | 16 | 2. [lit-html-server](https://github.com/popeindustries/lit-html-server) |
| 7df8e29 | 17 | 3. [haunted](https://github.com/matthewp/haunted) |
| 1269af9 | 18 | 4. [Tonic](https://github.com/optoolco/tonic) |
| 1269af9 | 19 | 5. [Atomico](https://github.com/atomicojs/atomico) |
| 1269af9 | 20 | 6. [fuco](https://github.com/wtnbass/fuco) |
| 1269af9 | 21 | |
| e955350 | 22 | And figuring out how each one implemented their on custom elements I came up with atoms-element. It now does proper rehydration: the server renders with [@lit-labs/ssr](https://github.com/lit/lit/tree/main/packages/labs/ssr), which emits hydration marker comments into the light DOM, and the client attaches to that same DOM via [@lit-labs/ssr-client](https://github.com/lit/lit/tree/main/packages/labs/ssr-client)'s `hydrate()` instead of tearing it down and re-rendering from scratch — so things like focus survive the first client render. |
| e955350 | 23 | |
| 0000000 | 24 | State lives directly in attributes rather than a separate reducer/store: `Fn.attrs = { count: Number }` declares a set of typed, required attributes up front (using the constructor itself as the type, the way Vue's props do), and `createElement` hands each one to the render function as a live `{ value }` getter/setter, validated against the declared type — mistyped or missing values throw immediately instead of failing silently. |
| b8566f5 | 25 | |
| 1269af9 | 26 | ## Example |
| b8566f5 | 27 | |
| b8566f5 | 28 | ```js |
| a4427c2 | 29 | import { classMap, createElement, css, html, renderHtml } from 'atoms-element/index.js'; |
| e955350 | 30 | |
| e955350 | 31 | const Counter = ({ name, count }) => { |
| e955350 | 32 | const increment = () => { count.value += 1; }; |
| e955350 | 33 | const decrement = () => { count.value -= 1; }; |
| a4427c2 | 34 | |
| b8566f5 | 35 | return html` |
| a4427c2 | 36 | <p>Counter: ${name}</p> |
| a4427c2 | 37 | <div class="controls"> |
| a4427c2 | 38 | <button type="button" aria-label="Decrement" @click=${decrement}>-</button> |
| a4427c2 | 39 | <output class=${classMap({ warning: count.value > 10 })}>${count.value}</output> |
| a4427c2 | 40 | <button type="button" aria-label="Increment" @click=${increment}>+</button> |
| b8566f5 | 41 | </div> |
| b8566f5 | 42 | `; |
| b8566f5 | 43 | }; |
| e955350 | 44 | Counter.attrs = { count: Number }; |
| b8566f5 | 45 | |
| a4427c2 | 46 | Counter.styles = css` |
| a4427c2 | 47 | :scope { |
| a4427c2 | 48 | display: block; |
| a4427c2 | 49 | margin-top: 2.5rem; |
| a4427c2 | 50 | color: rgb(55 65 81); |
| a4427c2 | 51 | --color-danger: rgb(239 68 68); |
| a4427c2 | 52 | --color-button-bg: rgb(209 213 219); |
| a4427c2 | 53 | --color-button-bg-hover: rgb(229 231 235); |
| a4427c2 | 54 | } |
| a4427c2 | 55 | p { |
| a4427c2 | 56 | margin: 0 0 0.5rem; |
| a4427c2 | 57 | } |
| a4427c2 | 58 | .controls { |
| a4427c2 | 59 | display: flex; |
| a4427c2 | 60 | align-items: center; |
| a4427c2 | 61 | } |
| a4427c2 | 62 | output { |
| a4427c2 | 63 | margin: 0 5rem; |
| a4427c2 | 64 | font-size: 1.875rem; |
| a4427c2 | 65 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; |
| a4427c2 | 66 | } |
| a4427c2 | 67 | output.warning { |
| a4427c2 | 68 | color: var(--color-danger); |
| a4427c2 | 69 | } |
| a4427c2 | 70 | button { |
| a4427c2 | 71 | background-color: var(--color-button-bg); |
| a4427c2 | 72 | color: inherit; |
| a4427c2 | 73 | border-radius: 0.25rem; |
| a4427c2 | 74 | padding: 0.5rem 1rem; |
| a4427c2 | 75 | font-size: 1.875rem; |
| a4427c2 | 76 | } |
| a4427c2 | 77 | button:hover { |
| a4427c2 | 78 | background-color: var(--color-button-bg-hover); |
| a4427c2 | 79 | } |
| a4427c2 | 80 | button:focus { |
| a4427c2 | 81 | outline: none; |
| a4427c2 | 82 | } |
| a4427c2 | 83 | `; |
| a4427c2 | 84 | |
| 318927e | 85 | createElement({ url: 'app-counter.js' }, Counter); |
| b8566f5 | 86 | |
| e955350 | 87 | console.log(renderHtml(html`<app-counter name="1" count="0"></app-counter>`)); |
| b8566f5 | 88 | ``` |