From 005035cb3d2efd21f0cbd9633e5941e659c71a97 Mon Sep 17 00:00:00 2001 From: fcrozatier Date: Fri, 7 Nov 2025 17:43:06 +0100 Subject: [PATCH 1/2] update docs --- README.md | 9 ++- packages/functorial/README.md | 138 +++++++++++++++++++++----------- packages/reflow/README.md | 145 +++++++++++++++++++++++----------- 3 files changed, 203 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index f766f3f..6883bb3 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ a clean, coherent and holistic design then this is for you too. solutions, where we loose track of what's going on. This quickly becomes un-debuggable and impossible to reason about. I don't trade long term understanding for immediate comfort. +4. The stack is **unapologetic** with concepts: I'll use the correct + terminology, without hiding from math or computer science terms, and without + rebranding them. That's because I **respect** your intelligence and believe + we can learn new concepts. Actually, the added value of manual programming is + theory building and **acquiring expertise**. In other words, the role of a + framework should be to provide you not only with tools, but with deeper + knowledge and a whole new vision of the field. ## Architecture @@ -28,7 +35,7 @@ The current pieces are (more to come): A new `Proxy`-based reactivity system, that goes beyond Signals. More idiomatic. More granular. -It's a structured way to declaratively interact with web API in a reactive +It's a structured way to declaratively interact with web APIs in a reactive manner ### [Reflow](./packages/reflow/README.md) diff --git a/packages/functorial/README.md b/packages/functorial/README.md index a43fce5..4d6d270 100644 --- a/packages/functorial/README.md +++ b/packages/functorial/README.md @@ -1,47 +1,97 @@ # Functorial -The reactivity primitive. +_The reactivity primitive_ ## Introduction -_The problem: we want a way to interact with web APIs in a faithful, reactive -and declarative manner._ +Functorial reactivity is an idiomatic way to interact with web APIs in a +faithful, reactive and declarative manner. It's different from Signals as you +not only map the data, but also the behaviors, in a structured way. -Functorial reactivity achieves this by letting you not only map the data, but -also the behaviors. - -For example, `delete` an object property to remove a listener, call `unshift()` -on a list to prepend data in the DOM. - -We always mutate the DOM for performance, so the idea is to use mutable -structures and reflect their changes (transport their operations) to update the -DOM. +For example, `delete` an object property to remove a listener or call +`unshift()` on a list to prepend data in a DOM container. As a consequence, this approach yields -- The highest level of granularity +- The highest level of granularity: faithfulness - A cristal-clear [mental model](#mental-model) -- A principled approach to manipulating web APIs declaratively +- A principled approach to interacting with web APIs declaratively +- A more natural reactivity primitive ## Mental Model -Functorial reactivity creates a faithful communication between your templates -and the DOM. +### Mutation-first -Faithful means that we both: +The DOM is a mutable structure, and for performance we update the DOM by +mutating it. Functorial reactivity reflects this in our templates with its focus +on mutable structures. State is held inside mutable structures (_eg._ `Object`, +`Array`) and their changes and operations are transported to corresponding DOM +updates. -- know the full story of what happens on the Template side -- can reach whole APIs dynamically on the DOM side +For example add a key-value pair to an object to add an attribute to a DOM +`Element`, delete it to remove the attribue. + +### Mapping Templates to DOM... + +Templates are where we declare the relation between a piece of state and the +DOM. + +With Functorial reactivity this relation is a faithful communication between our +templates and the DOM: + +1. We create a piece of state and map it to the DOM (up arrow) +2. We mutate the state or perform an operation on it (left arrow) +3. Our state listener is triggered with an event letting us know what happened, + so we can perform the right DOM update dynamically without diffing (right + arrow) +4. The DOM is in the same state as if we had directly applied this updated + state: the whole diagram commutes (down arrow) ![Mental Model]() -In practice, the `listen` callback gives us all the fine-grained details we need -about the data update to perform the corresponding surgical DOM updates. +### ... faithfully + +The relation between our templates and the DOM being faithful means that we +both: + +- **know the full story of what happens on the Template side** +- **can reach whole APIs dynamically on the DOM side** + +Since it's `Proxy`-based, the granularity on the left side is only constrained +by the resolution provided by the `Proxy` traps: we can know wether a key was +created, updated, deleted, or whether a method was called and with which +arguments. This tells us the full story as an event, which can be accessed as +the callback parameter of the `listen` function. + +> [!NOTE] +> In particular this granularity means we don't need diffing: the `Proxy` +> already knows what happens, so it would be a pure waste to discard this +> information to then reconstruct it afterwards with diffing. Instead this data +> is provided as a `ReactiveEvent` in the `listen` callback parameter. + +### Expressive, Structured approach + +Functorial reactivity focuses on mutable **structures**: the `listen` function +takes a structure to listen to as its first parameter. + +This, combined with the granularity of the `ReactiveEvent` which tells us how +the structure changes, let us create expressive mappings of semantics. + +For example, since all common web APIs all revolve around create and delete +operations, + +- `setAttribute` and `removeAttribute` +- `addEventListener` and `removeEventListener` +- `.classList.add` and `.classList.remove` +- `.style.setProperty` and `.style.removeProperty` + +the `delete` operation on a state object can be mapped to the expected +corresponding operation in the DOM. ### Videos -Here are a few videos explaining the concept and mental model behind functorial -reactivity, as well as a few examples: +Here are a few videos explaining the functorial reactivity mental model, with a +few examples: - [Concept and granularity demo](https://bsky.app/profile/fred-crozatier.dev/post/3lyktxp75x22a) - [Mental model and difference with Signals](https://bsky.app/profile/fred-crozatier.dev/post/3m3ctprjykc25) @@ -49,22 +99,22 @@ reactivity, as well as a few examples: ## Usage -Functorial is a low-level, framework-independent reactivity system with -zero-dependencies. You can use it directly but will have to implement common web -mappings (attributes, listeners etc.) yourself. +Functorial is a low-level, framework-independent reactivity system. You can use +it directly but will have to implement common web mappings (attributes, +listeners etc.) yourself. -These common functorial mappings are provided in [Reflow](../reflow/README.md) -which is the natural companion and recommended way to use Functorial. +These common mappings are provided in [Reflow](../reflow/README.md) which is the +recommended way to use Functorial. ### CDN -The library's single `.js` file can be load directly from a CDN +The library can be loaded directly from `esm.sh` ```html @@ -98,19 +148,20 @@ npx jsr add @f-stack/functorial ## Examples -Here are a few raw examples showcasing some of the basic Functorial features. -You can also have a look at the [Playground](../../playground/README.md) for -real life examples and usage with Reflow. +Here are a few examples showcasing some basic features. You can also have a look +at the [Playground](../../playground/README.md) for real life examples and usage +with Reflow. ### Reactive objects Create a reactive state with `reactive`. Listen to its updates with `listen`. -> [TIP!] You can create a reactive array, Map or Set by directly wrapping them -> like `reactive([])` or `reactive(new Map())`. With functorial reactivity we -> can reflect operations directly, with the `listen` callback being the source -> of truth for the DOM synchronisation logic. So we don't need to reimplement -> every data structure with a reactive variant +> [!TIP] +> You can create a reactive array, `Map` or `Set` by directly wrapping them like +> `reactive([])` or `reactive(new Map())`. The `listen` callback being the +> source of truth for the DOM synchronisation logic, we don't have to +> reimplement every data structure as a reactive variant. This is a key +> difference with Signals. ```ts import { listen, reactive } from "@f-stack/functorial"; @@ -132,7 +183,6 @@ state.count = 1; ### Derived values Use getters to cache derived values, or create them directly with `derived`. -These derived values are cached for performance. ```ts import { derived, listen, reactive } from "@f-stack/functorial"; @@ -168,7 +218,7 @@ state.count = 4; // } ``` -### Listen to operations +### Listen to changes and operations You can think of `listen` as a way to hook into the Proxy and get the full picture of what's going on. @@ -179,7 +229,7 @@ import { listen, reactive } from "@f-stack/functorial"; const array = reactive([1, 2, 3]); listen(array, (e) => { - // e tells you everything about what happened to `array` + // e tells us everything that happens to `array` console.log(e); }); @@ -192,7 +242,7 @@ array.push(4); // } ``` -### Writable derived values +### Writable-derived values Some derived values are also writable, like the `Array.length` property. Add a setter next to a getter to create a writable derived. @@ -214,6 +264,6 @@ const price = reactive({ ## [Playground](../../playground/README.md) -## [API](https://jsr.io/@f-stack/functorial/doc) +## API -Interactive API on JSR +Interactive API available on [JSR](https://jsr.io/@f-stack/functorial/doc) diff --git a/packages/reflow/README.md b/packages/reflow/README.md index 23730e7..595e011 100644 --- a/packages/reflow/README.md +++ b/packages/reflow/README.md @@ -81,7 +81,7 @@ const Demo = () => { }; // Directly append to your document body: -// document.body.append(Demo()); +// document.body.append(Demo().fragment); ``` ## Features @@ -96,15 +96,14 @@ const Demo = () => { - Template caching - Strong type safety with no extension required - Supports type parameters for even stronger type safety -- Zero 3rd-party runtime dependencies ## Mental model With Reflow we insert reactive data in our templates inside holes (sinks) to -declaratively and reactively manipulate web APIs (`Attr`, `EventListener`, -`DOMTokenList` etc). This is done in a very structured way. +declaratively and reactively interact with web APIs (`Attr`, `EventListener`, +`DOMTokenList` etc). -There are two sorts of sinks: +This is done in a structured way, with two sorts of sinks: 1. Element-level sinks @@ -124,9 +123,6 @@ There are two sorts of sinks: See below for a walkthrough of the various available sinks. -The template tag returns a live `DocumentFragment` that can be directly inserted -in the DOM. - ## Install Depending on your package manager: @@ -141,8 +137,8 @@ npx jsr add @f-stack/reflow ### `on` -Handles event listeners on an `Element`. Creates a functorial mapping with the -element `addEventListener` and `removeEventListener` methods +Handles event listeners on an `Element`. This sink creates a functorial mapping +with the element `addEventListener` and `removeEventListener` methods > [!TIP] > You can access a strongly typed `this` value by using a type parameter with a @@ -185,10 +181,10 @@ export const OnDemo = () => { ### `attr` -Handles attributes on an `Element`. Creates a functorial mapping with the -element `setAttribute` and `removeAttribute` methods. +Handles attributes on an `Element`. This sink creates a functorial mapping with +the element `setAttribute` and `removeAttribute` methods. -> [TIP!] You can pass `attr` and `AttrSink` a tag name as a type parameter for +> [!TIP] You can pass `attr` and `AttrSink` a tag name as a type parameter for > stronger type safety. All HTML, SVG and MathML tags are supported ```ts @@ -217,10 +213,10 @@ export const AttrDemo = () => { ### `prop` -Handles an `Element` properties. +Handles setting an `Element` properties. > [!TIP] -> You can use a type parameter for element-specific props type safety +> You can use a type parameter for element-specific type-safe props ```ts import { html, prop } from "@f-stack/reflow"; @@ -245,33 +241,28 @@ export const PropPage = () => { Runs a callback hook on the `Element` it is attached to. ```ts -import { reactive } from "@f-stack/functorial"; -import { attach, html, on } from "@f-stack/reflow"; +import { attach, html } from "@f-stack/reflow"; export const AttachDemo = () => { - const form = reactive({ value: "Bob" }); - return html` -
- - -
+ { + const context = canvas.getContext("2d"); + + if (context) { + context.fillStyle = "#40E0D0"; + context.fillRect(0, 0, 200, 200); + } + }, + )}>Enable JS `; }; ``` ### `classList` -Handles conditional classes on an `Element`. This creates a functorial mapping -with the element `classList.add` and `classList.remove` methods. +Handles conditional classes on an `Element`. This sink creates a functorial +mapping with the element `classList.add` and `classList.remove` methods. > [!TIP] > You can use getters in any reactive object to derive values like in this @@ -313,7 +304,7 @@ Handles the creation and update of `Text` nodes. > [!TIP] > For convenience, you can also use a `Primitive` or `ReactiveLeaf` directly to -> create `Text` nodes. +> create a `Text` node. ```ts import { html, on, text } from "@f-stack/reflow"; @@ -371,10 +362,9 @@ export const DerivedDemo = () => { ### `show` -Handles conditional templates. It takes 3 callbacks: the first returns the -conditional value, the second is the template, primitive or reactive leaf to use -in the `true` case and the third is the template, primitive or reactive leaf to -use in the `false` case. +Handles conditional templates. It takes 2+1 callbacks: the first returns the +conditional value, the second is the `DerivedSink` to use in the `true` case and +the third is the optional `DerivedSink` to use in the `false` case. > [!TIP] > For simple ternary conditions, you can use a `derived` sink like below. But @@ -521,11 +511,11 @@ export const MapDemo = () => { ### `style` -Handles inline styles on an `Element`. Creates a functorial mapping with the -element `style.setProperty` and `style.removeProperty` methods. +Handles inline styles on an `Element`. This sink creates a functorial mapping +with the element `style.setProperty` and `style.removeProperty` methods. > [!TIP] -> You can also manipulate reactive --dashed ident properties this way like below +> You can also use reactive `--dashed-ident` properties like below ```ts import { attr, html, on, style, type StyleSink } from "@f-stack/reflow"; @@ -574,7 +564,7 @@ export const StyleDemo = () => { Handles raw HTML. > [!WARNING] -> Only use this sink with trusted inputs +> Only use this unsafe sink with trusted inputs ```ts import { html, on, unsafeHTML } from "@f-stack/reflow"; @@ -605,6 +595,73 @@ export const UnsafeHTMLDemo = () => { }; ``` -## [API](https://jsr.io/@f-stack/reflow/doc) +## Lifecycle + +Use the `component` wrapper function as below when you need to handle cleanup, +either manually or automatically. + +### Auto cleanup + +The `this` value of the `component` callback function gives access to a local +`listen` function that is automatically cleaned up when the component effects +are disposed of. This prevents memory leaks and bugs with nested effects. + +```ts +import { html, on, show } from "@f-stack/reflow"; +import { component } from "../packages/reflow/src/sinks.ts"; +import { reactive } from "@f-stack/functorial"; + +export const AutoCleanupDemo = component(function () { + const display = reactive({ value: true }); + const count = reactive({ value: 0 }); + + return html` + + + ${show( + () => display.value, + // inline component + component(function () { + // use the local `listen` method via the this `EffectScope` for auto cleanup + this.listen(count, () => { + console.log("increment"); + }); + return html` +

count: ${count}

+ `; + }), + )} + `; +}); +``` + +### Manual cleanup + +The `EffectScope` of the `component` callback function also provides access to a +`DisposableStack` cleaned when the component instance is disposed. + +```ts +import { html } from "@f-stack/reflow"; +import { component } from "../packages/reflow/src/sinks.ts"; + +export const ManualCleanupDemo = component(function () { + const callback = () => { + console.log("loaded"); + }; + + document.addEventListener("DOMContentLoaded", callback); + + // manually add cleanup logic to the component `DisposableStack` + this.disposer.defer(() => { + document.removeEventListener("DOMContentLoaded", callback); + }); + + return html` + ... + `; +}); +``` + +## API -Interactive API on JSR. +Interactive API on [JSR](https://jsr.io/@f-stack/reflow/doc) From 460fbc46dca41f5f88248f4ea4062282930ca6b5 Mon Sep 17 00:00:00 2001 From: fcrozatier Date: Fri, 7 Nov 2025 17:48:07 +0100 Subject: [PATCH 2/2] fix types --- packages/reflow/README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/reflow/README.md b/packages/reflow/README.md index 595e011..44aabd4 100644 --- a/packages/reflow/README.md +++ b/packages/reflow/README.md @@ -607,8 +607,7 @@ The `this` value of the `component` callback function gives access to a local are disposed of. This prevents memory leaks and bugs with nested effects. ```ts -import { html, on, show } from "@f-stack/reflow"; -import { component } from "../packages/reflow/src/sinks.ts"; +import { component, type EffectScope, html, on, show } from "@f-stack/reflow"; import { reactive } from "@f-stack/functorial"; export const AutoCleanupDemo = component(function () { @@ -621,7 +620,7 @@ export const AutoCleanupDemo = component(function () { ${show( () => display.value, // inline component - component(function () { + component(function (this: EffectScope) { // use the local `listen` method via the this `EffectScope` for auto cleanup this.listen(count, () => { console.log("increment"); @@ -641,10 +640,9 @@ The `EffectScope` of the `component` callback function also provides access to a `DisposableStack` cleaned when the component instance is disposed. ```ts -import { html } from "@f-stack/reflow"; -import { component } from "../packages/reflow/src/sinks.ts"; +import { component, type EffectScope, html } from "@f-stack/reflow"; -export const ManualCleanupDemo = component(function () { +export const ManualCleanupDemo = component(function (this: EffectScope) { const callback = () => { console.log("loaded"); };