-
Fix an issue where properties were being set via
el.setAttribute('xyz', ...)
rather thanel['xyz'] = ...
. This lead to subtle but unexpected behaviour, particularly in situations like<input value={wire(...)} />
.This was an oversight when removing support for
attrs
from the previous Sinuous implementation: -
The build script now help with preparing the publishing area to make publishes less stressful. It also shows the delta in min+gz sizes compared to the previous build and tweaks sourcemaps.
> npm run build dom/index.js min:1928 min+gzip:955 Δ:0 state/index.js min:1916 min+gzip:909 Δ:0 stdlib/index.js min:508 min+gzip:301 Δ:+45 index.js min:155 min+gzip:144 Δ:0
The API is nearly settled and hopefully the next major release will be 1.0.0. This is an uncomfortably large release. I'll release smaller changes and fixes more frequently from now own to avoid this large of a changelog.
-
Use more explicit naming for everything (#2 and #8).
- Rename
haptic/h
tohaptic/dom
- Rename
haptic/v
tohaptic/w
tohaptic/wire
tohaptic/state
- Rename
haptic/u
tohaptic/util
tohaptic/stdlib
- Rename
-
Publish ESM and CJS builds using Node 16+'s
package.json#exports
field. -
Write documentation and introduction code that has been tested.
haptic/dom
-
Rewrite the main
h()
call to have less function calls (noitem()
). -
Function are serialized to strings in
haptic/h
rather than implicitly being wrapped in a reactive context. This forces JSX to be more explicit and requiresapi.patch
to search for a specific data type (a wire, by default).Passing a signal implicitly will no longer work:
// Bad return <p>Function is serialized to a string: {sig}</p> // Good return <p>Signal is subscribed and its value updates the DOM: {wire(sig)}</p>
-
Remove support for
attrs
. This was specific to Sinuous. -
Remove the global event proxy. This was specific to Sinuous.
-
Fix array handling to create and populate fragments correctly (#13).
-
Adopt
svg()
into thehaptic/dom
package instead ofhaptic/stdlib
.
haptic/state
-
Rename vocals to simply signals; a term the community is familiar with. Rename reactors to wires (note "wS+wC" and "core" were also used for some time).
-
Signals now accept a "SubToken"
$
to initiate a read-subscribe instead of the previouss(...)
wrapper function. Here's an updated example from the 0.6 changelog:const data = signal({ count: 0, text: '' }); wire($ => { console.log("Wire will run when count is updated:", data.count($)); console.log("Wire doesn't run when text is updated:", data.text()); })();
This makes the two types of reads look more visually similar and makes it clearly a read.
This $ token is actually also a function that makes it easier to unpack multiple signal values since it's a fairly common operation:
// This is fine and works... const [a, b, c] = [sigA($), sigB($), sigC($)] // This is smaller and has the same type support for TS const [a, b, c] = $(sigA, sigB, sigC);
-
Names/IDs for signals and wires are now their function name. The actual JS function name! This is from a nice
{[k](){}}[k]
hack and allows signals and wires to appear naturally in console logs and stacktraces. -
Transactions are now atomic so all wires read the same signal value (#9).
-
Anonymous (unnamed) signals can directly created with
= signal.anon(45);
-
Add lazy computed signals ✨ (#1 and #14)
This is Haptic's version of a
computed()
without creating a new data type.Instead, these are defined by passing a wire into a signal, which then acts as the computation engine for the signal, while the signal is responsible for communicating the value to other wires.
const state = signal({ count: 45, countSquared(wire($ => state.count($) ** 2)), countSquaredPlusFive(wire($ => state.countSquared($) + 5)), }); // Note that the computation has never run up to now. They're _lazy_. // Calling countSquaredPlusFive will run countSquared, since it's a dependency. state.countSquaredPlusFive(); // 2030 // Calling countSquared does _no work_. It's not stale. The value is cached. state.countSquared(); // 2025
-
Replace the expensive topological sort (Set to Array) to a ancestor lookup loop that actually considers all grandchildren, not only direct children.
-
Rework all internal wire states to use a 3-field bitmask (#14).
-
Unpausing is no longer done by calling the wire since it was inconsistent with how wires worked in all other cases. Wires must always be able to be run manually without changing state. Now running a paused wire leaves it paused. Use the new
wireResume()
to unpause (#14). -
Removed the ability to chain wires into multiple DOM patches. It was dangerous and lead to unpredictable behaviour with
when()
or computed signals. It's an accident waiting to happen (#14). -
Implement a test runner in Zora that supports TS, ESM, and file watching.
-
Change the function signature for
when()
to accept a$ => *
function instead of a wire. Instead, a wire is created internally.-when(wire($ => { +when($ => { const c = data.count($); return c <= 0 ? '-0' : c <= 10 ? '1..10' : '+10' -}), { +}, { '-0' : () => <p>There's no items</p> '1..10': () => <p>There's between 1 and 10 items</p> '+10' : () => <p>There's a lot of items</p> });
This is because I removed chaining for wires, so
when()
can't simply extend the given wire, it would need to nest it in a new wire. It makes most sense to be a computed signal, but typingwhen(signal.anon(wire($ =>...
is awful, so creating a single wire is the best API choice.
-
Redesign the reactivity engine from scratch based on explicit subscriptions.
Designed separately in https://github.com/heyheyhello/haptic-reactivity.
Replaces
haptic/s
ashaptic/v
.Introduces Vocals as signals and Reactors as effects. Subscriptions are explicitly linked by a function
s(...)
created for each reactor run:const v = vocals({ count: 0, text: '' }); rx(s => { console.log("Reactor will run when count is updated:", s(v.count)); console.log("Reactor doesn't run when text is updated:", v.text()); })();
Globally unique IDs are used to tag each vocal and reactor. This is useful for debugging subscriptions.
Accidental subscriptions are avoided without needing a
sample()
method.Reactors must consistently use vocals as either a read-pass or read-subscribe. Mixing these into the same reactor will throw.
Reactors tracking nesting (reactors created within a reactor run).
Reactors use a finite-state-machine to avoid infinite loops, track paused and stale states, and mark if they have subscriptions after a run.
Reactors can be paused. This includes all nested reactors. When manually run to unpause, the reactor only runs if is has been marked as stale.
Reactors are topologically sorted to avoid nested children reactors running before their parent. This is because reactors clear all children when run, so these children would otherwise run more times than needed.
-
Add
when()
to conditionally switch DOM content in an efficient way. -
Replace Sinuous'
api.subscribe
with a generic patch callback.
-
Drop computed signals. They're confusing.
-
List issues with the observer pattern architecture of Haptic and Sinuous. These will be addressed later.
-
Add
on()
,transaction()
, andcapture()
.
-
Rewrite Sinuous in TypeScript. Lifting only
sinuous/h
andsinuous/observable
to Haptic ashaptic/h
andhaptic/s
. -
Include multiple d.ts files which allow for patching other reactive libraries into the JSX namespace of
haptic/h
. -
Drop HTM over JSX: https://gitlab.com/nthm/stayknit/-/issues/1
I love the idea of HTM but it's fragile and no editor plugins provide comparable autocomplete, formatting, and error checking to JSX. It's too easy to have silently broken markup in HTM. It's also noticable runtime overhead.
HTM can be worth it for zero-transpilation workflows, but Haptic already uses TypeScript. That ship has sailed. Debugging is already supported by sourcemaps to show readble TS - JSX naturally fits there.
Haptic needs to approachable to new developers. It's a better developer experience to use JSX.
-
Design systems for SSR, CSS-in-JS, and Hydration. These are part of the modern web stack. They will be designed alongside Haptic to complete the picture.
-
Design lifecycle hook support. Supports
onAttach
andonDetach
hooks without usingMutationObserver
.
-
Began researching ideas for designing reactivity in ways that still love the DOM; without needing a virtual DOM or reconcilation algorithms.
Notes are at https://gitlab.com/nthm/lovebud
Discover Sinuous shortly after and contribute there instead.