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",
]
}