Skip to content

Commit

Permalink
uhtml/reactive 🦄
Browse files Browse the repository at this point in the history
This MR brings reactivity out of the box via signals based libraries.

You can bring your ogn signals based library or use the `uhtml/preactive`
export which already bundles `@preact/signals-core` in it.

The main difference with the *reactive* export, or its *preactive* one,
is that the *render* function needs the "what to render" second argument
to be a callback, otherwise signals don't get a chance to side effect.
  • Loading branch information
WebReflection committed Jan 4, 2024
1 parent 5ef2bb7 commit 99b0330
Show file tree
Hide file tree
Showing 15 changed files with 162 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Expand Up @@ -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
28 changes: 22 additions & 6 deletions README.md
Expand Up @@ -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';
Expand All @@ -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`
<button onclick=${() => { count.value++ }}>
Clicks: ${count.value}
</button>
`);
```
2 changes: 1 addition & 1 deletion 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 */
Expand Down
6 changes: 4 additions & 2 deletions 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 */
Expand Down
2 changes: 1 addition & 1 deletion 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 */
Expand Down
6 changes: 6 additions & 0 deletions 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 };
8 changes: 8 additions & 0 deletions 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 };
2 changes: 1 addition & 1 deletion 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 */
Expand Down
14 changes: 7 additions & 7 deletions 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<Element | DocumentFragment, import("../literals.js").Cache>} */
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;
Expand Down
33 changes: 33 additions & 0 deletions esm/render/reactive.js
@@ -0,0 +1,33 @@
import { create, drop } from 'gc-hook';

import render from './keyed.js';

/** @type {WeakMap<Element | DocumentFragment, Function>} */
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 });
};
};
92 changes: 20 additions & 72 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions package.json
Expand Up @@ -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 ."
Expand Down Expand Up @@ -59,13 +59,24 @@
"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",
"dependencies": {
"@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"
Expand All @@ -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"
}
}
16 changes: 16 additions & 0 deletions rollup/es.config.js
Expand Up @@ -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',
Expand Down

0 comments on commit 99b0330

Please sign in to comment.