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 1d43475
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions esm/reactive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*! (c) Andrea Giammarchi - MIT */
import { unroll } from './rabbit.js';
import { Hole } 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 reactive from './render/reactive.js';

/** @typedef {import("./literals.js").Cache} Cache */
/** @typedef {import("./literals.js").Target} Target */
/** @typedef {import("./literals.js").Value} Value */

/** @typedef {(ref:Object, key:string | number) => Tag} Bound */

/**
* @callback Tag
* @param {TemplateStringsArray} template
* @param {...Value} values
* @returns {Target}
*/

const keyed = new WeakMap;
const createRef = svg => /** @type {Bound} */ (ref, key) => {
/** @type {Tag} */
function tag(template, ...values) {
return unroll(this, new Hole(svg, template, values));
}

const memo = keyed.get(ref) || set(keyed, ref, new Map);
return memo.get(key) || set(memo, key, tag.bind(cache(empty)));
};

/** @type {Bound} Returns a bound tag to render HTML content. */
const htmlFor = createRef(false);

/** @type {Bound} Returns a bound tag to render SVG content. */
const svgFor = createRef(true);

export { Hole, reactive, html, svg, htmlFor, svgFor, attr };
8 changes: 8 additions & 0 deletions esm/reactive/preact.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 1d43475

Please sign in to comment.