@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.
@@ -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,9 @@
1
+ import { button, component, div, output } from '@pfern/elements'
2
+
3
+ export const counter = component((count = 0) =>
4
+ div(
5
+ output(count),
6
+ button(
7
+ { onclick: () => counter(count + 1) },
8
+ 'Increment')))
9
+
@@ -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
+ })