diff --git a/.gitignore b/.gitignore index 37ce4e9..735ae24 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ node.js !esm/node.js !esm/dom/node.js !test/dom/node.js +reactive.js +!esm/reactive.js +!esm/render/reactive.js +preactive.js +!test/preactive.js diff --git a/README.md b/README.md index a56c372..3572d2c 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,15 @@ ### Exports - * `uhtml` as default `{ Hole, render, html, svg, attr }` with smart auto-keyed nodes - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more - * `uhtml/keyed` with extras `{ Hole, render, html, svg, htmlFor, svgFor, attr }`, providing keyed utilities - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more - * `uhtml/node` with *same default* exports but it's for *one-off* nodes creation only so that no cache or updates are available and it's just an easy way to hook *uhtml* into your existing project for DOM creation (not manipulation!) - * `uhtml/init` which returns a `document => uhtml/keyed` utility that can be bootstrapped with `uhtml/dom`, [LinkeDOM](https://github.com/WebReflection/linkedom), [JSDOM](https://github.com/jsdom/jsdom) for either *SSR* or *Workers* support - * `uhtml/dom` which returns a specialized *uhtml* compliant DOM environment that can be passed to the `uhtml/init` export to have 100% same-thing running on both client or Web Worker / Server. This entry exports `{ Document, DOMParser }` where the former can be used to create a new *document* while the latter one can parse well formed HTML or SVG content and return the document out of the box. + * **uhtml** as default `{ Hole, render, html, svg, attr }` with smart auto-keyed nodes - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more + * **uhtml/keyed** with extras `{ Hole, render, html, svg, htmlFor, svgFor, attr }`, providing keyed utilities - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more + * **uhtml/node** with *same default* exports but it's for *one-off* nodes creation only so that no cache or updates are available and it's just an easy way to hook *uhtml* into your existing project for DOM creation (not manipulation!) + * **uhtml/init** which returns a `document => uhtml/keyed` utility that can be bootstrapped with `uhtml/dom`, [LinkeDOM](https://github.com/WebReflection/linkedom), [JSDOM](https://github.com/jsdom/jsdom) for either *SSR* or *Workers* support + * **uhtml/dom** which returns a specialized *uhtml* compliant DOM environment that can be passed to the `uhtml/init` export to have 100% same-thing running on both client or Web Worker / Server. This entry exports `{ Document, DOMParser }` where the former can be used to create a new *document* while the latter one can parse well formed HTML or SVG content and return the document out of the box. + * **uhtml/reactive** which allows usage of symbols within the optionally *keyed* render function. The only difference with other exports, beside exporting a `reactive` field instead of `render`, so that `const render = reactive(effect)` creates a reactive render per each library, is that the `render(where, () => what)`, with a function as second argument is mandatory when the rendered stuff has signals in it, otherwise these can't side-effect properly. + * **uhtml/preactive** is an already bundled `uhtml/reactive` with `@preact/signals-core` in it, so that its `render` exported function, among all other *preact* related exports, is already working. -**uhtml/init example** +### uhtml/init example ```js import init from 'uhtml/init'; @@ -40,3 +42,17 @@ const { attr } = init(document); ``` + +### uhtml/preactive example + +```js +import { render, html, signal } from 'uhtml/preactive'; + +const count = signal(0); + +render(document.body, () => html` + +`); +``` \ No newline at end of file diff --git a/esm/index.js b/esm/index.js index a17014c..13eb41a 100644 --- a/esm/index.js +++ b/esm/index.js @@ -1,7 +1,7 @@ /*! (c) Andrea Giammarchi - MIT */ - import { Hole } from './rabbit.js'; import { attr } from './handler.js'; + import render from './render/hole.js'; /** @typedef {import("./literals.js").Value} Value */ diff --git a/esm/keyed.js b/esm/keyed.js index 47a0f14..bdd5fdf 100644 --- a/esm/keyed.js +++ b/esm/keyed.js @@ -1,8 +1,10 @@ -import { cache } from './literals.js'; +/*! (c) Andrea Giammarchi - MIT */ import { Hole, unroll } from './rabbit.js'; +import { attr } from './handler.js'; +import { cache } from './literals.js'; import { empty, set } from './utils.js'; import { html, svg } from './index.js'; -import { attr } from './handler.js'; + import render from './render/keyed.js'; /** @typedef {import("./literals.js").Cache} Cache */ diff --git a/esm/node.js b/esm/node.js index b0eb09a..4f7fe0a 100644 --- a/esm/node.js +++ b/esm/node.js @@ -1,9 +1,9 @@ /*! (c) Andrea Giammarchi - MIT */ +import { attr } from './handler.js'; import create from './creator.js'; import parser from './parser.js'; import render from './render/node.js'; -import { attr } from './handler.js'; /** @typedef {import("./literals.js").DOMValue} DOMValue */ /** @typedef {import("./literals.js").Target} Target */ diff --git a/esm/reactive.js b/esm/reactive.js new file mode 100644 index 0000000..0eb4850 --- /dev/null +++ b/esm/reactive.js @@ -0,0 +1,6 @@ +/*! (c) Andrea Giammarchi - MIT */ +import { Hole, html, svg, htmlFor, svgFor, attr } from './keyed.js'; + +import reactive from './render/reactive.js'; + +export { Hole, reactive, html, svg, htmlFor, svgFor, attr }; diff --git a/esm/reactive/preact.js b/esm/reactive/preact.js new file mode 100644 index 0000000..65dfa11 --- /dev/null +++ b/esm/reactive/preact.js @@ -0,0 +1,8 @@ +import { effect } from '@preact/signals-core'; +export * from '@preact/signals-core'; + +import { Hole, reactive, html, svg, htmlFor, svgFor, attr } from '../reactive.js'; + +const render = reactive(effect); + +export { Hole, render, html, svg, htmlFor, svgFor, attr }; diff --git a/esm/render/hole.js b/esm/render/hole.js index d583008..4c93da3 100644 --- a/esm/render/hole.js +++ b/esm/render/hole.js @@ -1,5 +1,5 @@ -import { cache } from '../literals.js'; import { unroll } from '../rabbit.js'; +import { cache } from '../literals.js'; import { empty, set } from '../utils.js'; /** @typedef {import("../rabbit.js").Hole} Hole */ diff --git a/esm/render/keyed.js b/esm/render/keyed.js index 3f634ad..d07b7f6 100644 --- a/esm/render/keyed.js +++ b/esm/render/keyed.js @@ -1,17 +1,17 @@ -import { cache } from '../literals.js'; import { Hole, unroll } from '../rabbit.js'; +import { cache } from '../literals.js'; import { empty, set } from '../utils.js'; /** @type {WeakMap} */ const known = new WeakMap; /** - * Render with smart updates within a generic container. - * @template T - * @param {T} where the DOM node where to render content - * @param {(() => Hole) | Hole} what the hole to render - * @returns - */ + * Render with smart updates within a generic container. + * @template T + * @param {T} where the DOM node where to render content + * @param {(() => Hole) | Hole} what the hole to render + * @returns + */ export default (where, what) => { const info = known.get(where) || set(known, where, cache(empty)); const hole = typeof what === 'function' ? what() : what; diff --git a/esm/render/reactive.js b/esm/render/reactive.js new file mode 100644 index 0000000..a90e3fe --- /dev/null +++ b/esm/render/reactive.js @@ -0,0 +1,33 @@ +import { create, drop } from 'gc-hook'; + +import render from './keyed.js'; + +/** @type {WeakMap} */ +const effects = new WeakMap; + +/** + * @param {Function} dispose + * @returns {void} + */ +const onGC = dispose => dispose(); + +export default effect => { + /** + * Render with smart updates within a generic container. + * @template T + * @param {T} where the DOM node where to render content + * @param {() => Hole} what the hole to render + * @returns {T} + */ + return (where, what) => { + let dispose = effects.get(where); + if (dispose) { + drop(dispose); + dispose(); + } + const wr = new WeakRef(where); + dispose = effect(() => { render(wr.deref(), what) }); + effects.set(where, dispose); + return create(dispose, onGC, { return: where }); + }; +}; diff --git a/package-lock.json b/package-lock.json index 2cf62c7..3c4687f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "4.2.2", "license": "MIT", "dependencies": { + "@preact/signals-core": "*", "@webreflection/uparser": "^0.3.3", "custom-function": "^1.0.6", "domconstants": "^1.1.6", + "gc-hook": "^0.3.0", "html-escaper": "^3.0.3", "htmlparser2": "^9.0.0", "udomdiff": "^1.1.0" @@ -21,9 +23,11 @@ "@rollup/plugin-terser": "^0.4.4", "ascjs": "^6.0.3", "c8": "^8.0.1", - "linkedom": "^0.16.5", "rollup": "^4.9.2", "typescript": "^5.3.3" + }, + "optionalDependencies": { + "@preact/signals-core": "^1.5.1" } }, "node_modules/@babel/parser": { @@ -111,6 +115,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@preact/signals-core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.5.1.tgz", + "integrity": "sha512-dE6f+WCX5ZUDwXzUIWNMhhglmuLpqJhuy3X3xHrhZYI0Hm2LyQwOu0l9mdPiWrVNsE+Q7txOnJPgtIqHCYoBVA==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", @@ -429,12 +443,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -553,40 +561,6 @@ "node": ">= 8" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, "node_modules/custom-function": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/custom-function/-/custom-function-1.0.6.tgz", @@ -747,6 +721,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gc-hook": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.0.tgz", + "integrity": "sha512-Qkp0HM3z839Ns0LpXFJBXqClNW23wQo6JpUdJAjuf1/2jB+oUWSOMzeMv2yFq8Ur45z8IWw9hpRhkSjxSt5RWg==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -926,19 +905,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/linkedom": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.5.tgz", - "integrity": "sha512-FtcuLuxDtlKWWilm5Z0HgmrfMwO0tOfC6tu47fRXj2/KGEeDSh4ihiDwFKZSbJj6zh520r8XZjZ7v2Jb30HAQA==", - "dev": true, - "dependencies": { - "css-select": "^5.1.0", - "cssom": "^0.5.0", - "html-escaper": "^3.0.3", - "htmlparser2": "^9.0.0", - "uhyphen": "^0.2.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -993,18 +959,6 @@ "node": "*" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1364,12 +1318,6 @@ "resolved": "https://registry.npmjs.org/udomdiff/-/udomdiff-1.1.0.tgz", "integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA==" }, - "node_modules/uhyphen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", - "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", diff --git a/package.json b/package.json index fd9700e..4578918 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "rollup:es": "rollup --config rollup/es.config.js", "rollup:init": "rollup --config rollup/init.config.js", "server": "npx static-handler .", - "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";", + "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"reactive $(cat reactive.js | brotli | wc -c)\";echo \"preactive $(cat preactive.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";", "test": "c8 node test/coverage.js", "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info", "ts": "tsc -p ." @@ -59,6 +59,16 @@ "import": "./esm/node.js", "default": "./cjs/node.js" }, + "./reactive": { + "types": "./types/reactive.d.ts", + "import": "./esm/reactive.js", + "default": "./cjs/reactive.js" + }, + "./preactive": { + "types": "./types/reactive/preact.d.ts", + "import": "./esm/reactive/preact.js", + "default": "./cjs/reactive/preact.js" + }, "./package.json": "./package.json" }, "unpkg": "./keyed.js", @@ -66,6 +76,7 @@ "@webreflection/uparser": "^0.3.3", "custom-function": "^1.0.6", "domconstants": "^1.1.6", + "gc-hook": "^0.3.0", "html-escaper": "^3.0.3", "htmlparser2": "^9.0.0", "udomdiff": "^1.1.0" @@ -77,5 +88,8 @@ "bugs": { "url": "https://github.com/WebReflection/uhtml/issues" }, - "homepage": "https://github.com/WebReflection/uhtml#readme" + "homepage": "https://github.com/WebReflection/uhtml#readme", + "optionalDependencies": { + "@preact/signals-core": "^1.5.1" + } } diff --git a/rollup/es.config.js b/rollup/es.config.js index fd2e746..b77157a 100644 --- a/rollup/es.config.js +++ b/rollup/es.config.js @@ -42,6 +42,22 @@ export default [ file: './node.js', }, }, + { + plugins, + input: './esm/reactive.js', + output: { + esModule: true, + file: './reactive.js', + }, + }, + { + plugins, + input: './esm/reactive/preact.js', + output: { + esModule: true, + file: './preactive.js', + }, + }, { plugins, input: './esm/dom/index.js', diff --git a/test/preactive.html b/test/preactive.html new file mode 100644 index 0000000..02cfe3c --- /dev/null +++ b/test/preactive.html @@ -0,0 +1,20 @@ + + + + + + uhtml/preactive + + + diff --git a/tsconfig.json b/tsconfig.json index d2a4cb2..4e4d537 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,8 @@ "esm/init.js", "esm/keyed.js", "esm/node.js", + "esm/reactive.js", + "esm/reactive/preact.js", "esm/dom/index.js", ] }