@pfern/elements 0.1.2 → 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/elements.js +215 -62
- 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/elements.js
CHANGED
|
@@ -61,14 +61,14 @@ const diffTree = (a, b) => {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
64
|
/**
|
|
66
65
|
* Compares the children of two vnodes and returns patch list.
|
|
67
66
|
*
|
|
68
67
|
* @param {Array} aChildren - Previous vnode children
|
|
69
68
|
* @param {Array} bChildren - New vnode children
|
|
70
69
|
* @returns {Array} patches - One per child node
|
|
71
|
-
*/
|
|
70
|
+
*/
|
|
71
|
+
const diffChildren = (aChildren, bChildren) => {
|
|
72
72
|
const patches = []
|
|
73
73
|
const len = Math.max(aChildren.length, bChildren.length)
|
|
74
74
|
for (let i = 0; i < len; i++) {
|
|
@@ -85,6 +85,8 @@ const diffTree = (a, b) => {
|
|
|
85
85
|
* *if* the listener returns a vnode (to support declarative form updates).
|
|
86
86
|
* - Handlers for these event types receive `(elements, event)` as arguments,
|
|
87
87
|
* where `elements` is `event.target.elements` if available.
|
|
88
|
+
* - Async handlers are supported: if the listener returns a Promise,
|
|
89
|
+
* it will be awaited and the resulting vnode (if any) will be rendered.
|
|
88
90
|
*
|
|
89
91
|
* @param {HTMLElement} el - The DOM element to receive props
|
|
90
92
|
* @param {Object} props - Attributes and event listeners to assign
|
|
@@ -92,7 +94,7 @@ const diffTree = (a, b) => {
|
|
|
92
94
|
const assignProperties = (el, props) =>
|
|
93
95
|
Object.entries(props).forEach(([key, value]) => {
|
|
94
96
|
if (key.startsWith('on') && typeof value === 'function') {
|
|
95
|
-
el[key] = (...args) => {
|
|
97
|
+
el[key] = async (...args) => {
|
|
96
98
|
let target = el
|
|
97
99
|
while (target && !target.__root) target = target.parentNode
|
|
98
100
|
if (!target) return
|
|
@@ -102,9 +104,9 @@ const assignProperties = (el, props) =>
|
|
|
102
104
|
const isFormEvent = /^(oninput|onsubmit|onchange)$/.test(key)
|
|
103
105
|
const elements = isFormEvent && event?.target?.elements || null
|
|
104
106
|
|
|
105
|
-
const result = isFormEvent
|
|
107
|
+
const result = await (isFormEvent
|
|
106
108
|
? value.call(el, elements, event)
|
|
107
|
-
: value.call(el, event)
|
|
109
|
+
: value.call(el, event))
|
|
108
110
|
|
|
109
111
|
if (isFormEvent && result !== undefined) {
|
|
110
112
|
event.preventDefault()
|
|
@@ -119,11 +121,12 @@ const assignProperties = (el, props) =>
|
|
|
119
121
|
|
|
120
122
|
if (DEBUG && result !== undefined && !Array.isArray(result)) {
|
|
121
123
|
isFormEvent && event.preventDefault()
|
|
122
|
-
DEBUG
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
DEBUG
|
|
125
|
+
&& console.warn(
|
|
126
|
+
`Listener '${key}' on <${el.tagName.toLowerCase()}> returned "${result}".\n`
|
|
127
|
+
+ 'If you intended a UI update, return a vnode array like: div({}, ...).\n'
|
|
128
|
+
+ 'Otherwise, return undefined (or nothing) for native event listener behavior.'
|
|
129
|
+
)
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
if (Array.isArray(result)) {
|
|
@@ -153,8 +156,10 @@ const assignProperties = (el, props) =>
|
|
|
153
156
|
el.setAttribute(key, value)
|
|
154
157
|
}
|
|
155
158
|
} catch {
|
|
156
|
-
DEBUG
|
|
157
|
-
|
|
159
|
+
DEBUG
|
|
160
|
+
&& console.warn(
|
|
161
|
+
`Illegal DOM property assignment for ${el.tagName}: ${key}: ${value}`
|
|
162
|
+
)
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
})
|
|
@@ -194,9 +199,12 @@ const renderTree = (node, isRoot = true) => {
|
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
let el =
|
|
197
|
-
tag === 'html'
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
tag === 'html'
|
|
203
|
+
? document.documentElement
|
|
204
|
+
: tag === 'head'
|
|
205
|
+
? document.head
|
|
206
|
+
: tag === 'body'
|
|
207
|
+
? document.body
|
|
200
208
|
: svgTagNames.includes(tag)
|
|
201
209
|
? document.createElementNS(svgNS, tag)
|
|
202
210
|
: document.createElement(tag)
|
|
@@ -311,88 +319,232 @@ export const component = fn => {
|
|
|
311
319
|
|
|
312
320
|
const htmlTagNames = [
|
|
313
321
|
// Document metadata
|
|
314
|
-
'html',
|
|
322
|
+
'html',
|
|
323
|
+
'head',
|
|
324
|
+
'base',
|
|
325
|
+
'link',
|
|
326
|
+
'meta',
|
|
327
|
+
'title',
|
|
315
328
|
|
|
316
329
|
// Sections
|
|
317
|
-
'body',
|
|
318
|
-
'
|
|
330
|
+
'body',
|
|
331
|
+
'header',
|
|
332
|
+
'hgroup',
|
|
333
|
+
'nav',
|
|
334
|
+
'main',
|
|
335
|
+
'section',
|
|
336
|
+
'article',
|
|
337
|
+
'aside',
|
|
338
|
+
'footer',
|
|
339
|
+
'address',
|
|
319
340
|
|
|
320
341
|
// Text content
|
|
321
|
-
'h1',
|
|
322
|
-
'
|
|
342
|
+
'h1',
|
|
343
|
+
'h2',
|
|
344
|
+
'h3',
|
|
345
|
+
'h4',
|
|
346
|
+
'h5',
|
|
347
|
+
'h6',
|
|
348
|
+
'p',
|
|
349
|
+
'hr',
|
|
350
|
+
'menu',
|
|
351
|
+
'pre',
|
|
352
|
+
'blockquote',
|
|
353
|
+
'ol',
|
|
354
|
+
'ul',
|
|
355
|
+
'li',
|
|
356
|
+
'dl',
|
|
357
|
+
'dt',
|
|
358
|
+
'dd',
|
|
359
|
+
'figure',
|
|
360
|
+
'figcaption',
|
|
323
361
|
'div',
|
|
324
362
|
|
|
325
363
|
// Inline text semantics
|
|
326
|
-
'a',
|
|
327
|
-
'
|
|
328
|
-
'
|
|
329
|
-
'
|
|
364
|
+
'a',
|
|
365
|
+
'abbr',
|
|
366
|
+
'b',
|
|
367
|
+
'bdi',
|
|
368
|
+
'bdo',
|
|
369
|
+
'br',
|
|
370
|
+
'cite',
|
|
371
|
+
'code',
|
|
372
|
+
'data',
|
|
373
|
+
'dfn',
|
|
374
|
+
'em',
|
|
375
|
+
'i',
|
|
376
|
+
'kbd',
|
|
377
|
+
'mark',
|
|
378
|
+
'q',
|
|
379
|
+
'rb',
|
|
380
|
+
'rp',
|
|
381
|
+
'rt',
|
|
382
|
+
'rtc',
|
|
383
|
+
'ruby',
|
|
384
|
+
's',
|
|
385
|
+
'samp',
|
|
386
|
+
'small',
|
|
387
|
+
'span',
|
|
388
|
+
'strong',
|
|
389
|
+
'sub',
|
|
390
|
+
'sup',
|
|
391
|
+
'time',
|
|
392
|
+
'u',
|
|
393
|
+
'var',
|
|
394
|
+
'wbr',
|
|
330
395
|
|
|
331
396
|
// Edits
|
|
332
|
-
'ins',
|
|
397
|
+
'ins',
|
|
398
|
+
'del',
|
|
333
399
|
|
|
334
400
|
// Embedded content
|
|
335
|
-
'img',
|
|
336
|
-
'
|
|
401
|
+
'img',
|
|
402
|
+
'iframe',
|
|
403
|
+
'embed',
|
|
404
|
+
'object',
|
|
405
|
+
'param',
|
|
406
|
+
'video',
|
|
407
|
+
'audio',
|
|
408
|
+
'source',
|
|
409
|
+
'track',
|
|
410
|
+
'picture',
|
|
337
411
|
|
|
338
412
|
// Table content
|
|
339
|
-
'table',
|
|
340
|
-
'
|
|
413
|
+
'table',
|
|
414
|
+
'caption',
|
|
415
|
+
'thead',
|
|
416
|
+
'tbody',
|
|
417
|
+
'tfoot',
|
|
418
|
+
'tr',
|
|
419
|
+
'th',
|
|
420
|
+
'td',
|
|
421
|
+
'colgroup',
|
|
422
|
+
'col',
|
|
341
423
|
|
|
342
424
|
// Forms
|
|
343
|
-
'form',
|
|
344
|
-
'
|
|
345
|
-
'
|
|
425
|
+
'form',
|
|
426
|
+
'fieldset',
|
|
427
|
+
'legend',
|
|
428
|
+
'label',
|
|
429
|
+
'input',
|
|
430
|
+
'button',
|
|
431
|
+
'select',
|
|
432
|
+
'datalist',
|
|
433
|
+
'optgroup',
|
|
434
|
+
'option',
|
|
435
|
+
'textarea',
|
|
436
|
+
'output',
|
|
437
|
+
'progress',
|
|
438
|
+
'meter',
|
|
346
439
|
|
|
347
440
|
// Interactive elements
|
|
348
|
-
'details',
|
|
441
|
+
'details',
|
|
442
|
+
'search',
|
|
443
|
+
'summary',
|
|
444
|
+
'dialog',
|
|
445
|
+
'slot',
|
|
446
|
+
'template',
|
|
349
447
|
|
|
350
448
|
// Scripting and style
|
|
351
|
-
'script',
|
|
449
|
+
'script',
|
|
450
|
+
'noscript',
|
|
451
|
+
'style',
|
|
352
452
|
|
|
353
453
|
// Web components and others
|
|
354
|
-
'canvas',
|
|
454
|
+
'canvas',
|
|
455
|
+
'picture',
|
|
456
|
+
'map',
|
|
457
|
+
'area',
|
|
458
|
+
'slot'
|
|
355
459
|
]
|
|
356
460
|
|
|
357
461
|
const svgTagNames = [
|
|
358
462
|
// Animation elements
|
|
359
|
-
'a',
|
|
463
|
+
'a',
|
|
464
|
+
'animate',
|
|
465
|
+
'animateMotion',
|
|
466
|
+
'animateTransform',
|
|
467
|
+
'mpath',
|
|
468
|
+
'set',
|
|
360
469
|
|
|
361
470
|
// Basic shapes
|
|
362
|
-
'circle',
|
|
471
|
+
'circle',
|
|
472
|
+
'ellipse',
|
|
473
|
+
'line',
|
|
474
|
+
'path',
|
|
475
|
+
'polygon',
|
|
476
|
+
'polyline',
|
|
477
|
+
'rect',
|
|
363
478
|
|
|
364
479
|
// Container / structural
|
|
365
|
-
'defs',
|
|
480
|
+
'defs',
|
|
481
|
+
'g',
|
|
482
|
+
'marker',
|
|
483
|
+
'mask',
|
|
484
|
+
'pattern',
|
|
485
|
+
'svg',
|
|
486
|
+
'switch',
|
|
487
|
+
'symbol',
|
|
488
|
+
'use',
|
|
366
489
|
|
|
367
490
|
// Descriptive
|
|
368
|
-
'desc',
|
|
491
|
+
'desc',
|
|
492
|
+
'metadata',
|
|
493
|
+
'title',
|
|
369
494
|
|
|
370
495
|
// Filter primitives
|
|
371
|
-
'filter',
|
|
372
|
-
'
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
'
|
|
376
|
-
'
|
|
496
|
+
'filter',
|
|
497
|
+
'feBlend',
|
|
498
|
+
'feColorMatrix',
|
|
499
|
+
'feComponentTransfer',
|
|
500
|
+
'feComposite',
|
|
501
|
+
'feConvolveMatrix',
|
|
502
|
+
'feDiffuseLighting',
|
|
503
|
+
'feDisplacementMap',
|
|
504
|
+
'feDistantLight',
|
|
505
|
+
'feDropShadow',
|
|
506
|
+
'feFlood',
|
|
507
|
+
'feFuncA',
|
|
508
|
+
'feFuncB',
|
|
509
|
+
'feFuncG',
|
|
510
|
+
'feFuncR',
|
|
511
|
+
'feGaussianBlur',
|
|
512
|
+
'feImage',
|
|
513
|
+
'feMerge',
|
|
514
|
+
'feMergeNode',
|
|
515
|
+
'feMorphology',
|
|
516
|
+
'feOffset',
|
|
517
|
+
'fePointLight',
|
|
518
|
+
'feSpecularLighting',
|
|
519
|
+
'feSpotLight',
|
|
520
|
+
'feTile',
|
|
521
|
+
'feTurbulence',
|
|
377
522
|
|
|
378
523
|
// Gradient / paint servers
|
|
379
|
-
'linearGradient',
|
|
524
|
+
'linearGradient',
|
|
525
|
+
'radialGradient',
|
|
526
|
+
'stop',
|
|
380
527
|
|
|
381
528
|
// Graphics elements
|
|
382
|
-
'image',
|
|
529
|
+
'image',
|
|
530
|
+
'foreignObject', // included in graphics section as non‑standard children
|
|
383
531
|
|
|
384
532
|
// Text and text-path
|
|
385
|
-
'text',
|
|
533
|
+
'text',
|
|
534
|
+
'textPath',
|
|
535
|
+
'tspan',
|
|
386
536
|
|
|
387
537
|
// Scripting/style
|
|
388
|
-
'script',
|
|
538
|
+
'script',
|
|
539
|
+
'style',
|
|
389
540
|
|
|
390
541
|
// View
|
|
391
542
|
'view'
|
|
392
543
|
]
|
|
393
544
|
|
|
394
545
|
const tagNames = [...htmlTagNames, ...svgTagNames]
|
|
395
|
-
const isPropsObject = x =>
|
|
546
|
+
const isPropsObject = x =>
|
|
547
|
+
typeof x === 'object'
|
|
396
548
|
&& x !== null
|
|
397
549
|
&& !Array.isArray(x)
|
|
398
550
|
&& !(typeof Node !== 'undefined' && x instanceof Node)
|
|
@@ -422,18 +574,20 @@ const isPropsObject = x => typeof x === 'object'
|
|
|
422
574
|
*
|
|
423
575
|
* @type {Record<string, ElementHelper>}
|
|
424
576
|
*/
|
|
425
|
-
export const elements = tagNames.reduce(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
577
|
+
export const elements = tagNames.reduce(
|
|
578
|
+
(acc, tag) => ({
|
|
579
|
+
...acc,
|
|
580
|
+
[tag]: (propsOrChild, ...children) => {
|
|
581
|
+
const props = isPropsObject(propsOrChild) ? propsOrChild : {}
|
|
582
|
+
const actualChildren =
|
|
583
|
+
props === propsOrChild ? children : [propsOrChild, ...children]
|
|
584
|
+
return [tag, props, ...actualChildren]
|
|
585
|
+
}
|
|
586
|
+
}),
|
|
587
|
+
{
|
|
588
|
+
fragment: (...children) => ['fragment', {}, ...children]
|
|
433
589
|
}
|
|
434
|
-
|
|
435
|
-
fragment: (...children) => ['fragment', {}, ...children]
|
|
436
|
-
})
|
|
590
|
+
)
|
|
437
591
|
|
|
438
592
|
/**
|
|
439
593
|
* <html>
|
|
@@ -1928,4 +2082,3 @@ export const view = elements.view
|
|
|
1928
2082
|
|
|
1929
2083
|
// TODO: MathML
|
|
1930
2084
|
// https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element
|
|
1931
|
-
|
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
|
-
|