@pfern/elements 0.2.0 → 0.2.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 +336 -34
- package/package.json +2 -1
- package/src/core/props.js +7 -1
- package/src/ssr.js +5 -7
package/README.md
CHANGED
|
@@ -1,31 +1,183 @@
|
|
|
1
|
+
<!-- Generated by scripts/sync-readmes.js. Source: README.md -->
|
|
2
|
+
|
|
1
3
|
# @pfern/elements
|
|
2
4
|
|
|
3
|
-
A
|
|
5
|
+
A functional, stateless UI toolkit for composing reactive web pages.
|
|
4
6
|
|
|
5
|
-
Elements.js
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
Elements.js borrows the simple elegance of functional UI composition from
|
|
8
|
+
[React](https://react.dev/), distilled to its purest form:
|
|
9
|
+
|
|
10
|
+
- No JSX.
|
|
11
|
+
- No hooks or keys.
|
|
12
|
+
- No virtual DOM heuristics.
|
|
13
|
+
|
|
14
|
+
Components are pure functions; updates are just calling the function again with
|
|
15
|
+
new arguments.
|
|
16
|
+
|
|
17
|
+
While you may choose to manage application logic with tools like
|
|
18
|
+
[Redux](https://redux.js.org/) or [Zustand](https://github.com/pmndrs/zustand),
|
|
19
|
+
Elements.js keeps _UI state_ exactly where it belongs: in the [DOM][dom] itself.
|
|
20
|
+
|
|
21
|
+
## Principles
|
|
22
|
+
|
|
23
|
+
- **Pure data model:** UI elements are represented as data-in, data-out
|
|
24
|
+
functions. They accept W3C standard `props` and child elements as arguments, and
|
|
25
|
+
return nested arrays.
|
|
26
|
+
- **Dynamic updates:** When an event handler returns the output of a component
|
|
27
|
+
element defined within its scope, the element is updated with its new
|
|
28
|
+
arguments.
|
|
29
|
+
- **Imperative boundary, functional surface:** DOM mutation is abstracted away,
|
|
30
|
+
keeping the authoring experience functional and composable.
|
|
31
|
+
|
|
32
|
+
### Example: Recursive counter
|
|
33
|
+
```js
|
|
34
|
+
import { button, component, div, output } from '@pfern/elements'
|
|
35
|
+
|
|
36
|
+
export const counter = component((count = 0) =>
|
|
37
|
+
div(
|
|
38
|
+
output(count),
|
|
39
|
+
button({ onclick: () => counter(count + 1) },
|
|
40
|
+
'Increment')))
|
|
41
|
+
```
|
|
8
42
|
|
|
9
|
-
##
|
|
43
|
+
## Quick Start
|
|
10
44
|
|
|
45
|
+
### Install as a dependency
|
|
11
46
|
```sh
|
|
12
|
-
npm
|
|
47
|
+
npm install @pfern/elements
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Optional 3D / X3DOM helpers
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install @pfern/elements @pfern/elements-x3dom
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Install as a minimal starter app
|
|
57
|
+
```sh
|
|
58
|
+
npx @pfern/create-elements my-app
|
|
59
|
+
cd my-app
|
|
60
|
+
npm install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Source code for the examples on this page can be found in the
|
|
64
|
+
[examples/](https://github.com/pfernandez/elements/tree/main/examples) directory of this repository, which are hosted as a live
|
|
65
|
+
demo [here](https://pfernandez.github.io/elements). The starter app also
|
|
66
|
+
includes examples as well as simple URL router for page navigation.
|
|
67
|
+
|
|
68
|
+
## Example: Todos App
|
|
69
|
+
```js
|
|
70
|
+
import { button, component, div, form, input, li, span, ul }
|
|
71
|
+
from '@pfern/elements'
|
|
72
|
+
|
|
73
|
+
const demoItems = [{ value: 'Add my first todo', done: true },
|
|
74
|
+
{ value: 'Install elements.js', done: false }]
|
|
75
|
+
|
|
76
|
+
export const todos = component(
|
|
77
|
+
(items = demoItems) => {
|
|
78
|
+
const add = ({ todo: { value } }) =>
|
|
79
|
+
value && todos([...items, { value, done: false }])
|
|
80
|
+
|
|
81
|
+
const remove = item =>
|
|
82
|
+
todos(items.filter(i => i !== item))
|
|
83
|
+
|
|
84
|
+
const toggle = item =>
|
|
85
|
+
todos(items.map(i => i === item ? { ...i, done: !item.done } : i))
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
div({ class: 'todos' },
|
|
89
|
+
form({ onsubmit: add },
|
|
90
|
+
input({ name: 'todo', placeholder: 'What needs doing?' }),
|
|
91
|
+
button({ type: 'submit' }, 'Add')),
|
|
92
|
+
|
|
93
|
+
ul(...items.map(item =>
|
|
94
|
+
li({ style:
|
|
95
|
+
{ 'text-decoration': item.done ? 'line-through' : 'none' } },
|
|
96
|
+
span({ onclick: () => toggle(item) }, item.value),
|
|
97
|
+
button({ onclick: () => remove(item) }, '✕'))))))
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Root Rendering Shortcut
|
|
102
|
+
|
|
103
|
+
If you use `html`, `head`, or `body` as the top-level tag, `render()` will
|
|
104
|
+
automatically mount into the corresponding document element—no need to pass a
|
|
105
|
+
container.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import { body, h1, h2, head, header, html,
|
|
109
|
+
link, main, meta, render, section, title } from '@pfern/elements'
|
|
110
|
+
import { todos } from './components/todos.js'
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
html(
|
|
114
|
+
head(
|
|
115
|
+
title('Elements.js'),
|
|
116
|
+
meta({ name: 'viewport',
|
|
117
|
+
content: 'width=device-width, initial-scale=1.0' }),
|
|
118
|
+
link({ rel: 'stylesheet', href: 'css/style.css' })),
|
|
119
|
+
body(
|
|
120
|
+
header(h1('Elements.js Demo')),
|
|
121
|
+
main(
|
|
122
|
+
section(
|
|
123
|
+
h2('Todos'),
|
|
124
|
+
todos())))))
|
|
13
125
|
```
|
|
14
126
|
|
|
15
127
|
## How Updates Work
|
|
16
128
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
129
|
+
Elements.js is designed so you typically call `render()` once at startup (see
|
|
130
|
+
`examples/index.js`). After that, updates happen by returning a vnode from an
|
|
131
|
+
event handler.
|
|
132
|
+
|
|
133
|
+
### What is a vnode?
|
|
134
|
+
|
|
135
|
+
Elements.js represents UI as plain arrays called **vnodes** (virtual nodes):
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
['div', { class: 'box' }, 'hello', ['span', {}, 'world']]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `tag`: a string tag name (or `'fragment'` for a wrapper-less group)
|
|
142
|
+
- `props`: an object (attributes, events, and Elements.js hooks like `ontick`)
|
|
143
|
+
- `children`: strings/numbers/vnodes (and optionally `null`/`undefined` slots)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### Declarative Events
|
|
147
|
+
|
|
148
|
+
- Any event handler (e.g. `onclick`, `onsubmit`, `oninput`) may return a vnode
|
|
149
|
+
array to trigger a replacement.
|
|
150
|
+
- If the handler returns `undefined` (or any non-vnode value), the event is
|
|
151
|
+
passive and the DOM is left alone.
|
|
152
|
+
- Returned vnodes are applied at the closest component boundary.
|
|
153
|
+
- If you return a vnode from an `<a href>` `onclick` handler, Elements.js
|
|
154
|
+
prevents default navigation for unmodified left-clicks.
|
|
155
|
+
|
|
156
|
+
Errors are not swallowed: thrown errors and rejected Promises propagate.
|
|
157
|
+
|
|
158
|
+
### Form Events
|
|
159
|
+
|
|
160
|
+
For `onsubmit`, `oninput`, and `onchange`, Elements.js provides a special
|
|
161
|
+
signature:
|
|
20
162
|
|
|
163
|
+
```js
|
|
164
|
+
(event.target.elements, event)
|
|
165
|
+
```
|
|
21
166
|
|
|
22
|
-
|
|
23
|
-
calls `event.preventDefault()` for unmodified left-clicks so SPAs can use real
|
|
24
|
-
links without manual boilerplate.
|
|
167
|
+
That is, your handler receives:
|
|
25
168
|
|
|
169
|
+
1. `elements`: the HTML form’s named inputs
|
|
170
|
+
2. `event`: the original DOM event object
|
|
26
171
|
|
|
172
|
+
Elements.js will automatically call `event.preventDefault()` *only if* your
|
|
173
|
+
handler returns a vnode.
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
form({ onsubmit: ({ todo: { value } }, e) =>
|
|
177
|
+
value && todos([...items, { value, done: false }]) })
|
|
178
|
+
```
|
|
27
179
|
|
|
28
|
-
### Routing
|
|
180
|
+
### Routing (optional)
|
|
29
181
|
|
|
30
182
|
For SPAs, register a URL-change handler once:
|
|
31
183
|
|
|
@@ -36,8 +188,10 @@ onNavigate(() => App())
|
|
|
36
188
|
```
|
|
37
189
|
|
|
38
190
|
With a handler registered, `a({ href: '/path' }, ...)` intercepts unmodified
|
|
39
|
-
left-clicks for same-origin links and uses the History API instead of
|
|
40
|
-
the page.
|
|
191
|
+
left-clicks for same-origin links and uses the History API instead of
|
|
192
|
+
reloading the page.
|
|
193
|
+
|
|
194
|
+
You can also call `navigate('/path')` directly.
|
|
41
195
|
|
|
42
196
|
### SSG / SSR
|
|
43
197
|
|
|
@@ -49,10 +203,9 @@ import { div, html, head, body, title, toHtmlString } from '@pfern/elements'
|
|
|
49
203
|
|
|
50
204
|
toHtmlString(div('Hello')) // => <div>Hello</div>
|
|
51
205
|
|
|
52
|
-
const doc =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
body(div('Hello')))
|
|
206
|
+
const doc = html(
|
|
207
|
+
head(title('My page')),
|
|
208
|
+
body(div('Hello')))
|
|
56
209
|
|
|
57
210
|
const htmlText = toHtmlString(doc, { doctype: true })
|
|
58
211
|
```
|
|
@@ -62,35 +215,184 @@ Notes:
|
|
|
62
215
|
serialization.
|
|
63
216
|
- `innerHTML` is treated as an explicit escape hatch and is inserted verbatim.
|
|
64
217
|
|
|
65
|
-
|
|
218
|
+
### Explicit Rerenders
|
|
219
|
+
|
|
220
|
+
Calling `render(vtree, container)` again is supported (diff + patch). This is
|
|
221
|
+
useful for explicit rerenders (e.g. dev reload, external state updates).
|
|
222
|
+
|
|
223
|
+
To force a full remount (discarding existing DOM state), pass
|
|
224
|
+
`{ replace: true }`.
|
|
225
|
+
|
|
226
|
+
### Why Replacement (No Keys)
|
|
227
|
+
|
|
228
|
+
Replacement updates keep the model simple:
|
|
229
|
+
|
|
230
|
+
- You never have to maintain key stability.
|
|
231
|
+
- Identity is the closest component boundary.
|
|
232
|
+
- The DOM remains the single source of truth for UI state.
|
|
233
|
+
|
|
234
|
+
## Props
|
|
235
|
+
|
|
236
|
+
Element functions accept a single props object as first argument:
|
|
66
237
|
|
|
67
238
|
```js
|
|
68
|
-
|
|
239
|
+
div({ id: 'x', class: 'box' }, 'hello')
|
|
240
|
+
```
|
|
69
241
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
242
|
+
In the DOM runtime:
|
|
243
|
+
|
|
244
|
+
- Most props are assigned via `setAttribute`.
|
|
245
|
+
- A small set of keys are treated as property exceptions when the property
|
|
246
|
+
exists on the element.
|
|
247
|
+
- Omitting a prop in a subsequent update clears it from the element.
|
|
248
|
+
|
|
249
|
+
This keeps updates symmetric and predictable.
|
|
250
|
+
|
|
251
|
+
## `ontick` (animation hook)
|
|
252
|
+
|
|
253
|
+
`ontick` is a hook (not a DOM event) that runs once per animation frame. It can
|
|
254
|
+
thread context across frames:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
transform({
|
|
258
|
+
ontick: (el, ctx = { rotation: 0 }, dt) => {
|
|
259
|
+
el.setAttribute('rotation', `0 1 1 ${ctx.rotation}`)
|
|
260
|
+
return { ...ctx, rotation: ctx.rotation + 0.001 * dt }
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`ontick` must be synchronous. If it throws (or returns a Promise), ticking
|
|
266
|
+
stops, and the error is not swallowed.
|
|
267
|
+
|
|
268
|
+
If the element is inside an `<x3d>` scene, Elements.js waits for the X3DOM
|
|
269
|
+
runtime to be ready before ticking.
|
|
270
|
+
|
|
271
|
+
## X3D / X3DOM (experimental)
|
|
272
|
+
|
|
273
|
+
The optional `@pfern/elements-x3dom` package includes elements for X3DOM’s
|
|
274
|
+
supported X3D node set. You can import them and create 3D scenes
|
|
275
|
+
declaratively:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
npm i @pfern/elements @pfern/elements-x3dom
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Demo: Interactive 3D Cube
|
|
282
|
+
```js
|
|
283
|
+
import { appearance, box, material, scene,
|
|
284
|
+
shape, transform, viewpoint, x3d } from '@pfern/elements-x3dom'
|
|
285
|
+
|
|
286
|
+
export const cubeScene = () =>
|
|
287
|
+
x3d(
|
|
288
|
+
scene(
|
|
289
|
+
viewpoint({ position: '0 0 6', description: 'Default View' }),
|
|
290
|
+
transform({ rotation: '0 1 0 0.5' },
|
|
291
|
+
shape(
|
|
292
|
+
appearance(
|
|
293
|
+
material({ diffuseColor: '0.2 0.6 1.0' })),
|
|
294
|
+
box()))))
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Lazy Loading
|
|
298
|
+
|
|
299
|
+
X3DOM is lazy-loaded the first time you call any helper from
|
|
300
|
+
`@pfern/elements-x3dom`. For correctness and stability, it always loads the
|
|
301
|
+
vendored `x3dom-full` bundle (plus `x3dom.css`).
|
|
302
|
+
|
|
303
|
+
## Types (the docs)
|
|
304
|
+
|
|
305
|
+
Elements.js is JS-first: TypeScript is not required at runtime. This package
|
|
306
|
+
ships `.d.ts` files so editors like VSCode can provide rich inline docs and
|
|
307
|
+
autocomplete.
|
|
308
|
+
|
|
309
|
+
The goal is for type definitions to be the canonical reference for:
|
|
310
|
+
|
|
311
|
+
* HTML/SVG/X3D element helpers
|
|
312
|
+
* DOM events (including the special form-event signature)
|
|
313
|
+
* Elements.js-specific prop conventions like `ontick`, plus supported
|
|
314
|
+
prop shorthands like `style` (object) and `innerHTML` (escape hatch)
|
|
74
315
|
|
|
75
|
-
|
|
316
|
+
Most props are assigned as attributes. A small set of keys are treated as
|
|
317
|
+
property exceptions (when the property exists on the element): `value`,
|
|
318
|
+
`checked`, `selected`, `disabled`, `multiple`, `muted`, `volume`,
|
|
319
|
+
`currentTime`, `playbackRate`, `open`, `indeterminate`.
|
|
320
|
+
|
|
321
|
+
Omitting a prop in a subsequent update clears it from the element.
|
|
322
|
+
|
|
323
|
+
## API
|
|
324
|
+
|
|
325
|
+
### `component(fn)`
|
|
326
|
+
|
|
327
|
+
Wrap a recursive pure function that returns a vnode.
|
|
328
|
+
|
|
329
|
+
### `render(vnode[, container])`
|
|
330
|
+
|
|
331
|
+
Render a vnode into the DOM. If `vnode[0]` is `html`, `head`, or `body`, no
|
|
332
|
+
`container` is required.
|
|
333
|
+
|
|
334
|
+
Pass `{ replace: true }` to force a full remount.
|
|
335
|
+
|
|
336
|
+
### `elements`
|
|
337
|
+
|
|
338
|
+
All tag helpers are also exported in a map for dynamic use:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
import { elements } from '@pfern/elements'
|
|
342
|
+
|
|
343
|
+
const { div, button } = elements
|
|
76
344
|
```
|
|
77
345
|
|
|
78
|
-
|
|
346
|
+
### DOM Elements
|
|
347
|
+
|
|
348
|
+
Every HTML and SVG tag is available as a function:
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
div({ id: 'box' }, 'hello')
|
|
352
|
+
svg({ width: 100 }, circle({ r: 10 }))
|
|
353
|
+
```
|
|
79
354
|
|
|
80
|
-
|
|
81
|
-
A small curated helper set is available as a separate entrypoint:
|
|
355
|
+
Curated MathML helpers are available as a separate entrypoint:
|
|
82
356
|
|
|
83
357
|
```js
|
|
84
358
|
import { apply, ci, csymbol, math } from '@pfern/elements/mathml'
|
|
85
359
|
|
|
86
360
|
math(
|
|
87
|
-
apply(csymbol({ cd: 'ski' }, 'app'), ci('f'), ci('x'))
|
|
361
|
+
apply(csymbol({ cd: 'ski' }, 'app'), ci('f'), ci('x'))
|
|
362
|
+
)
|
|
88
363
|
```
|
|
89
364
|
|
|
90
|
-
|
|
365
|
+
For X3D / X3DOM nodes, use `@pfern/elements-x3dom`:
|
|
91
366
|
|
|
92
|
-
|
|
367
|
+
```js
|
|
368
|
+
import { box } from '@pfern/elements-x3dom'
|
|
93
369
|
|
|
94
|
-
|
|
95
|
-
npm i @pfern/elements @pfern/elements-x3dom
|
|
370
|
+
box({ size: '2 2 2', solid: true })
|
|
96
371
|
```
|
|
372
|
+
|
|
373
|
+
### `onNavigate(fn[, options])`
|
|
374
|
+
|
|
375
|
+
Register a handler to run after `popstate` (including calls to `navigate()`).
|
|
376
|
+
Use this to re-render your app on URL changes.
|
|
377
|
+
|
|
378
|
+
### `toHtmlString(vnode[, options])`
|
|
379
|
+
|
|
380
|
+
Serialize a vnode tree to HTML (SSG / SSR). Pass `{ doctype: true }` to emit
|
|
381
|
+
`<!doctype html>`.
|
|
382
|
+
|
|
383
|
+
### `navigate(path[, options])`
|
|
384
|
+
|
|
385
|
+
`navigate` updates `window.history` and dispatches a `popstate` event. It is a
|
|
386
|
+
tiny convenience for router-style apps.
|
|
387
|
+
|
|
388
|
+
### Testing Philosophy
|
|
389
|
+
|
|
390
|
+
Tests run in Node and use a small in-repo fake DOM for behavioral DOM checks.
|
|
391
|
+
See [`packages/elements/test/README.md`](https://github.com/pfernandez/elements/blob/main/packages/elements/test/README.md).
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT License
|
|
396
|
+
Copyright (c) 2026 Paul Fernandez
|
|
397
|
+
|
|
398
|
+
[dom]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pfern/elements",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A minimalist, pure functional declarative UI toolkit.",
|
|
5
5
|
"author": "Paul Fernandez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"clean:types": "node scripts/clean-types.js",
|
|
30
30
|
"build:types": "node scripts/build-types.js",
|
|
31
31
|
"typecheck": "node scripts/typecheck.js",
|
|
32
|
+
"prepack": "node ../../scripts/sync-readmes.js",
|
|
32
33
|
"test": "node --test test/*.test.* --test-reporter spec",
|
|
33
34
|
"test:coverage": "node scripts/test-coverage.js"
|
|
34
35
|
},
|
package/src/core/props.js
CHANGED
|
@@ -99,7 +99,9 @@ export const removeMissingProps = (el, prevProps, nextProps) => {
|
|
|
99
99
|
const keys = Object.keys(prevProps)
|
|
100
100
|
for (let i = 0; i < keys.length; i++) {
|
|
101
101
|
const key = keys[i]
|
|
102
|
-
|
|
102
|
+
if (key in nextProps) continue
|
|
103
|
+
|
|
104
|
+
clearProp(el, key)
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
@@ -125,6 +127,10 @@ export const assignProperties = (el, props, env) => {
|
|
|
125
127
|
const key = keys[i]
|
|
126
128
|
const value = props[key]
|
|
127
129
|
|
|
130
|
+
if (key === 'className') {
|
|
131
|
+
throw new TypeError('Invalid prop: className. Use `class`.')
|
|
132
|
+
}
|
|
133
|
+
|
|
128
134
|
if (key === 'ontick' && typeof value === 'function') {
|
|
129
135
|
el.ontick = value
|
|
130
136
|
startTickLoop(el, value, { ready: isX3DOMReadyFor })
|
package/src/ssr.js
CHANGED
|
@@ -88,17 +88,16 @@ const shouldDropProp = (key, value) => {
|
|
|
88
88
|
const attrsToString = props => {
|
|
89
89
|
if (!isObject(props)) return ''
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
: props
|
|
91
|
+
if (props.className != null && props.className !== false)
|
|
92
|
+
throw new TypeError('Invalid prop: className. Use `class`.')
|
|
94
93
|
|
|
95
|
-
const keys = Object.keys(
|
|
96
|
-
.filter(k => !shouldDropProp(k,
|
|
94
|
+
const keys = Object.keys(props)
|
|
95
|
+
.filter(k => !shouldDropProp(k, props[k]))
|
|
97
96
|
.sort()
|
|
98
97
|
|
|
99
98
|
let out = ''
|
|
100
99
|
for (const key of keys) {
|
|
101
|
-
const value =
|
|
100
|
+
const value = props[key]
|
|
102
101
|
if (value == null || value === false) {
|
|
103
102
|
// Preserve `aria-*` / `data-*` boolean semantics for false.
|
|
104
103
|
if (typeof value === 'boolean'
|
|
@@ -172,4 +171,3 @@ const toHtmlStringInner = vnode => {
|
|
|
172
171
|
*/
|
|
173
172
|
export const toHtmlString = (vnode, { doctype = false } = {}) =>
|
|
174
173
|
`${doctype ? '<!doctype html>' : ''}${toHtmlStringInner(vnode)}`
|
|
175
|
-
|