@pfern/elements 0.1.3 → 0.1.4
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/README.md +75 -51
- package/package.json +16 -7
- package/types/elements.d.ts +1381 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Elements.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Elements.js is a minimalist declarative UI toolkit designed around purity,
|
|
4
|
+
immutability, and HTML semantics.
|
|
4
5
|
|
|
5
6
|
## Features
|
|
6
7
|
|
|
@@ -14,7 +15,8 @@ A minimalist declarative UI toolkit designed around purity, immutability, and HT
|
|
|
14
15
|
|
|
15
16
|
## Why Elements.js?
|
|
16
17
|
|
|
17
|
-
Modern frameworks introduced declarative UI—but buried it beneath lifecycle
|
|
18
|
+
Modern frameworks introduced declarative UI—but buried it beneath lifecycle
|
|
19
|
+
hooks, mutable state, and complex diffing algorithms.
|
|
18
20
|
|
|
19
21
|
**Elements.js goes further:**
|
|
20
22
|
|
|
@@ -36,7 +38,8 @@ Yes. Elements.js proves it.
|
|
|
36
38
|
* No lifecycle methods or effects
|
|
37
39
|
* Every component is a function
|
|
38
40
|
|
|
39
|
-
To update a view: just **call the function again** with new arguments. The DOM
|
|
41
|
+
To update a view: just **call the function again** with new arguments. The DOM
|
|
42
|
+
subtree is replaced in place.
|
|
40
43
|
|
|
41
44
|
### State lives in the DOM
|
|
42
45
|
|
|
@@ -49,23 +52,22 @@ The DOM node *is the history*. Input state is passed as an argument.
|
|
|
49
52
|
* No transpilation step
|
|
50
53
|
* No reactive graph to debug
|
|
51
54
|
|
|
52
|
-
Elements.js embraces the full truth of each function call as the only valid
|
|
55
|
+
Elements.js embraces the full truth of each function call as the only valid
|
|
56
|
+
state.
|
|
53
57
|
|
|
54
58
|
---
|
|
55
59
|
|
|
56
60
|
## Example: Counter
|
|
57
61
|
|
|
58
62
|
```js
|
|
59
|
-
import {
|
|
63
|
+
import { button, component, div, output } from '@pfern/elements'
|
|
60
64
|
|
|
61
|
-
const counter = component((count = 0) =>
|
|
65
|
+
export const counter = component((count = 0) =>
|
|
62
66
|
div(
|
|
63
|
-
|
|
64
|
-
button(
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
render(counter(), document.body);
|
|
67
|
+
output(count),
|
|
68
|
+
button(
|
|
69
|
+
{ onclick: () => counter(count + 1) },
|
|
70
|
+
'Increment')))
|
|
69
71
|
```
|
|
70
72
|
|
|
71
73
|
* Each click returns a new call to `counter(count + 1)`
|
|
@@ -77,30 +79,36 @@ render(counter(), document.body);
|
|
|
77
79
|
## Form Example: Todos App
|
|
78
80
|
|
|
79
81
|
```js
|
|
80
|
-
import { button, div, component, form, input, li, span, ul } from './elements.js';
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
import { button, component, div,
|
|
84
|
+
form, input, li, span, ul } from '@pfern/elements'
|
|
85
|
+
|
|
86
|
+
export const todos = component(
|
|
87
|
+
(items = [{ value: 'Add my first todo', done: true }]) => {
|
|
88
|
+
|
|
89
|
+
const add = ({ todo: { value } }) =>
|
|
90
|
+
value && todos([...items, { value, done: false }])
|
|
91
|
+
|
|
92
|
+
const remove = item =>
|
|
93
|
+
todos(items.filter(i => i !== item))
|
|
94
|
+
|
|
95
|
+
const toggle = item =>
|
|
96
|
+
todos(items.map(i => i === item ? { ...i, done: !item.done } : i))
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
div({ class: 'todos' },
|
|
100
|
+
|
|
101
|
+
form({ onsubmit: add },
|
|
102
|
+
input({ name: 'todo', placeholder: 'What needs doing?' }),
|
|
103
|
+
button({ type: 'submit' }, 'Add')),
|
|
104
|
+
|
|
105
|
+
ul(...items.map(item =>
|
|
106
|
+
li(
|
|
107
|
+
{ style:
|
|
108
|
+
{ 'text-decoration': item.done ? 'line-through' : 'none' } },
|
|
109
|
+
span({ onclick: () => toggle(item) }, item.value),
|
|
110
|
+
button({ onclick: () => remove(item) }, '✕'))))))})
|
|
85
111
|
|
|
86
|
-
const remove = item =>
|
|
87
|
-
todos(items.filter(i => i !== item))
|
|
88
|
-
|
|
89
|
-
const toggle = item =>
|
|
90
|
-
todos(items.map(i => i === item ? { ...i, done: !item.done } : i))
|
|
91
|
-
|
|
92
|
-
return div({ class: 'todos' },
|
|
93
|
-
form({ onsubmit: add },
|
|
94
|
-
input({ name: 'todo', placeholder: 'What needs doing?' }),
|
|
95
|
-
button({ type: 'submit' }, 'Add')),
|
|
96
|
-
ul(...items.map(item =>
|
|
97
|
-
li({ style: { 'text-decoration': item.done ? 'line-through' : 'none' } },
|
|
98
|
-
span({ onclick: () => toggle(item) }, item.value),
|
|
99
|
-
button({ onclick: () => remove(item) }, '✕')
|
|
100
|
-
))
|
|
101
|
-
)
|
|
102
|
-
)
|
|
103
|
-
})
|
|
104
112
|
```
|
|
105
113
|
|
|
106
114
|
This is a complete MVC-style app:
|
|
@@ -109,11 +117,17 @@ This is a complete MVC-style app:
|
|
|
109
117
|
* Immutable
|
|
110
118
|
* Pure
|
|
111
119
|
|
|
120
|
+
You can view these examples live on [Github
|
|
121
|
+
Pages](https://pfernandez.github.io/elements/) or by running them locally with
|
|
122
|
+
`npm run dev`.
|
|
123
|
+
|
|
112
124
|
---
|
|
113
125
|
|
|
114
126
|
## Root Rendering Shortcut
|
|
115
127
|
|
|
116
|
-
If you use `html`, `head`, or `body` as the top-level tag, `render()` will
|
|
128
|
+
If you use `html`, `head`, or `body` as the top-level tag, `render()` will
|
|
129
|
+
automatically mount into the corresponding document element—no need to pass a
|
|
130
|
+
container.
|
|
117
131
|
|
|
118
132
|
```js
|
|
119
133
|
import {
|
|
@@ -126,7 +140,8 @@ render(
|
|
|
126
140
|
html(
|
|
127
141
|
head(
|
|
128
142
|
title('Elements.js'),
|
|
129
|
-
meta({ name: 'viewport',
|
|
143
|
+
meta({ name: 'viewport',
|
|
144
|
+
content: 'width=device-width, initial-scale=1.0' }),
|
|
130
145
|
link({ rel: 'stylesheet', href: 'css/style.css' })
|
|
131
146
|
),
|
|
132
147
|
body(
|
|
@@ -134,29 +149,29 @@ render(
|
|
|
134
149
|
main(
|
|
135
150
|
section(
|
|
136
151
|
h2('Todos'),
|
|
137
|
-
todos()
|
|
138
|
-
)
|
|
139
|
-
)
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
|
-
)
|
|
152
|
+
todos())))))
|
|
143
153
|
```
|
|
144
154
|
|
|
145
155
|
---
|
|
146
156
|
|
|
147
157
|
## Declarative Events
|
|
148
158
|
|
|
149
|
-
All event listeners in Elements.js are pure functions. You can return a vnode
|
|
159
|
+
All event listeners in Elements.js are pure functions. You can return a vnode
|
|
160
|
+
from a listener to declaratively update the component tree—- no mutation or
|
|
161
|
+
imperative logic required.
|
|
150
162
|
|
|
151
163
|
### General Behavior
|
|
152
164
|
|
|
153
|
-
* Any event handler (e.g. `onclick`, `onsubmit`, `oninput`) may return a new
|
|
154
|
-
|
|
165
|
+
* Any event handler (e.g. `onclick`, `onsubmit`, `oninput`) may return a new
|
|
166
|
+
vnode to trigger a subtree replacement.
|
|
167
|
+
* If the handler returns `undefined`, the event is treated as passive (no update
|
|
168
|
+
occurs).
|
|
155
169
|
* Returned vnodes are passed to `component()` to re-render declaratively.
|
|
156
170
|
|
|
157
171
|
### Form Events
|
|
158
172
|
|
|
159
|
-
For `onsubmit`, `oninput`, and `onchange`, Elements.js provides a special
|
|
173
|
+
For `onsubmit`, `oninput`, and `onchange`, Elements.js provides a special
|
|
174
|
+
signature:
|
|
160
175
|
|
|
161
176
|
```js
|
|
162
177
|
(event.target.elements, event)
|
|
@@ -167,7 +182,8 @@ That is, your handler receives:
|
|
|
167
182
|
1. `elements`: the HTML form’s named inputs
|
|
168
183
|
2. `event`: the original DOM event object
|
|
169
184
|
|
|
170
|
-
Elements.js will automatically call `event.preventDefault()` *only if* your
|
|
185
|
+
Elements.js will automatically call `event.preventDefault()` *only if* your
|
|
186
|
+
handler returns a vnode.
|
|
171
187
|
|
|
172
188
|
```js
|
|
173
189
|
form({
|
|
@@ -176,7 +192,8 @@ form({
|
|
|
176
192
|
})
|
|
177
193
|
```
|
|
178
194
|
|
|
179
|
-
If the handler returns nothing, `preventDefault()` is skipped and the form
|
|
195
|
+
If the handler returns nothing, `preventDefault()` is skipped and the form
|
|
196
|
+
submits natively.
|
|
180
197
|
|
|
181
198
|
---
|
|
182
199
|
|
|
@@ -188,7 +205,8 @@ Wrap a recursive pure function that returns a vnode.
|
|
|
188
205
|
|
|
189
206
|
### `render(vnode[, container])`
|
|
190
207
|
|
|
191
|
-
Render a vnode into the DOM. If `vnode[0]` is `html`, `head`, or `body`, no
|
|
208
|
+
Render a vnode into the DOM. If `vnode[0]` is `html`, `head`, or `body`, no
|
|
209
|
+
`container` is required.
|
|
192
210
|
|
|
193
211
|
### DOM Elements
|
|
194
212
|
|
|
@@ -201,17 +219,23 @@ svg({ width: 100 }, circle({ r: 10 }))
|
|
|
201
219
|
|
|
202
220
|
### TypeScript & JSDoc
|
|
203
221
|
|
|
204
|
-
Each tag function (e.g. `div`, `button`, `svg`) includes a `@typedef` and
|
|
222
|
+
Each tag function (e.g. `div`, `button`, `svg`) includes a `@typedef` and
|
|
223
|
+
MDN-sourced description to:
|
|
205
224
|
|
|
206
225
|
* Provide editor hints
|
|
207
226
|
* Encourage accessibility and semantic markup
|
|
208
227
|
* Enable intelligent autocomplete
|
|
209
228
|
|
|
229
|
+
### Testing Philosophy
|
|
230
|
+
|
|
231
|
+
Elements are data-in, data-out only, so mocking and headless browsers like
|
|
232
|
+
`jsdom` are unnecessary out of the box. See the tests [in this
|
|
233
|
+
repository](test/README.md) for some examples.
|
|
234
|
+
|
|
210
235
|
---
|
|
211
236
|
|
|
212
237
|
## Status
|
|
213
238
|
|
|
214
|
-
* ✅ Production-ready core
|
|
215
239
|
* 🧪 Fully tested (data-in/data-out behavior)
|
|
216
240
|
* ⚡ Under 2kB min+gzip
|
|
217
241
|
* ✅ Node and browser compatible
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pfern/elements",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A minimalist, pure functional declarative UI toolkit.",
|
|
5
|
-
"main": "elements.js",
|
|
6
5
|
"type": "module",
|
|
6
|
+
"main": "elements.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
11
|
+
"default": "./elements.js"
|
|
12
|
+
}
|
|
9
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"./elements.js",
|
|
16
|
+
"./types"
|
|
17
|
+
],
|
|
10
18
|
"keywords": [
|
|
11
19
|
"ui",
|
|
12
20
|
"functional",
|
|
@@ -19,20 +27,21 @@
|
|
|
19
27
|
"html"
|
|
20
28
|
],
|
|
21
29
|
"scripts": {
|
|
22
|
-
"dev": "vite
|
|
30
|
+
"dev": "vite",
|
|
23
31
|
"build": "vite build",
|
|
24
32
|
"preview": "vite preview",
|
|
25
33
|
"test": "node --test test/*.test.* --test-reporter spec"
|
|
26
34
|
},
|
|
27
35
|
"repository": {
|
|
28
36
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/pfernandez/elements.git"
|
|
37
|
+
"url": "git+https://github.com/pfernandez/elements.git"
|
|
30
38
|
},
|
|
31
39
|
"author": "Paul Fernandez",
|
|
32
40
|
"license": "MIT",
|
|
33
41
|
"devDependencies": {
|
|
42
|
+
"@types/node": "^25.0.10",
|
|
34
43
|
"eslint": "^7.32.0",
|
|
35
|
-
"
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vite": "^7.3.1"
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
|
-
|