@pfern/create-elements 0.0.1
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 +23 -0
- package/index.js +50 -0
- package/package.json +24 -0
- package/template/.eslintrc.json +205 -0
- package/template/README.md +279 -0
- package/template/gitignore +2 -0
- package/template/index.html +4 -0
- package/template/package-lock.json +2177 -0
- package/template/package.json +28 -0
- package/template/src/components/counter.js +9 -0
- package/template/src/components/todos.js +29 -0
- package/template/src/index.js +27 -0
- package/template/src/style.css +95 -0
- package/template/test/README.md +77 -0
- package/template/test/elements.test.js +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# create-elements
|
|
2
|
+
|
|
3
|
+
A CLI package to generate a minimal template project with Elements.js.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
```js
|
|
9
|
+
npm install -g \@pfern/create-elements
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Usage
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm \@pfern/create-elements [project-name]
|
|
16
|
+
|
|
17
|
+
# Enter a project name if none was provided
|
|
18
|
+
|
|
19
|
+
cd project-name
|
|
20
|
+
npm install
|
|
21
|
+
npm run dev
|
|
22
|
+
```
|
|
23
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
async function init() {
|
|
12
|
+
let projectName = process.argv[2];
|
|
13
|
+
|
|
14
|
+
if (!projectName) {
|
|
15
|
+
const rl = readline.createInterface({ input, output });
|
|
16
|
+
projectName = await rl.question('Project name: (elements-app) ');
|
|
17
|
+
rl.close();
|
|
18
|
+
|
|
19
|
+
projectName = projectName.trim() || 'elements-app';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
23
|
+
const templateDir = path.resolve(__dirname, 'template');
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
26
|
+
console.error(`✖ Error: Directory "${projectName}" is not empty.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`\nScaffolding project in ${targetDir}...`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
fs.cpSync(templateDir, targetDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const gitignorePath = path.join(targetDir, 'gitignore');
|
|
36
|
+
if (fs.existsSync(gitignorePath)) {
|
|
37
|
+
fs.renameSync(gitignorePath, path.join(targetDir, '.gitignore'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n✔ Done! To get started:`);
|
|
41
|
+
console.log(` cd ${projectName}`);
|
|
42
|
+
console.log(` npm install\n`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('✖ Error copying template:', err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
init().catch(console.error);
|
|
50
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pfern/create-elements",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A CLI script to generate a starter app with elements.js",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ui",
|
|
8
|
+
"functional",
|
|
9
|
+
"declarative",
|
|
10
|
+
"frontend",
|
|
11
|
+
"vdom",
|
|
12
|
+
"jsx-free",
|
|
13
|
+
"stateless",
|
|
14
|
+
"recursive",
|
|
15
|
+
"html"
|
|
16
|
+
],
|
|
17
|
+
"author": "Paul Fernandez",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"create-elements": "./index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": ["index.js", "template/"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
{ "root": true,
|
|
2
|
+
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
|
|
3
|
+
"env": { "browser": true, "es2021": true },
|
|
4
|
+
|
|
5
|
+
"rules": {
|
|
6
|
+
"array-bracket-newline": [ "warn", "consistent" ],
|
|
7
|
+
"arrow-parens": [ "warn", "as-needed" ],
|
|
8
|
+
"arrow-spacing": "warn",
|
|
9
|
+
"comma-spacing": [ "warn", { "before": false, "after": true } ],
|
|
10
|
+
"comma-style": [ "warn", "last" ],
|
|
11
|
+
"computed-property-spacing": [ "warn", "never" ],
|
|
12
|
+
"eol-last": [ "warn", "always" ],
|
|
13
|
+
"indent": [ "warn", 2, {
|
|
14
|
+
"ImportDeclaration": "first",
|
|
15
|
+
"ObjectExpression": "first",
|
|
16
|
+
"VariableDeclarator": {
|
|
17
|
+
"var": 2,
|
|
18
|
+
"let": 2,
|
|
19
|
+
"const": 3 } } ],
|
|
20
|
+
"key-spacing": [ "warn", { "mode": "minimum" } ],
|
|
21
|
+
"linebreak-style": [ "error", "unix" ],
|
|
22
|
+
"no-extra-parens": "warn",
|
|
23
|
+
"no-multiple-empty-lines": [ "warn", { "max": 2, "maxEOF": 1 } ],
|
|
24
|
+
"no-multi-spaces": [ "warn", {
|
|
25
|
+
"ignoreEOLComments": true,
|
|
26
|
+
"exceptions": { "Property": false } } ],
|
|
27
|
+
"no-trailing-spaces": "warn",
|
|
28
|
+
"no-undef": "warn",
|
|
29
|
+
"no-unused-vars": [ "warn", {
|
|
30
|
+
"args": "all",
|
|
31
|
+
"varsIgnorePattern": "^_",
|
|
32
|
+
"argsIgnorePattern": "^_" } ],
|
|
33
|
+
|
|
34
|
+
"object-curly-newline": [ "warn", { "consistent": true } ],
|
|
35
|
+
"object-curly-spacing": [ "warn", "always" ],
|
|
36
|
+
"operator-linebreak": [ "warn", "before", {
|
|
37
|
+
"overrides": {
|
|
38
|
+
"=": "ignore" } } ],
|
|
39
|
+
"prefer-rest-params": "warn",
|
|
40
|
+
"quote-props": [ "warn", "as-needed" ],
|
|
41
|
+
"quotes": [ "warn", "single" ],
|
|
42
|
+
"semi": [ "warn", "never" ],
|
|
43
|
+
"sort-imports": "warn",
|
|
44
|
+
"space-before-function-paren": [ "warn", {
|
|
45
|
+
"anonymous": "never",
|
|
46
|
+
"asyncArrow": "always",
|
|
47
|
+
"named": "never" } ],
|
|
48
|
+
"spaced-comment": [ "warn", "always" ],
|
|
49
|
+
"space-in-parens": "warn",
|
|
50
|
+
"space-infix-ops": "warn" },
|
|
51
|
+
|
|
52
|
+
"globals": {
|
|
53
|
+
"process": "readonly",
|
|
54
|
+
"test": "readonly",
|
|
55
|
+
|
|
56
|
+
"apply": "readonly",
|
|
57
|
+
"bool": "readonly",
|
|
58
|
+
"deepMap": "readonly",
|
|
59
|
+
"each": "readonly",
|
|
60
|
+
"entries": "readonly",
|
|
61
|
+
"eq": "readonly",
|
|
62
|
+
"evaluate": "readonly",
|
|
63
|
+
"exists": "readonly",
|
|
64
|
+
"filter": "readonly",
|
|
65
|
+
"first": "readonly",
|
|
66
|
+
"globalize": "readonly",
|
|
67
|
+
"identity": "readonly",
|
|
68
|
+
"instance": "readonly",
|
|
69
|
+
"isArray": "readonly",
|
|
70
|
+
"isFunction": "readonly",
|
|
71
|
+
"join": "readonly",
|
|
72
|
+
"keys": "readonly",
|
|
73
|
+
"last": "readonly",
|
|
74
|
+
"length": "readonly",
|
|
75
|
+
"log": "readonly",
|
|
76
|
+
"omap": "readonly",
|
|
77
|
+
"omit": "readonly",
|
|
78
|
+
"partial": "readonly",
|
|
79
|
+
"push": "readonly",
|
|
80
|
+
"rest": "readonly",
|
|
81
|
+
"reverse": "readonly",
|
|
82
|
+
"slice": "readonly",
|
|
83
|
+
"split": "readonly",
|
|
84
|
+
"store": "readonly",
|
|
85
|
+
"sum": "readonly",
|
|
86
|
+
"type": "readonly",
|
|
87
|
+
"walk": "readonly",
|
|
88
|
+
|
|
89
|
+
"a": "readonly",
|
|
90
|
+
"abbr": "readonly",
|
|
91
|
+
"address": "readonly",
|
|
92
|
+
"area": "readonly",
|
|
93
|
+
"article": "readonly",
|
|
94
|
+
"aside": "readonly",
|
|
95
|
+
"audio": "readonly",
|
|
96
|
+
"b": "readonly",
|
|
97
|
+
"base": "readonly",
|
|
98
|
+
"bdi": "readonly",
|
|
99
|
+
"bdo": "readonly",
|
|
100
|
+
"blockquote": "readonly",
|
|
101
|
+
"body": "readonly",
|
|
102
|
+
"br": "readonly",
|
|
103
|
+
"button": "readonly",
|
|
104
|
+
"canvas": "readonly",
|
|
105
|
+
"caption": "readonly",
|
|
106
|
+
"cite": "readonly",
|
|
107
|
+
"code": "readonly",
|
|
108
|
+
"col": "readonly",
|
|
109
|
+
"colgroup": "readonly",
|
|
110
|
+
"data": "readonly",
|
|
111
|
+
"datalist": "readonly",
|
|
112
|
+
"dd": "readonly",
|
|
113
|
+
"del": "readonly",
|
|
114
|
+
"details": "readonly",
|
|
115
|
+
"dfn": "readonly",
|
|
116
|
+
"dialog": "readonly",
|
|
117
|
+
"div": "readonly",
|
|
118
|
+
"dl": "readonly",
|
|
119
|
+
"doctype": "readonly",
|
|
120
|
+
"dt": "readonly",
|
|
121
|
+
"el": "readonly",
|
|
122
|
+
"element": "readonly",
|
|
123
|
+
"em": "readonly",
|
|
124
|
+
"embed": "readonly",
|
|
125
|
+
"fieldset": "readonly",
|
|
126
|
+
"figcaption": "readonly",
|
|
127
|
+
"figure": "readonly",
|
|
128
|
+
"footer": "readonly",
|
|
129
|
+
"form": "readonly",
|
|
130
|
+
"fragment": "readonly",
|
|
131
|
+
"h1": "readonly",
|
|
132
|
+
"h2": "readonly",
|
|
133
|
+
"h3": "readonly",
|
|
134
|
+
"h4": "readonly",
|
|
135
|
+
"h5": "readonly",
|
|
136
|
+
"h6": "readonly",
|
|
137
|
+
"head": "readonly",
|
|
138
|
+
"header": "readonly",
|
|
139
|
+
"hgroup": "readonly",
|
|
140
|
+
"hr": "readonly",
|
|
141
|
+
"html": "readonly",
|
|
142
|
+
"i": "readonly",
|
|
143
|
+
"iframe": "readonly",
|
|
144
|
+
"img": "readonly",
|
|
145
|
+
"input": "readonly",
|
|
146
|
+
"ins": "readonly",
|
|
147
|
+
"kbd": "readonly",
|
|
148
|
+
"label": "readonly",
|
|
149
|
+
"legend": "readonly",
|
|
150
|
+
"li": "readonly",
|
|
151
|
+
"link": "readonly",
|
|
152
|
+
"main": "readonly",
|
|
153
|
+
"map": "readonly",
|
|
154
|
+
"mark": "readonly",
|
|
155
|
+
"menu": "readonly",
|
|
156
|
+
"meta": "readonly",
|
|
157
|
+
"meter": "readonly",
|
|
158
|
+
"nav": "readonly",
|
|
159
|
+
"noscript": "readonly",
|
|
160
|
+
"object": "readonly",
|
|
161
|
+
"ol": "readonly",
|
|
162
|
+
"optgroup": "readonly",
|
|
163
|
+
"option": "readonly",
|
|
164
|
+
"output": "readonly",
|
|
165
|
+
"p": "readonly",
|
|
166
|
+
"param": "readonly",
|
|
167
|
+
"picture": "readonly",
|
|
168
|
+
"pre": "readonly",
|
|
169
|
+
"progress": "readonly",
|
|
170
|
+
"q": "readonly",
|
|
171
|
+
"rp": "readonly",
|
|
172
|
+
"rt": "readonly",
|
|
173
|
+
"ruby": "readonly",
|
|
174
|
+
"s": "readonly",
|
|
175
|
+
"samp": "readonly",
|
|
176
|
+
"script": "readonly",
|
|
177
|
+
"section": "readonly",
|
|
178
|
+
"select": "readonly",
|
|
179
|
+
"slot": "readonly",
|
|
180
|
+
"small": "readonly",
|
|
181
|
+
"source": "readonly",
|
|
182
|
+
"span": "readonly",
|
|
183
|
+
"strong": "readonly",
|
|
184
|
+
"style": "readonly",
|
|
185
|
+
"sub": "readonly",
|
|
186
|
+
"summary": "readonly",
|
|
187
|
+
"sup": "readonly",
|
|
188
|
+
"table": "readonly",
|
|
189
|
+
"tbody": "readonly",
|
|
190
|
+
"td": "readonly",
|
|
191
|
+
"template": "readonly",
|
|
192
|
+
"textarea": "readonly",
|
|
193
|
+
"tfoot": "readonly",
|
|
194
|
+
"th": "readonly",
|
|
195
|
+
"thead": "readonly",
|
|
196
|
+
"time": "readonly",
|
|
197
|
+
"title": "readonly",
|
|
198
|
+
"tr": "readonly",
|
|
199
|
+
"track": "readonly",
|
|
200
|
+
"u": "readonly",
|
|
201
|
+
"ul": "readonly",
|
|
202
|
+
"var": "readonly",
|
|
203
|
+
"video": "readonly",
|
|
204
|
+
"wbr": "readonly" } }
|
|
205
|
+
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Elements.js Starter App
|
|
2
|
+
|
|
3
|
+
Elements.js is a minimalist declarative UI toolkit designed around purity,
|
|
4
|
+
immutability, and HTML semantics.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
* Zero-dependency functional UI engine
|
|
9
|
+
* Stateless components defined as pure functions
|
|
10
|
+
* Fully declarative, deeply composable view trees
|
|
11
|
+
* HTML element functions with JSDoc and TypeScript-friendly signatures
|
|
12
|
+
* No hooks, no classes, no virtual DOM heuristics
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why Elements.js?
|
|
17
|
+
|
|
18
|
+
Modern frameworks introduced declarative UI—but buried it beneath lifecycle
|
|
19
|
+
hooks, mutable state, and complex diffing algorithms.
|
|
20
|
+
|
|
21
|
+
**Elements.js goes further:**
|
|
22
|
+
|
|
23
|
+
* Pure functions represent both logic and view
|
|
24
|
+
* The DOM *is* your state model
|
|
25
|
+
* Re-rendering is *recursion*, not reconciliation
|
|
26
|
+
|
|
27
|
+
> Can UI be defined as a tree of pure function calls—nothing more?
|
|
28
|
+
|
|
29
|
+
Yes. Elements.js proves it.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Philosophy
|
|
34
|
+
|
|
35
|
+
### Declarative from top to bottom
|
|
36
|
+
|
|
37
|
+
* No internal component state
|
|
38
|
+
* No lifecycle methods or effects
|
|
39
|
+
* Every component is a function
|
|
40
|
+
|
|
41
|
+
To update a view: just **call the function again** with new arguments. The DOM
|
|
42
|
+
subtree is replaced in place.
|
|
43
|
+
|
|
44
|
+
### State lives in the DOM
|
|
45
|
+
|
|
46
|
+
There is no observer graph, no `useState`, and no memory of previous renders.
|
|
47
|
+
The DOM node *is the history*. Input state is passed as an argument.
|
|
48
|
+
|
|
49
|
+
### Minimal abstraction
|
|
50
|
+
|
|
51
|
+
* No keys, refs, proxies, or context systems
|
|
52
|
+
* No transpilation step
|
|
53
|
+
* No reactive graph to debug
|
|
54
|
+
|
|
55
|
+
Elements.js embraces the full truth of each function call as the only valid
|
|
56
|
+
state.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Example: Counter
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { button, component, div, output } from '@pfern/elements'
|
|
64
|
+
|
|
65
|
+
export const counter = component((count = 0) =>
|
|
66
|
+
div(
|
|
67
|
+
output(count),
|
|
68
|
+
button(
|
|
69
|
+
{ onclick: () => counter(count + 1) },
|
|
70
|
+
'Increment')))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
* Each click returns a new call to `counter(count + 1)`
|
|
74
|
+
* The old DOM node is replaced with the new one
|
|
75
|
+
* No virtual DOM, no diffing
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Form Example: Todos App
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
|
|
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) }, '✕'))))))})
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This is a complete MVC-style app:
|
|
115
|
+
|
|
116
|
+
* Stateless
|
|
117
|
+
* Immutable
|
|
118
|
+
* Pure
|
|
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
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Root Rendering Shortcut
|
|
127
|
+
|
|
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.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import {
|
|
134
|
+
body, h1, h2, head, header, html,
|
|
135
|
+
link, main, meta, render, section, title
|
|
136
|
+
} from './elements.js'
|
|
137
|
+
import { todos } from './components/todos.js'
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
html(
|
|
141
|
+
head(
|
|
142
|
+
title('Elements.js'),
|
|
143
|
+
meta({ name: 'viewport',
|
|
144
|
+
content: 'width=device-width, initial-scale=1.0' }),
|
|
145
|
+
link({ rel: 'stylesheet', href: 'css/style.css' })
|
|
146
|
+
),
|
|
147
|
+
body(
|
|
148
|
+
header(h1('Elements.js Demo')),
|
|
149
|
+
main(
|
|
150
|
+
section(
|
|
151
|
+
h2('Todos'),
|
|
152
|
+
todos())))))
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Declarative Events
|
|
158
|
+
|
|
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.
|
|
162
|
+
|
|
163
|
+
### General Behavior
|
|
164
|
+
|
|
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).
|
|
169
|
+
* Returned vnodes are passed to `component()` to re-render declaratively.
|
|
170
|
+
|
|
171
|
+
### Form Events
|
|
172
|
+
|
|
173
|
+
For `onsubmit`, `oninput`, and `onchange`, Elements.js provides a special
|
|
174
|
+
signature:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
(event.target.elements, event)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
That is, your handler receives:
|
|
181
|
+
|
|
182
|
+
1. `elements`: the HTML form’s named inputs
|
|
183
|
+
2. `event`: the original DOM event object
|
|
184
|
+
|
|
185
|
+
Elements.js will automatically call `event.preventDefault()` *only if* your
|
|
186
|
+
handler returns a vnode.
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
form({
|
|
190
|
+
onsubmit: ({ todo: { value } }, e) =>
|
|
191
|
+
value && todos([...items, { value, done: false }])
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
If the handler returns nothing, `preventDefault()` is skipped and the form
|
|
196
|
+
submits natively.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## API
|
|
201
|
+
|
|
202
|
+
### `component(fn)`
|
|
203
|
+
|
|
204
|
+
Wrap a recursive pure function that returns a vnode.
|
|
205
|
+
|
|
206
|
+
### `render(vnode[, container])`
|
|
207
|
+
|
|
208
|
+
Render a vnode into the DOM. If `vnode[0]` is `html`, `head`, or `body`, no
|
|
209
|
+
`container` is required.
|
|
210
|
+
|
|
211
|
+
### DOM Elements
|
|
212
|
+
|
|
213
|
+
Every HTML and SVG tag is available as a function:
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
div({ id: 'box' }, 'hello')
|
|
217
|
+
svg({ width: 100 }, circle({ r: 10 }))
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### TypeScript & JSDoc
|
|
221
|
+
|
|
222
|
+
Each tag function (e.g. `div`, `button`, `svg`) includes a `@typedef` and
|
|
223
|
+
MDN-sourced description to:
|
|
224
|
+
|
|
225
|
+
* Provide editor hints
|
|
226
|
+
* Encourage accessibility and semantic markup
|
|
227
|
+
* Enable intelligent autocomplete
|
|
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
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Status
|
|
238
|
+
|
|
239
|
+
* 🧪 Fully tested (data-in/data-out behavior)
|
|
240
|
+
* ⚡ Under 2kB min+gzip
|
|
241
|
+
* ✅ Node and browser compatible
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Installation
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npm install @pfern/elements
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Or clone the repo and use as an ES module:
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
import { render, div, component, ... } from './elements.js';
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Summary
|
|
260
|
+
|
|
261
|
+
Elements.js is a thought experiment turned practical:
|
|
262
|
+
|
|
263
|
+
> Can UI be nothing but functions?
|
|
264
|
+
|
|
265
|
+
Turns out, yes.
|
|
266
|
+
|
|
267
|
+
* No diffing
|
|
268
|
+
* No state hooks
|
|
269
|
+
* No lifecycle
|
|
270
|
+
* No reconciliation heuristics
|
|
271
|
+
|
|
272
|
+
Just pure declarative HTML—rewritten in JavaScript.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
**Lightweight. Immutable. Composable.**
|
|
277
|
+
|
|
278
|
+
Give it a try. You might never go back.
|
|
279
|
+
|