hyper-dom-expressions 0.50.0-next.2 → 0.50.0-next.3
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/CHANGELOG.md +14 -0
- package/README.md +87 -39
- package/dist/hyper-dom-expressions.js +45 -29
- package/lib/hyper-dom-expressions.js +45 -29
- package/package.json +4 -3
- package/types/index.d.ts +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# hyper-dom-expressions
|
|
2
2
|
|
|
3
|
+
## 0.50.0-next.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4c4fb65: Fix ownership leak when composing control-flow components (`For`, `Show`, etc.) in hyperscript (solidjs/solid#2453). `h(...)` now returns a tagged zero-arity thunk whose body runs under `untrack`, so render effects created inside a child component — notably per-row effects created by `mapArray` inside `For` — are rooted under the child's owner instead of whichever `r.insert` effect happens to consume it. Mutating a parent list signal no longer disposes sibling rows' render effects.
|
|
8
|
+
|
|
9
|
+
The exported surface changes shape: `h(...)` previously returned a DOM node (or array) eagerly; it now returns a thunk you invoke. Recommended usage is `r.render(h(App), mountEl)` — `render` calls the thunk inside its root. Nested `h(...)` calls compose freely; tagged thunks auto-invoke at consumption and user accessors (`() => expr`) continue to route through `r.insert`. `props.children` is uniformly wrapped via `dynamicProperty` the same way other zero-arity function props are, matching Solid JSX's getter convention — consumers that want reactive re-reads wrap the access in a thunk (`h("p", () => props.children)`).
|
|
10
|
+
|
|
11
|
+
- 4dae801: Normalize the `repository` field in every package to the standard npm
|
|
12
|
+
convention: a `git+https://github.com/ryansolid/dom-expressions.git` URL
|
|
13
|
+
with a `directory` pointing at the package within the monorepo. Restores
|
|
14
|
+
"View source" / "Open in repo" links on the npm registry and unblocks
|
|
15
|
+
tooling that resolves source from package metadata.
|
|
16
|
+
|
|
3
17
|
## 0.50.0-next.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -5,66 +5,114 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
HyperScript DSL for [DOM Expressions](https://github.com/ryansolid/dom-expressions), targeting fine-grained reactive libraries that want a no-build authoring syntax.
|
|
9
|
+
|
|
10
|
+
> **Performance note.** Of the four DOM-expressions frontends, hyperscript is the **slowest**. Every `h(...)` call materializes a small tree at runtime, versus the precompiled constants and cloned templates emitted by [`babel-plugin-jsx-dom-expressions`](https://github.com/ryansolid/dom-expressions/blob/main/packages/babel-plugin-jsx-dom-expressions), or the parse-once-then-clone caching of [`lit-dom-expressions`](https://github.com/ryansolid/dom-expressions/blob/main/packages/lit-dom-expressions). Both hyper and lit run with no build step — if you just want a no-tooling authoring syntax, prefer `lit-dom-expressions`; it's considerably faster. Hyperscript's niche is interop with React-style JSX transforms and other ecosystems that already emit `h(tag, props, …children)` calls.
|
|
9
11
|
|
|
10
12
|
## Compatible Libraries
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
- [Solid](https://github.com/ryansolid/solid)
|
|
15
|
+
- [ko-jsx](https://github.com/ryansolid/ko-jsx)
|
|
16
|
+
- [mobx-jsx](https://github.com/ryansolid/mobx-jsx)
|
|
14
17
|
|
|
15
18
|
## Getting Started
|
|
16
19
|
|
|
17
|
-
Install alongside
|
|
20
|
+
Install alongside DOM Expressions and a reactive library. For Solid:
|
|
18
21
|
|
|
19
22
|
```sh
|
|
20
|
-
|
|
23
|
+
npm install @solidjs/signals dom-expressions hyper-dom-expressions
|
|
21
24
|
```
|
|
22
|
-
Create your configuration and run dom-expressions command to generate runtime.js. More info [here](https://github.com/ryansolid/dom-expressions).
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
Initialize `h` against the runtime:
|
|
27
|
+
|
|
25
28
|
```js
|
|
26
|
-
import { createHyperScript } from
|
|
27
|
-
import * as r from
|
|
29
|
+
import { createHyperScript } from "hyper-dom-expressions";
|
|
30
|
+
import * as r from "dom-expressions/src/client";
|
|
28
31
|
|
|
29
32
|
const h = createHyperScript(r);
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
Consumers typically re-export a pre-wired `h` — Solid exposes `solid-js/h`.
|
|
36
|
+
|
|
37
|
+
## The `h` contract
|
|
38
|
+
|
|
39
|
+
`h(...)` is **lazy**. Every call returns a zero-arity thunk tagged with an internal symbol; the thunk materializes DOM (or invokes the component) under the current reactive owner when called. Laziness is what keeps per-row render effects inside `For`/`mapArray` rooted under their own owners rather than the parent `insert` effect.
|
|
40
|
+
|
|
33
41
|
```js
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
h('tr', [
|
|
37
|
-
h('td.col-md-1', row.id),
|
|
38
|
-
h('td.col-md-4', h('a', {onClick: [select, row.id]}, () => row.label)),
|
|
39
|
-
h('td.col-md-1', h('a', {onClick: [remove, row.id]}, h('span.glyphicon.glyphicon-remove'))),
|
|
40
|
-
h('td.col-md-6')
|
|
41
|
-
])
|
|
42
|
-
))
|
|
43
|
-
));
|
|
44
|
-
|
|
45
|
-
S.root(() => r.insert(document.getElementById('main'), view());)
|
|
42
|
+
const tree = h("div", h(Counter)); // thunk
|
|
43
|
+
tree(); // materializes DOM
|
|
46
44
|
```
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
Mount via `r.render`. Pass the thunk directly; `render` invokes it inside its root so the whole tree materializes under that owner:
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
```js
|
|
49
|
+
import { render } from "dom-expressions/src/client";
|
|
50
|
+
|
|
51
|
+
const App = () => h("main", h(Home));
|
|
52
|
+
|
|
53
|
+
render(h(App), document.getElementById("app"));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Inside `h(...)` composition works without ceremony: nested `h(...)` children are invoked once at consumption, and user-supplied accessors (`() => expr`) route through `r.insert` so they stay reactive.
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
## Components, props, and children
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
```jsx
|
|
56
|
-
const view = <>
|
|
57
|
-
<Component prop={value} />
|
|
58
|
-
{someValue()}
|
|
59
|
-
</>
|
|
60
|
+
- **Props are uniform.** Zero-arity function props are routed through `dynamicProperty` so reading them invokes the accessor and returns the current value — the same getter-style convention Solid's JSX compiler produces. Event props (`on…`) and higher-arity functions (e.g. a `For` render callback) pass through unchanged.
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
- **`props.children`** mirrors the caller's input:
|
|
63
|
+
|
|
64
|
+
| call shape | `props.children` |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `h(Comp)` | `undefined` |
|
|
67
|
+
| `h(Comp, { children: v })` | `v` |
|
|
68
|
+
| `h(Comp, null, a)` | `a` |
|
|
69
|
+
| `h(Comp, null, a, b, c)` | `[a, b, c]` |
|
|
70
|
+
|
|
71
|
+
Nested `h(...)` thunks flow through as-is and auto-invoke once when consumed.
|
|
72
|
+
|
|
73
|
+
- **Reactive consumption.** `h("p", props.children)` reads `props.children` once at render time. For reactive re-reads, the consumer wraps in its own accessor: `h("p", () => props.children)`. `r.insert` tracks the read and re-runs on change. This mirrors Solid JSX, which compiles `{props.children}` to `insert(el, () => props.children)`.
|
|
74
|
+
|
|
75
|
+
- **Fragments** are either plain arrays (`[h(...), h(...)]`) or the built-in `h.Fragment` component.
|
|
76
|
+
|
|
77
|
+
- **JSX-compiler interop.** Components compiled by `babel-plugin-jsx-dom-expressions` can be invoked from inside `h(...)`. Their bodies see the same `props` shape they expect from compiled call sites (getters for dynamic props, `children` as a value / function / array), so typical library components (e.g. Solid Router routes) work. The reverse direction — passing a hyperscript thunk to compiled call sites expecting an element — is not supported.
|
|
78
|
+
|
|
79
|
+
## Example
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { createHyperScript } from "hyper-dom-expressions";
|
|
83
|
+
import * as r from "dom-expressions/src/client";
|
|
84
|
+
import { createSignal, mapArray } from "@solidjs/signals";
|
|
85
|
+
|
|
86
|
+
const h = createHyperScript(r);
|
|
87
|
+
|
|
88
|
+
const For = props => mapArray(() => props.each, props.children);
|
|
89
|
+
|
|
90
|
+
const App = () => {
|
|
91
|
+
const [rows, setRows] = createSignal([
|
|
92
|
+
{ id: 1, label: "one" },
|
|
93
|
+
{ id: 2, label: "two" }
|
|
94
|
+
]);
|
|
95
|
+
return h(
|
|
96
|
+
"table.table",
|
|
97
|
+
h(
|
|
98
|
+
"tbody",
|
|
99
|
+
h(For, { each: rows }, row =>
|
|
100
|
+
h(
|
|
101
|
+
"tr",
|
|
102
|
+
h("td.col-md-1", () => row().id),
|
|
103
|
+
h("td.col-md-4", () => row().label)
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
r.render(h(App), document.getElementById("main"));
|
|
66
111
|
```
|
|
67
112
|
|
|
68
|
-
##
|
|
113
|
+
## Differences from JSX
|
|
69
114
|
|
|
70
|
-
|
|
115
|
+
- Refs are passed as a function prop (`ref: el => { … }`).
|
|
116
|
+
- Reactivity is explicit: wrap expressions in a function when they should be tracked (`() => count()`), including when forwarding a component prop (`h("p", () => props.foo)`).
|
|
117
|
+
- Fragments are arrays (or `h.Fragment`).
|
|
118
|
+
- Tag selectors understand `#id` and `.class` shorthands (`h("div#main.sel", …)`).
|
|
@@ -1,14 +1,36 @@
|
|
|
1
|
+
// Tags h() thunks so consumers can distinguish them from user-written
|
|
2
|
+
// accessors and invoke them once under the correct owner rather than
|
|
3
|
+
// wrapping them in an effect.
|
|
4
|
+
const $ELEMENT = Symbol("hyper-element");
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
// Inspired by https://github.com/hyperhype/hyperscript
|
|
2
17
|
function createHyperScript(r) {
|
|
3
|
-
function h() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
18
|
+
function h(...rawArgs) {
|
|
19
|
+
if (rawArgs.length === 1 && Array.isArray(rawArgs[0])) return rawArgs[0];
|
|
20
|
+
const thunk = () => r.untrack(() => materialize(rawArgs));
|
|
21
|
+
thunk[$ELEMENT] = true;
|
|
22
|
+
return thunk;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function materialize(args) {
|
|
26
|
+
let e;
|
|
27
|
+
let classes = [];
|
|
28
|
+
let multiExpression = false;
|
|
29
|
+
args = args.slice();
|
|
8
30
|
|
|
9
31
|
function item(l) {
|
|
32
|
+
if (l == null) return;
|
|
10
33
|
const type = typeof l;
|
|
11
|
-
if (l == null) ;else
|
|
12
34
|
if ("string" === type) {
|
|
13
35
|
if (!e) parseClass(l);else
|
|
14
36
|
e.appendChild(document.createTextNode(l));
|
|
@@ -47,40 +69,34 @@ function createHyperScript(r) {
|
|
|
47
69
|
r.assign(e, l, !!args.length);
|
|
48
70
|
} else if ("function" === type) {
|
|
49
71
|
if (!e) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
props = args.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
const d = Object.getOwnPropertyDescriptors(props);
|
|
62
|
-
for (const k in d) {
|
|
63
|
-
if (typeof d[k].value === "function" && !d[k].value.length) r.dynamicProperty(props, k);
|
|
72
|
+
const first = args[0];
|
|
73
|
+
const props =
|
|
74
|
+
first == null ||
|
|
75
|
+
typeof first === "object" && !Array.isArray(first) && !(first instanceof Element) ?
|
|
76
|
+
args.shift() || {} :
|
|
77
|
+
{};
|
|
78
|
+
if (args.length) props.children = args.length > 1 ? args : args[0];
|
|
79
|
+
for (const k in props) {
|
|
80
|
+
const v = props[k];
|
|
81
|
+
if (typeof v === "function" && !v.length) r.dynamicProperty(props, k);
|
|
64
82
|
}
|
|
65
83
|
e = r.createComponent(l, props);
|
|
84
|
+
// Drain nested h() thunks so downstream sees the real render result.
|
|
85
|
+
while (typeof e === "function" && e[$ELEMENT]) e = e();
|
|
66
86
|
args = [];
|
|
67
|
-
} else
|
|
68
|
-
|
|
69
|
-
}
|
|
87
|
+
} else if (l[$ELEMENT]) item(l());else
|
|
88
|
+
r.insert(e, l, multiExpression ? null : undefined);
|
|
70
89
|
}
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
if (args
|
|
74
|
-
typeof args[0] === "string" && detectMultiExpression(args);
|
|
92
|
+
if (typeof args[0] === "string") detectMultiExpression(args);
|
|
75
93
|
while (args.length) item(args.shift());
|
|
76
94
|
if (e instanceof Element && classes.length) e.classList.add(...classes);
|
|
77
95
|
return e;
|
|
78
96
|
|
|
79
97
|
function parseClass(string) {
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// https://mathiasbynens.be/notes/css-escapes .
|
|
83
|
-
|
|
98
|
+
// Does not understand escaped CSS specials; see
|
|
99
|
+
// https://mathiasbynens.be/notes/css-escapes.
|
|
84
100
|
const m = string.split(/([\.#]?[^\s#.]+)/);
|
|
85
101
|
if (/^\.|#/.test(m[1])) e = document.createElement("div");
|
|
86
102
|
for (let i = 0; i < m.length; i++) {
|
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Tags h() thunks so consumers can distinguish them from user-written
|
|
4
|
+
// accessors and invoke them once under the correct owner rather than
|
|
5
|
+
// wrapping them in an effect.
|
|
6
|
+
const $ELEMENT = Symbol("hyper-element");
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
3
18
|
// Inspired by https://github.com/hyperhype/hyperscript
|
|
4
19
|
function createHyperScript(r) {
|
|
5
|
-
function h() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
function h(...rawArgs) {
|
|
21
|
+
if (rawArgs.length === 1 && Array.isArray(rawArgs[0])) return rawArgs[0];
|
|
22
|
+
const thunk = () => r.untrack(() => materialize(rawArgs));
|
|
23
|
+
thunk[$ELEMENT] = true;
|
|
24
|
+
return thunk;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function materialize(args) {
|
|
28
|
+
let e;
|
|
29
|
+
let classes = [];
|
|
30
|
+
let multiExpression = false;
|
|
31
|
+
args = args.slice();
|
|
10
32
|
|
|
11
33
|
function item(l) {
|
|
34
|
+
if (l == null) return;
|
|
12
35
|
const type = typeof l;
|
|
13
|
-
if (l == null) ;else
|
|
14
36
|
if ("string" === type) {
|
|
15
37
|
if (!e) parseClass(l);else
|
|
16
38
|
e.appendChild(document.createTextNode(l));
|
|
@@ -49,40 +71,34 @@ function createHyperScript(r) {
|
|
|
49
71
|
r.assign(e, l, !!args.length);
|
|
50
72
|
} else if ("function" === type) {
|
|
51
73
|
if (!e) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
props = args.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const d = Object.getOwnPropertyDescriptors(props);
|
|
64
|
-
for (const k in d) {
|
|
65
|
-
if (typeof d[k].value === "function" && !d[k].value.length) r.dynamicProperty(props, k);
|
|
74
|
+
const first = args[0];
|
|
75
|
+
const props =
|
|
76
|
+
first == null ||
|
|
77
|
+
typeof first === "object" && !Array.isArray(first) && !(first instanceof Element) ?
|
|
78
|
+
args.shift() || {} :
|
|
79
|
+
{};
|
|
80
|
+
if (args.length) props.children = args.length > 1 ? args : args[0];
|
|
81
|
+
for (const k in props) {
|
|
82
|
+
const v = props[k];
|
|
83
|
+
if (typeof v === "function" && !v.length) r.dynamicProperty(props, k);
|
|
66
84
|
}
|
|
67
85
|
e = r.createComponent(l, props);
|
|
86
|
+
// Drain nested h() thunks so downstream sees the real render result.
|
|
87
|
+
while (typeof e === "function" && e[$ELEMENT]) e = e();
|
|
68
88
|
args = [];
|
|
69
|
-
} else
|
|
70
|
-
|
|
71
|
-
}
|
|
89
|
+
} else if (l[$ELEMENT]) item(l());else
|
|
90
|
+
r.insert(e, l, multiExpression ? null : undefined);
|
|
72
91
|
}
|
|
73
92
|
}
|
|
74
93
|
|
|
75
|
-
if (args
|
|
76
|
-
typeof args[0] === "string" && detectMultiExpression(args);
|
|
94
|
+
if (typeof args[0] === "string") detectMultiExpression(args);
|
|
77
95
|
while (args.length) item(args.shift());
|
|
78
96
|
if (e instanceof Element && classes.length) e.classList.add(...classes);
|
|
79
97
|
return e;
|
|
80
98
|
|
|
81
99
|
function parseClass(string) {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// https://mathiasbynens.be/notes/css-escapes .
|
|
85
|
-
|
|
100
|
+
// Does not understand escaped CSS specials; see
|
|
101
|
+
// https://mathiasbynens.be/notes/css-escapes.
|
|
86
102
|
const m = string.split(/([\.#]?[^\s#.]+)/);
|
|
87
103
|
if (/^\.|#/.test(m[1])) e = document.createElement("div");
|
|
88
104
|
for (let i = 0; i < m.length; i++) {
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyper-dom-expressions",
|
|
3
3
|
"description": "A Fine-Grained Rendering Runtime API using HyperScript",
|
|
4
|
-
"version": "0.50.0-next.
|
|
4
|
+
"version": "0.50.0-next.3",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/ryansolid/dom-expressions
|
|
9
|
+
"url": "git+https://github.com/ryansolid/dom-expressions.git",
|
|
10
|
+
"directory": "packages/hyper-dom-expressions"
|
|
10
11
|
},
|
|
11
12
|
"main": "lib/hyper-dom-expressions.js",
|
|
12
13
|
"module": "dist/hyper-dom-expressions.js",
|
|
13
14
|
"types": "types/index.d.ts",
|
|
14
15
|
"sideEffects": false,
|
|
15
16
|
"devDependencies": {
|
|
16
|
-
"dom-expressions": "0.50.0-next.
|
|
17
|
+
"dom-expressions": "0.50.0-next.3"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "rollup -c --bundleConfigAsCjs && tsc",
|
package/types/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ interface Runtime {
|
|
|
5
5
|
assign(node: Element, props: any, skipChildren?: Boolean): void;
|
|
6
6
|
createComponent(Comp: (props: any) => any, props: any): any;
|
|
7
7
|
dynamicProperty(props: any, key: string): any;
|
|
8
|
+
untrack<T>(fn: () => T): T;
|
|
8
9
|
SVGElements: Set<string>;
|
|
9
10
|
MathMLElements: Set<string>;
|
|
10
11
|
Namespaces: Record<string, string>;
|
|
@@ -12,11 +13,16 @@ interface Runtime {
|
|
|
12
13
|
type ExpandableNode = Node & {
|
|
13
14
|
[key: string]: any;
|
|
14
15
|
};
|
|
16
|
+
declare const $ELEMENT: unique symbol;
|
|
17
|
+
export type HyperElement = {
|
|
18
|
+
(): ExpandableNode | ExpandableNode[];
|
|
19
|
+
[$ELEMENT]: true;
|
|
20
|
+
};
|
|
15
21
|
export type HyperScript = {
|
|
16
|
-
(...args: any[]):
|
|
22
|
+
(...args: any[]): HyperElement | ExpandableNode[];
|
|
17
23
|
Fragment: (props: {
|
|
18
|
-
children:
|
|
19
|
-
}) =>
|
|
24
|
+
children: any;
|
|
25
|
+
}) => any;
|
|
20
26
|
};
|
|
21
27
|
export declare function createHyperScript(r: Runtime): HyperScript;
|
|
22
28
|
export {};
|