@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "elements-template",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "An elements.js app.",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"test": "node --test test/*.test.* --test-reporter spec"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": ""
|
|
17
|
+
},
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"eslint": "^9.39.2",
|
|
22
|
+
"vite": "^7.3.1"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@pfern/elements": "^0.1.3",
|
|
26
|
+
"@picocss/pico": "^2.1.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { button, component, div,
|
|
2
|
+
form, input, li, span, ul } from '@pfern/elements'
|
|
3
|
+
|
|
4
|
+
export const todos = component(
|
|
5
|
+
(items = [{ value: 'Add my first todo', done: true }]) => {
|
|
6
|
+
|
|
7
|
+
const add = ({ todo: { value } }) =>
|
|
8
|
+
value && todos([...items, { value, done: false }])
|
|
9
|
+
|
|
10
|
+
const remove = item =>
|
|
11
|
+
todos(items.filter(i => i !== item))
|
|
12
|
+
|
|
13
|
+
const toggle = item =>
|
|
14
|
+
todos(items.map(i => i === item ? { ...i, done: !item.done } : i))
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
div({ class: 'todos' },
|
|
18
|
+
|
|
19
|
+
form({ onsubmit: add },
|
|
20
|
+
input({ name: 'todo', placeholder: 'What needs doing?' }),
|
|
21
|
+
button({ type: 'submit' }, 'Add')),
|
|
22
|
+
|
|
23
|
+
ul(...items.map(item =>
|
|
24
|
+
li(
|
|
25
|
+
{ style:
|
|
26
|
+
{ 'text-decoration': item.done ? 'line-through' : 'none' } },
|
|
27
|
+
span({ onclick: () => toggle(item) }, item.value),
|
|
28
|
+
button({ onclick: () => remove(item) }, '✕'))))))})
|
|
29
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { body, div, h1, h2, head, header, html,
|
|
2
|
+
link, main, meta, render, section, title } from '@pfern/elements'
|
|
3
|
+
import { counter } from './components/counter.js'
|
|
4
|
+
import { todos } from './components/todos.js'
|
|
5
|
+
|
|
6
|
+
render(
|
|
7
|
+
html(
|
|
8
|
+
head(
|
|
9
|
+
title('elements.js'),
|
|
10
|
+
meta({ name: 'viewport',
|
|
11
|
+
content: 'width=device-width, initial-scale=1.0' }),
|
|
12
|
+
link({ rel: 'stylesheet', href: 'src/style.css' })),
|
|
13
|
+
body(
|
|
14
|
+
header(
|
|
15
|
+
h1('Elements.js Demo')),
|
|
16
|
+
main(
|
|
17
|
+
section(
|
|
18
|
+
h2('Todos'),
|
|
19
|
+
todos()),
|
|
20
|
+
section({ class: 'grid' },
|
|
21
|
+
div(
|
|
22
|
+
h2('Counter 1'),
|
|
23
|
+
counter()),
|
|
24
|
+
div(
|
|
25
|
+
h2('Counter 2'),
|
|
26
|
+
counter()))))))
|
|
27
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
/*******************************************************************************
|
|
3
|
+
* Reset & base theme
|
|
4
|
+
******************************************************************************/
|
|
5
|
+
|
|
6
|
+
@import '@picocss/pico/css/pico.sand.css';
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
--pico-font-family-sans-serif: 'Arial', sans-serif;
|
|
10
|
+
--pico-border-radius: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Light color scheme (Default) */
|
|
14
|
+
/* Can be forced with data-theme="light" */
|
|
15
|
+
[data-theme="light"],
|
|
16
|
+
:root:not([data-theme="dark"]) {
|
|
17
|
+
--pico-primary-background: #ccc;
|
|
18
|
+
--pico-primary-border: #ccc;
|
|
19
|
+
--pico-primary-hover-color: var(--pico-primary-inverse);
|
|
20
|
+
--pico-primary-hover-background: transparent;
|
|
21
|
+
--pico-primary-hover-border: var(--pico-primary-hover-color);
|
|
22
|
+
--pico-button-hover-box-shadow: var(--pico-primary-hover-background);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
input:not([type=checkbox],[type=radio],[type=range]) {
|
|
26
|
+
height: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*******************************************************************************
|
|
30
|
+
* Layout
|
|
31
|
+
******************************************************************************/
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
padding: 20px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body>main, main {
|
|
38
|
+
border-bottom: 1px solid #ddd;
|
|
39
|
+
padding-top: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
body>header, header {
|
|
43
|
+
padding-bottom: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
section {
|
|
47
|
+
border-top: 1px solid #ddd;
|
|
48
|
+
padding-top: 1em;
|
|
49
|
+
margin: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*******************************************************************************
|
|
53
|
+
* General
|
|
54
|
+
******************************************************************************/
|
|
55
|
+
|
|
56
|
+
output {
|
|
57
|
+
font-size: xx-large;
|
|
58
|
+
font-weight: bold;
|
|
59
|
+
font-family: monospace;
|
|
60
|
+
padding: 1em;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
li > span:hover {
|
|
64
|
+
opacity: 0.8;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
form > * {
|
|
68
|
+
display: inline-block;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
li > button {
|
|
72
|
+
padding: 2px 8px 1px;
|
|
73
|
+
margin-left: 10px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*******************************************************************************
|
|
77
|
+
* Apps
|
|
78
|
+
******************************************************************************/
|
|
79
|
+
|
|
80
|
+
.todos form {
|
|
81
|
+
max-width: 600px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.todos form input {
|
|
85
|
+
width: 75%;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.todos form button {
|
|
89
|
+
width: 25%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.todos li {
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Testing Philosophy: Elements.js
|
|
2
|
+
|
|
3
|
+
Elements.js is designed around **purity**, **immutability**, and **data-in/data-out UI logic**.
|
|
4
|
+
|
|
5
|
+
This testing suite reflects that philosophy:
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What We Test
|
|
10
|
+
|
|
11
|
+
### Element Structure
|
|
12
|
+
|
|
13
|
+
* Every exported tag (e.g. `div`, `svg`, `form`) is a pure function.
|
|
14
|
+
* It returns a **vnode array**: `['tag', props, ...children]`
|
|
15
|
+
* Props and children must appear in the correct positions.
|
|
16
|
+
|
|
17
|
+
### Event Listeners
|
|
18
|
+
|
|
19
|
+
* Event handlers return vnodes to declaratively update the view.
|
|
20
|
+
* Special cases like `onsubmit`, `oninput`, `onchange` receive `(elements, event)`.
|
|
21
|
+
* Listeners returning falsy values (`null`, `false`, `''`) are treated as passive.
|
|
22
|
+
|
|
23
|
+
### Components
|
|
24
|
+
|
|
25
|
+
* `component(fn)` wraps a recursive, stateless function that can call itself with new arguments.
|
|
26
|
+
* Recursive updates should return well-formed vnodes and trigger no side effects.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## What We Don't Test
|
|
31
|
+
|
|
32
|
+
We **do not** test:
|
|
33
|
+
|
|
34
|
+
* Real DOM rendering or patching (that’s internal)
|
|
35
|
+
* Whether `preventDefault()` was called (covered by behavior, not inspection)
|
|
36
|
+
* Any mutation of the DOM
|
|
37
|
+
* Internal utilities like `assignProperties`, `diffTree`, or `render` directly
|
|
38
|
+
|
|
39
|
+
Instead, we test only what is **observable through public exports**.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Purity Contract
|
|
44
|
+
|
|
45
|
+
> **Every test must be resolvable by examining the return value.**
|
|
46
|
+
> No test depends on the DOM, mutation, timers, side effects, or internal state.
|
|
47
|
+
|
|
48
|
+
This allows the entire system to be:
|
|
49
|
+
|
|
50
|
+
* Predictable
|
|
51
|
+
* Stateless
|
|
52
|
+
* Transparent
|
|
53
|
+
|
|
54
|
+
And trivially portable to other runtimes (SSR, testing, WASM, etc).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Running Tests
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node --test
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
All tests use native `node:test` and `assert`—no external dependencies.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Adding New Tests
|
|
69
|
+
|
|
70
|
+
When adding features, ask:
|
|
71
|
+
|
|
72
|
+
* Is this behavior observable at the vnode or component level?
|
|
73
|
+
* Can it be tested using only function return values and inputs?
|
|
74
|
+
|
|
75
|
+
If yes → write a test.
|
|
76
|
+
If not → consider whether the feature belongs in this framework at all.
|
|
77
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { button, div, form, input } from '@pfern/elements'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
|
|
5
|
+
describe('Elements.js example tests', () => {
|
|
6
|
+
test('div() returns a vnode with tag "div"', () => {
|
|
7
|
+
const vnode = div({ id: 'test' }, 'hello')
|
|
8
|
+
assert.deepEqual(vnode, ['div', { id: 'test' }, 'hello'])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('button with onclick handler returns new vnode', () => {
|
|
12
|
+
const handler = () => ['span', {}, 'clicked']
|
|
13
|
+
const b = button({ onclick: handler }, 'Click Me')
|
|
14
|
+
const result = b[1].onclick()
|
|
15
|
+
assert.deepEqual(result, ['span', {}, 'clicked'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('form onsubmit handler receives elements and event', () => {
|
|
19
|
+
let receivedElements, receivedEvent
|
|
20
|
+
const handler = (elements, event) => {
|
|
21
|
+
receivedElements = elements
|
|
22
|
+
receivedEvent = event
|
|
23
|
+
return ['div', {}, 'submitted']
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fakeElements = { task: { value: 'buy milk' } }
|
|
27
|
+
const fakeEvent = { type: 'submit', foo: 'bar' }
|
|
28
|
+
|
|
29
|
+
const f = form({ onsubmit: handler }, input({ name: 'task' }))
|
|
30
|
+
const result = f[1].onsubmit(fakeElements, fakeEvent)
|
|
31
|
+
|
|
32
|
+
assert.equal(receivedElements.task.value, 'buy milk')
|
|
33
|
+
assert.equal(receivedEvent.foo, 'bar')
|
|
34
|
+
assert.deepEqual(result, ['div', {}, 'submitted'])
|
|
35
|
+
})
|
|
36
|
+
})
|