Signal is subscribed and its value updates the DOM: {wire(sig)}
57 | ```
58 |
59 | - Remove support for `attrs`. This was specific to Sinuous.
60 |
61 | - Remove the global event proxy. This was specific to Sinuous.
62 |
63 | - Fix array handling to create and populate fragments correctly (#13).
64 |
65 | - Adopt `svg()` into the `haptic/dom` package instead of `haptic/stdlib`.
66 |
67 | **`haptic/state`**
68 |
69 | - Rename vocals to simply signals; a term the community is familiar with. Rename
70 | reactors to wires (note "wS+wC" and "core" were also used for some time).
71 |
72 | - Signals now accept a "SubToken" `$` to initiate a read-subscribe instead of
73 | the previous `s(...)` wrapper function. Here's an updated example from the 0.6
74 | changelog:
75 |
76 | ```ts
77 | const data = signal({ count: 0, text: '' });
78 | wire($ => {
79 | console.log("Wire will run when count is updated:", data.count($));
80 | console.log("Wire doesn't run when text is updated:", data.text());
81 | })();
82 | ```
83 |
84 | This makes the two types of reads look more visually similar and makes it
85 | clearly a read.
86 |
87 | This $ token is actually also a function that makes it easier to unpack
88 | multiple signal values since it's a fairly common operation:
89 |
90 | ```tsx
91 | // This is fine and works...
92 | const [a, b, c] = [sigA($), sigB($), sigC($)]
93 | // This is smaller and has the same type support for TS
94 | const [a, b, c] = $(sigA, sigB, sigC);
95 | ```
96 |
97 | - Names/IDs for signals and wires are now their function name. The actual JS
98 | function name! This is from a nice `{[k](){}}[k]` hack and allows signals and
99 | wires to appear naturally in console logs and stacktraces.
100 |
101 | - Transactions are now atomic so all wires read the same signal value (#9).
102 |
103 | - Anonymous (unnamed) signals can directly created with `= signal.anon(45);`
104 |
105 | - Add lazy computed signals ✨ (#1 and #14)
106 |
107 | This is Haptic's version of a `computed()` without creating a new data type.
108 |
109 | Instead, these are defined by passing a wire into a signal, which then acts as
110 | the computation engine for the signal, while the signal is responsible for
111 | communicating the value to other wires.
112 |
113 | ```ts
114 | const state = signal({
115 | count: 45,
116 | countSquared(wire($ => state.count($) ** 2)),
117 | countSquaredPlusFive(wire($ => state.countSquared($) + 5)),
118 | });
119 | // Note that the computation has never run up to now. They're _lazy_.
120 |
121 | // Calling countSquaredPlusFive will run countSquared, since it's a dependency.
122 | state.countSquaredPlusFive(); // 2030
123 |
124 | // Calling countSquared does _no work_. It's not stale. The value is cached.
125 | state.countSquared(); // 2025
126 | ```
127 |
128 | - Replace the expensive topological sort (Set to Array) to a ancestor lookup
129 | loop that actually considers all grandchildren, not only direct children.
130 |
131 | - Rework all internal wire states to use a 3-field bitmask (#14).
132 |
133 | - Unpausing is no longer done by calling the wire since it was inconsistent with
134 | how wires worked in all other cases. Wires must always be able to be run
135 | manually without changing state. Now running a paused wire leaves it paused.
136 | Use the new `wireResume()` to unpause (#14).
137 |
138 | - Removed the ability to chain wires into multiple DOM patches. It was dangerous
139 | and lead to unpredictable behaviour with `when()` or computed signals. It's an
140 | accident waiting to happen (#14).
141 |
142 | - Implement a test runner in Zora that supports TS, ESM, and file watching.
143 |
144 | - Change the function signature for `when()` to accept a `$ => *` function
145 | instead of a wire. Instead, a wire is created internally.
146 |
147 | ```diff
148 | -when(wire($ => {
149 | +when($ => {
150 | const c = data.count($);
151 | return c <= 0 ? '-0' : c <= 10 ? '1..10' : '+10'
152 | -}), {
153 | +}, {
154 | '-0' : () =>
There's no items
155 | '1..10': () =>
There's between 1 and 10 items
156 | '+10' : () =>
There's a lot of items
157 | });
158 | ```
159 |
160 | This is because I removed chaining for wires, so `when()` can't simply extend
161 | the given wire, it would need to nest it in a new wire. It makes most sense to
162 | be a computed signal, but typing `when(signal.anon(wire($ =>...` is awful, so
163 | creating a single wire is the best API choice.
164 |
165 | ## 0.8.0
166 |
167 | - Redesign the reactivity engine from scratch based on explicit subscriptions.
168 |
169 | Designed separately in https://github.com/heyheyhello/haptic-reactivity.
170 |
171 | Replaces `haptic/s` as `haptic/v`.
172 |
173 | Introduces _Vocals_ as signals and _Reactors_ as effects. Subscriptions are
174 | explicitly linked by a function `s(...)` created for each reactor run:
175 |
176 | ```ts
177 | const v = vocals({ count: 0, text: '' });
178 | rx(s => {
179 | console.log("Reactor will run when count is updated:", s(v.count));
180 | console.log("Reactor doesn't run when text is updated:", v.text());
181 | })();
182 | ```
183 |
184 | Globally unique IDs are used to tag each vocal and reactor. This is useful for
185 | debugging subscriptions.
186 |
187 | Accidental subscriptions are avoided without needing a `sample()` method.
188 |
189 | Reactors must consistently use vocals as either a read-pass or read-subscribe.
190 | Mixing these into the same reactor will throw.
191 |
192 | Reactors tracking nesting (reactors created within a reactor run).
193 |
194 | Reactors use a finite-state-machine to avoid infinite loops, track paused and
195 | stale states, and mark if they have subscriptions after a run.
196 |
197 | Reactors can be paused. This includes all nested reactors. When manually run
198 | to unpause, the reactor only runs if is has been marked as _stale_.
199 |
200 | Reactors are topologically sorted to avoid nested children reactors running
201 | before their parent. This is because reactors clear all children when run, so
202 | these children would otherwise run more times than needed.
203 |
204 | - Add `when()` to conditionally switch DOM content in an efficient way.
205 |
206 | - Replace Sinuous' `api.subscribe` with a generic patch callback.
207 |
208 | ## 0.1.0 - 0.6.0
209 |
210 | - Drop computed signals. They're confusing.
211 |
212 | - List issues with the observer pattern architecture of Haptic and Sinuous.
213 | These will be addressed later.
214 |
215 | - Add `on()`, `transaction()`, and `capture()`.
216 |
217 | ## 0.0.0
218 |
219 | - Rewrite Sinuous in TypeScript. Lifting only `sinuous/h` and
220 | `sinuous/observable` to Haptic as `haptic/h` and `haptic/s`.
221 |
222 | - Include multiple d.ts files which allow for patching other reactive libraries
223 | into the JSX namespace of `haptic/h`.
224 |
225 | - Drop HTM over JSX: https://gitlab.com/nthm/stayknit/-/issues/1
226 |
227 | I love the idea of HTM but it's fragile and no editor plugins provide
228 | comparable autocomplete, formatting, and error checking to JSX. It's too easy
229 | to have silently broken markup in HTM. It's also noticable runtime overhead.
230 |
231 | HTM can be worth it for zero-transpilation workflows, but Haptic already uses
232 | TypeScript. That ship has sailed. Debugging is already supported by sourcemaps
233 | to show readble TS - JSX naturally fits there.
234 |
235 | Haptic needs to approachable to new developers. It's a better developer
236 | experience to use JSX.
237 |
238 | - Design systems for SSR, CSS-in-JS, and Hydration. These are part of the modern
239 | web stack. They will be designed alongside Haptic to complete the picture.
240 |
241 | https://github.com/heyheyhello/stayknit/
242 |
243 | - Design lifecycle hook support. Supports `onAttach` and `onDetach` hooks
244 | **without** using `MutationObserver`.
245 |
246 | https://www.npmjs.com/package/sinuous-lifecycle
247 |
248 | ## Origin
249 |
250 | - Began researching ideas for designing reactivity in ways that still love the
251 | DOM; without needing a virtual DOM or reconcilation algorithms.
252 |
253 | Notes are at https://gitlab.com/nthm/lovebud
254 |
255 | Discover Sinuous shortly after and contribute there instead.
256 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2021 Gen Hames
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "haptic",
3 | "version": "0.10.1",
4 | "description": "Reactive TSX library in 1.6kb with no compiler, no magic, and no virtual DOM.",
5 | "type": "module",
6 | "main": "./publish/index.js",
7 | "types": "./publish/index.d.ts",
8 | "license": "MIT",
9 | "author": "Gen Hames",
10 | "exports": {
11 | ".": {
12 | "import": "./publish/index.js",
13 | "require": "./publish/index.cjs"
14 | },
15 | "./dom": {
16 | "import": "./publish/dom/index.js",
17 | "require": "./publish/dom/index.cjs"
18 | },
19 | "./state": {
20 | "import": "./publish/state/index.js",
21 | "require": "./publish/state/index.cjs"
22 | },
23 | "./stdlib": {
24 | "import": "./publish/stdlib/index.js",
25 | "require": "./publish/stdlib/index.cjs"
26 | }
27 | },
28 | "keywords": [
29 | "reactive",
30 | "dom",
31 | "tsx",
32 | "frontend",
33 | "framework"
34 | ],
35 | "repository": {
36 | "type": "git",
37 | "url": "https://github.com/heyheyhello/haptic"
38 | },
39 | "scripts": {
40 | "build": "node --no-warnings --experimental-loader esbuild-node-loader build.ts",
41 | "test": "node --no-warnings --experimental-loader esbuild-node-loader test.ts",
42 | "gen-dts": "tsc --project tsconfig.json",
43 | "bundlesize": "echo $(esbuild --bundle src/bundle.ts --format=esm --minify --define:S_RUNNING=4 --define:S_SKIP_RUN_QUEUE=2 --define:S_NEEDS_RUN=1 | gzip -9 | wc -c) min+gzip bytes"
44 | },
45 | "devDependencies": {
46 | "@types/node": "^16.4.12",
47 | "@typescript-eslint/eslint-plugin": "^4.29.0",
48 | "@typescript-eslint/parser": "^4.29.0",
49 | "esbuild": "^0.12.18",
50 | "esbuild-node-loader": "^0.1.1",
51 | "eslint": "^7.32.0",
52 | "eslint-plugin-react": "^7.24.0",
53 | "fflate": "^0.7.1",
54 | "typescript": "^4.3.5",
55 | "zora": "^5.0.0",
56 | "zora-reporters": "^1.0.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/publish/dom/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { GenericEventAttrs, HTMLAttrs, SVGAttrs, HTMLElements, SVGElements } from '../jsx';
2 |
3 | type El = Element | Node | DocumentFragment;
4 | type Tag = El | Component | [] | string;
5 | type Component = (...args: unknown[]) => El | undefined;
6 |
7 | declare function h(tag: Tag, props?: unknown, ...children: unknown[]): El | undefined;
8 | declare namespace h {
9 | namespace JSX {
10 | type Element = HTMLElement | SVGElement | DocumentFragment;
11 | interface ElementAttributesProperty {
12 | props: unknown;
13 | }
14 | interface ElementChildrenAttribute {
15 | children: unknown;
16 | }
17 | interface IntrinsicAttributes {
18 | children?: never;
19 | }
20 | type DOMAttributes
21 | = GenericEventAttrs
22 | & { children?: unknown };
23 | type HTMLAttributes
24 | = HTMLAttrs
25 | & DOMAttributes;
26 | type SVGAttributes
27 | = SVGAttrs
28 | & HTMLAttributes;
29 | type IntrinsicElements
30 | = { [El in keyof HTMLElements]: HTMLAttributes }
31 | & { [El in keyof SVGElements]: SVGAttributes };
32 | }
33 | }
34 |
35 | /** Renders SVGs by setting h() to the SVG namespace */
36 | declare function svg Node>(closure: T): ReturnType;
37 |
38 | type Frag = { _startMark: Text };
39 | declare const api: {
40 | /** Hyperscript reviver */
41 | h: typeof h;
42 | /** Add a node before a reference node or at the end */
43 | add: (parent: Node, value: unknown, endMark?: Node) => Node | Frag;
44 | /** Insert a node into an existing node */
45 | insert: (el: Node, value: unknown, endMark?: Node, current?: Node | Frag, startNode?: ChildNode | null) => Node | Frag | undefined;
46 | /** Set attributes and propeties on a node */
47 | property: (el: Node, value: unknown, name: string | null, isAttr?: boolean, isCss?: boolean) => void;
48 | /** Removes nodes, starting from `startNode` (inclusive) to `endMark` (exclusive) */
49 | rm: (parent: Node, startNode: ChildNode | null, endMark: Node) => void;
50 | /** DOM patcher. Receives unknown JSX elements and attributes. To mark the DOM
51 | location as reactive, return true. Call patchDOM() anytime to update. */
52 | patch: (value: unknown, patchDOM?: (value: unknown) => void, el?: Node, attribute?: string) => boolean;
53 | /** Element namespace URL such as SVG or MathML */
54 | ns?: string;
55 | };
56 |
57 | export { h, svg, api };
58 | export type { Component, El, Tag };
59 |
--------------------------------------------------------------------------------
/publish/index.d.ts:
--------------------------------------------------------------------------------
1 | import { api as _api, svg } from './dom/index.js';
2 | import type { Component, El, Tag } from './dom/index.js';
3 | import type { Wire } from './state/index.js';
4 | import type { GenericEventAttrs, HTMLAttrs, SVGAttrs, HTMLElements, SVGElements } from './jsx';
5 |
6 | type DistributeWire = T extends any ? Wire : never;
7 |
8 | declare function h(tag: Tag, props?: unknown, ...children: unknown[]): El | undefined;
9 | declare namespace h {
10 | namespace JSX {
11 | type MaybeWire = T | DistributeWire;
12 | type AllowWireForProperties = {
13 | [K in keyof T]: MaybeWire;
14 | };
15 | type Element = HTMLElement | SVGElement | DocumentFragment;
16 | interface ElementAttributesProperty {
17 | props: unknown;
18 | }
19 | interface ElementChildrenAttribute {
20 | children: unknown;
21 | }
22 | interface IntrinsicAttributes {
23 | children?: never;
24 | }
25 | type DOMAttributes
26 | = GenericEventAttrs
27 | & { children?: unknown };
28 | type HTMLAttributes
29 | = AllowWireForProperties>
30 | & { style?: MaybeWire | { [key: string]: MaybeWire } }
31 | & DOMAttributes;
32 | type SVGAttributes
33 | = AllowWireForProperties
34 | & HTMLAttributes;
35 | type IntrinsicElements
36 | = { [El in keyof HTMLElements]: HTMLAttributes }
37 | & { [El in keyof SVGElements]: SVGAttributes };
38 | }
39 | }
40 |
41 | // Swap out h to have the correct JSX namespace
42 | declare const api: Omit & { h: typeof h };
43 |
44 | export { h, svg, api };
45 | export type { Component, El, Tag };
46 |
--------------------------------------------------------------------------------
/publish/jsx.d.ts:
--------------------------------------------------------------------------------
1 | // This file contains building blocks to help construct a JSX namespace
2 | export declare type HTMLElements = {
3 | a: HTMLAnchorElement;
4 | abbr: HTMLElement;
5 | address: HTMLElement;
6 | area: HTMLAreaElement;
7 | article: HTMLElement;
8 | aside: HTMLElement;
9 | audio: HTMLAudioElement;
10 | b: HTMLElement;
11 | base: HTMLBaseElement;
12 | bdi: HTMLElement;
13 | bdo: HTMLElement;
14 | big: HTMLElement;
15 | blockquote: HTMLQuoteElement;
16 | body: HTMLBodyElement;
17 | br: HTMLBRElement;
18 | button: HTMLButtonElement;
19 | canvas: HTMLCanvasElement;
20 | caption: HTMLTableCaptionElement;
21 | cite: HTMLElement;
22 | code: HTMLElement;
23 | col: HTMLTableColElement;
24 | colgroup: HTMLTableColElement;
25 | data: HTMLDataElement;
26 | datalist: HTMLDataListElement;
27 | dd: HTMLElement;
28 | del: HTMLModElement;
29 | details: HTMLDetailsElement;
30 | dfn: HTMLElement;
31 | dialog: HTMLDialogElement;
32 | div: HTMLDivElement;
33 | dl: HTMLDListElement;
34 | dt: HTMLElement;
35 | em: HTMLElement;
36 | embed: HTMLEmbedElement;
37 | fieldset: HTMLFieldSetElement;
38 | figcaption: HTMLElement;
39 | figure: HTMLElement;
40 | footer: HTMLElement;
41 | form: HTMLFormElement;
42 | h1: HTMLHeadingElement;
43 | h2: HTMLHeadingElement;
44 | h3: HTMLHeadingElement;
45 | h4: HTMLHeadingElement;
46 | h5: HTMLHeadingElement;
47 | h6: HTMLHeadingElement;
48 | head: HTMLHeadElement;
49 | header: HTMLElement;
50 | hgroup: HTMLElement;
51 | hr: HTMLHRElement;
52 | html: HTMLHtmlElement;
53 | i: HTMLElement;
54 | iframe: HTMLIFrameElement;
55 | img: HTMLImageElement;
56 | input: HTMLInputElement;
57 | ins: HTMLModElement;
58 | kbd: HTMLElement;
59 | keygen: HTMLUnknownElement;
60 | label: HTMLLabelElement;
61 | legend: HTMLLegendElement;
62 | li: HTMLLIElement;
63 | link: HTMLLinkElement;
64 | main: HTMLElement;
65 | map: HTMLMapElement;
66 | mark: HTMLElement;
67 | menu: HTMLMenuElement;
68 | menuitem: HTMLUnknownElement;
69 | meta: HTMLMetaElement;
70 | meter: HTMLMeterElement;
71 | nav: HTMLElement;
72 | noscript: HTMLElement;
73 | object: HTMLObjectElement;
74 | ol: HTMLOListElement;
75 | optgroup: HTMLOptGroupElement;
76 | option: HTMLOptionElement;
77 | output: HTMLOutputElement;
78 | p: HTMLParagraphElement;
79 | param: HTMLParamElement;
80 | picture: HTMLPictureElement;
81 | pre: HTMLPreElement;
82 | progress: HTMLProgressElement;
83 | q: HTMLQuoteElement;
84 | rp: HTMLElement;
85 | rt: HTMLElement;
86 | ruby: HTMLElement;
87 | s: HTMLElement;
88 | samp: HTMLElement;
89 | script: HTMLScriptElement;
90 | section: HTMLElement;
91 | select: HTMLSelectElement;
92 | slot: HTMLSlotElement;
93 | small: HTMLElement;
94 | source: HTMLSourceElement;
95 | span: HTMLSpanElement;
96 | strong: HTMLElement;
97 | style: HTMLStyleElement;
98 | sub: HTMLElement;
99 | summary: HTMLElement;
100 | sup: HTMLElement;
101 | table: HTMLTableElement;
102 | tbody: HTMLTableSectionElement;
103 | td: HTMLTableCellElement;
104 | textarea: HTMLTextAreaElement;
105 | tfoot: HTMLTableSectionElement;
106 | th: HTMLTableCellElement;
107 | thead: HTMLTableSectionElement;
108 | time: HTMLTimeElement;
109 | title: HTMLTitleElement;
110 | tr: HTMLTableRowElement;
111 | track: HTMLTrackElement;
112 | u: HTMLElement;
113 | ul: HTMLUListElement;
114 | var: HTMLElement;
115 | video: HTMLVideoElement;
116 | wbr: HTMLElement;
117 | };
118 |
119 | export declare type SVGElements = {
120 | svg: SVGSVGElement;
121 | animate: SVGAnimateElement;
122 | circle: SVGCircleElement;
123 | clipPath: SVGClipPathElement;
124 | defs: SVGDefsElement;
125 | desc: SVGDescElement;
126 | ellipse: SVGEllipseElement;
127 | feBlend: SVGFEBlendElement;
128 | feColorMatrix: SVGFEColorMatrixElement;
129 | feComponentTransfer: SVGFEComponentTransferElement;
130 | feComposite: SVGFECompositeElement;
131 | feConvolveMatrix: SVGFEConvolveMatrixElement;
132 | feDiffuseLighting: SVGFEDiffuseLightingElement;
133 | feDisplacementMap: SVGFEDisplacementMapElement;
134 | feFlood: SVGFEFloodElement;
135 | feGaussianBlur: SVGFEGaussianBlurElement;
136 | feImage: SVGFEImageElement;
137 | feMerge: SVGFEMergeElement;
138 | feMergeNode: SVGFEMergeNodeElement;
139 | feMorphology: SVGFEMorphologyElement;
140 | feOffset: SVGFEOffsetElement;
141 | feSpecularLighting: SVGFESpecularLightingElement;
142 | feTile: SVGFETileElement;
143 | feTurbulence: SVGFETurbulenceElement;
144 | filter: SVGFilterElement;
145 | foreignObject: SVGForeignObjectElement;
146 | g: SVGGElement;
147 | image: SVGImageElement;
148 | line: SVGLineElement;
149 | linearGradient: SVGLinearGradientElement;
150 | marker: SVGMarkerElement;
151 | mask: SVGMaskElement;
152 | path: SVGPathElement;
153 | pattern: SVGPatternElement;
154 | polygon: SVGPolygonElement;
155 | polyline: SVGPolylineElement;
156 | radialGradient: SVGRadialGradientElement;
157 | rect: SVGRectElement;
158 | stop: SVGStopElement;
159 | symbol: SVGSymbolElement;
160 | text: SVGTextElement;
161 | tspan: SVGTSpanElement;
162 | use: SVGUseElement;
163 | };
164 |
165 | type TargetedEvent
166 |
167 | = Omit
168 | & { readonly currentTarget: Target; };
169 |
170 | type EventHandler = { (event: E): void; }
171 |
172 | type AnimationEventHandler
173 | = EventHandler>;
174 | type ClipboardEventHandler
175 | = EventHandler>;
176 | type CompositionEventHandler
177 | = EventHandler>;
178 | type DragEventHandler
179 | = EventHandler>;
180 | type FocusEventHandler
181 | = EventHandler>;
182 | type GenericEventHandler
183 | = EventHandler>;
184 | type KeyboardEventHandler
185 | = EventHandler>;
186 | type MouseEventHandler
187 | = EventHandler>;
188 | type PointerEventHandler
189 | = EventHandler>;
190 | type TouchEventHandler
191 | = EventHandler>;
192 | type TransitionEventHandler
193 | = EventHandler>;
194 | type UIEventHandler
195 | = EventHandler>;
196 | type WheelEventHandler
197 | = EventHandler>;
198 |
199 | // Receives an element as Target such as HTMLDivElement
200 | export declare type GenericEventAttrs = {
201 | // Image Events
202 | onLoad?: GenericEventHandler;
203 | onLoadCapture?: GenericEventHandler;
204 | onError?: GenericEventHandler;
205 | onErrorCapture?: GenericEventHandler;
206 |
207 | // Clipboard Events
208 | onCopy?: ClipboardEventHandler;
209 | onCopyCapture?: ClipboardEventHandler;
210 | onCut?: ClipboardEventHandler;
211 | onCutCapture?: ClipboardEventHandler;
212 | onPaste?: ClipboardEventHandler;
213 | onPasteCapture?: ClipboardEventHandler;
214 |
215 | // Composition Events
216 | onCompositionEnd?: CompositionEventHandler;
217 | onCompositionEndCapture?: CompositionEventHandler;
218 | onCompositionStart?: CompositionEventHandler;
219 | onCompositionStartCapture?: CompositionEventHandler;
220 | onCompositionUpdate?: CompositionEventHandler;
221 | onCompositionUpdateCapture?: CompositionEventHandler;
222 |
223 | // Details Events
224 | onToggle?: GenericEventHandler;
225 |
226 | // Focus Events
227 | onFocus?: FocusEventHandler;
228 | onFocusCapture?: FocusEventHandler;
229 | onBlur?: FocusEventHandler;
230 | onBlurCapture?: FocusEventHandler;
231 |
232 | // Form Events
233 | onChange?: GenericEventHandler;
234 | onChangeCapture?: GenericEventHandler;
235 | onInput?: GenericEventHandler;
236 | onInputCapture?: GenericEventHandler;
237 | onSearch?: GenericEventHandler;
238 | onSearchCapture?: GenericEventHandler;
239 | onSubmit?: GenericEventHandler;
240 | onSubmitCapture?: GenericEventHandler;
241 | onInvalid?: GenericEventHandler;
242 | onInvalidCapture?: GenericEventHandler;
243 |
244 | // Keyboard Events
245 | onKeyDown?: KeyboardEventHandler;
246 | onKeyDownCapture?: KeyboardEventHandler;
247 | onKeyPress?: KeyboardEventHandler;
248 | onKeyPressCapture?: KeyboardEventHandler;
249 | onKeyUp?: KeyboardEventHandler;
250 | onKeyUpCapture?: KeyboardEventHandler;
251 |
252 | // Media Events
253 | onAbort?: GenericEventHandler;
254 | onAbortCapture?: GenericEventHandler;
255 | onCanPlay?: GenericEventHandler;
256 | onCanPlayCapture?: GenericEventHandler;
257 | onCanPlayThrough?: GenericEventHandler;
258 | onCanPlayThroughCapture?: GenericEventHandler;
259 | onDurationChange?: GenericEventHandler;
260 | onDurationChangeCapture?: GenericEventHandler;
261 | onEmptied?: GenericEventHandler;
262 | onEmptiedCapture?: GenericEventHandler;
263 | onEncrypted?: GenericEventHandler;
264 | onEncryptedCapture?: GenericEventHandler;
265 | onEnded?: GenericEventHandler;
266 | onEndedCapture?: GenericEventHandler;
267 | onLoadedData?: GenericEventHandler;
268 | onLoadedDataCapture?: GenericEventHandler;
269 | onLoadedMetadata?: GenericEventHandler;
270 | onLoadedMetadataCapture?: GenericEventHandler;
271 | onLoadStart?: GenericEventHandler;
272 | onLoadStartCapture?: GenericEventHandler;
273 | onPause?: GenericEventHandler;
274 | onPauseCapture?: GenericEventHandler;
275 | onPlay?: GenericEventHandler;
276 | onPlayCapture?: GenericEventHandler;
277 | onPlaying?: GenericEventHandler;
278 | onPlayingCapture?: GenericEventHandler;
279 | onProgress?: GenericEventHandler;
280 | onProgressCapture?: GenericEventHandler;
281 | onRateChange?: GenericEventHandler;
282 | onRateChangeCapture?: GenericEventHandler;
283 | onSeeked?: GenericEventHandler;
284 | onSeekedCapture?: GenericEventHandler;
285 | onSeeking?: GenericEventHandler;
286 | onSeekingCapture?: GenericEventHandler;
287 | onStalled?: GenericEventHandler;
288 | onStalledCapture?: GenericEventHandler;
289 | onSuspend?: GenericEventHandler;
290 | onSuspendCapture?: GenericEventHandler;
291 | onTimeUpdate?: GenericEventHandler;
292 | onTimeUpdateCapture?: GenericEventHandler;
293 | onVolumeChange?: GenericEventHandler;
294 | onVolumeChangeCapture?: GenericEventHandler;
295 | onWaiting?: GenericEventHandler;
296 | onWaitingCapture?: GenericEventHandler;
297 |
298 | // MouseEvents
299 | onClick?: MouseEventHandler;
300 | onClickCapture?: MouseEventHandler;
301 | onContextMenu?: MouseEventHandler;
302 | onContextMenuCapture?: MouseEventHandler;
303 | onDblClick?: MouseEventHandler;
304 | onDblClickCapture?: MouseEventHandler;
305 | onDrag?: DragEventHandler;
306 | onDragCapture?: DragEventHandler;
307 | onDragEnd?: DragEventHandler;
308 | onDragEndCapture?: DragEventHandler;
309 | onDragEnter?: DragEventHandler;
310 | onDragEnterCapture?: DragEventHandler;
311 | onDragExit?: DragEventHandler;
312 | onDragExitCapture?: DragEventHandler;
313 | onDragLeave?: DragEventHandler;
314 | onDragLeaveCapture?: DragEventHandler;
315 | onDragOver?: DragEventHandler;
316 | onDragOverCapture?: DragEventHandler;
317 | onDragStart?: DragEventHandler;
318 | onDragStartCapture?: DragEventHandler;
319 | onDrop?: DragEventHandler;
320 | onDropCapture?: DragEventHandler;
321 | onMouseDown?: MouseEventHandler;
322 | onMouseDownCapture?: MouseEventHandler;
323 | onMouseEnter?: MouseEventHandler;
324 | onMouseEnterCapture?: MouseEventHandler;
325 | onMouseLeave?: MouseEventHandler;
326 | onMouseLeaveCapture?: MouseEventHandler;
327 | onMouseMove?: MouseEventHandler;
328 | onMouseMoveCapture?: MouseEventHandler;
329 | onMouseOut?: MouseEventHandler;
330 | onMouseOutCapture?: MouseEventHandler;
331 | onMouseOver?: MouseEventHandler;
332 | onMouseOverCapture?: MouseEventHandler;
333 | onMouseUp?: MouseEventHandler;
334 | onMouseUpCapture?: MouseEventHandler;
335 |
336 | // Selection Events
337 | onSelect?: GenericEventHandler;
338 | onSelectCapture?: GenericEventHandler;
339 |
340 | // Touch Events
341 | onTouchCancel?: TouchEventHandler;
342 | onTouchCancelCapture?: TouchEventHandler;
343 | onTouchEnd?: TouchEventHandler;
344 | onTouchEndCapture?: TouchEventHandler;
345 | onTouchMove?: TouchEventHandler;
346 | onTouchMoveCapture?: TouchEventHandler;
347 | onTouchStart?: TouchEventHandler;
348 | onTouchStartCapture?: TouchEventHandler;
349 |
350 | // Pointer Events
351 | onPointerOver?: PointerEventHandler;
352 | onPointerOverCapture?: PointerEventHandler;
353 | onPointerEnter?: PointerEventHandler;
354 | onPointerEnterCapture?: PointerEventHandler;
355 | onPointerDown?: PointerEventHandler;
356 | onPointerDownCapture?: PointerEventHandler;
357 | onPointerMove?: PointerEventHandler;
358 | onPointerMoveCapture?: PointerEventHandler;
359 | onPointerUp?: PointerEventHandler;
360 | onPointerUpCapture?: PointerEventHandler;
361 | onPointerCancel?: PointerEventHandler;
362 | onPointerCancelCapture?: PointerEventHandler;
363 | onPointerOut?: PointerEventHandler;
364 | onPointerOutCapture?: PointerEventHandler;
365 | onPointerLeave?: PointerEventHandler;
366 | onPointerLeaveCapture?: PointerEventHandler;
367 | onGotPointerCapture?: PointerEventHandler;
368 | onGotPointerCaptureCapture?: PointerEventHandler;
369 | onLostPointerCapture?: PointerEventHandler;
370 | onLostPointerCaptureCapture?: PointerEventHandler;
371 |
372 | // UI Events
373 | onScroll?: UIEventHandler;
374 | onScrollCapture?: UIEventHandler;
375 |
376 | // Wheel Events
377 | onWheel?: WheelEventHandler;
378 | onWheelCapture?: WheelEventHandler;
379 |
380 | // Animation Events
381 | onAnimationStart?: AnimationEventHandler;
382 | onAnimationStartCapture?: AnimationEventHandler;
383 | onAnimationEnd?: AnimationEventHandler;
384 | onAnimationEndCapture?: AnimationEventHandler;
385 | onAnimationIteration?: AnimationEventHandler;
386 | onAnimationIterationCapture?: AnimationEventHandler;
387 |
388 | // Transition Events
389 | onTransitionEnd?: TransitionEventHandler;
390 | onTransitionEndCapture?: TransitionEventHandler;
391 | };
392 |
393 | // Note: HTML elements will also need GenericEventAttributes
394 | export declare type HTMLAttrs = {
395 | // Standard HTML Attributes
396 | accept?: string;
397 | acceptCharset?: string;
398 | accessKey?: string;
399 | action?: string;
400 | allowFullScreen?: boolean;
401 | allowTransparency?: boolean;
402 | alt?: string;
403 | as?: string;
404 | async?: boolean;
405 | autocomplete?: string;
406 | autoComplete?: string;
407 | autocorrect?: string;
408 | autoCorrect?: string;
409 | autofocus?: boolean;
410 | autoFocus?: boolean;
411 | autoPlay?: boolean;
412 | capture?: boolean;
413 | cellPadding?: number | string;
414 | cellSpacing?: number | string;
415 | charSet?: string;
416 | challenge?: string;
417 | checked?: boolean;
418 | class?: string;
419 | className?: string;
420 | cols?: number;
421 | colSpan?: number;
422 | content?: string;
423 | contentEditable?: boolean;
424 | contextMenu?: string;
425 | controls?: boolean;
426 | controlsList?: string;
427 | coords?: string;
428 | crossOrigin?: string;
429 | data?: string;
430 | dateTime?: string;
431 | default?: boolean;
432 | defer?: boolean;
433 | dir?: 'auto' | 'rtl' | 'ltr';
434 | disabled?: boolean;
435 | disableRemotePlayback?: boolean;
436 | download?: unknown;
437 | draggable?: boolean;
438 | encType?: string;
439 | form?: string;
440 | formAction?: string;
441 | formEncType?: string;
442 | formMethod?: string;
443 | formNoValidate?: boolean;
444 | formTarget?: string;
445 | frameBorder?: number | string;
446 | headers?: string;
447 | height?: number | string;
448 | hidden?: boolean;
449 | high?: number;
450 | href?: string;
451 | hrefLang?: string;
452 | for?: string;
453 | htmlFor?: string;
454 | httpEquiv?: string;
455 | icon?: string;
456 | id?: string;
457 | inputMode?: string;
458 | integrity?: string;
459 | is?: string;
460 | keyParams?: string;
461 | keyType?: string;
462 | kind?: string;
463 | label?: string;
464 | lang?: string;
465 | list?: string;
466 | loop?: boolean;
467 | low?: number;
468 | manifest?: string;
469 | marginHeight?: number;
470 | marginWidth?: number;
471 | max?: number | string;
472 | maxLength?: number;
473 | media?: string;
474 | mediaGroup?: string;
475 | method?: string;
476 | min?: number | string;
477 | minLength?: number;
478 | multiple?: boolean;
479 | muted?: boolean;
480 | name?: string;
481 | nonce?: string;
482 | noValidate?: boolean;
483 | open?: boolean;
484 | optimum?: number;
485 | pattern?: string;
486 | placeholder?: string;
487 | playsInline?: boolean;
488 | poster?: string;
489 | preload?: string;
490 | radioGroup?: string;
491 | readOnly?: boolean;
492 | rel?: string;
493 | required?: boolean;
494 | role?: string;
495 | rows?: number;
496 | rowSpan?: number;
497 | sandbox?: string;
498 | scope?: string;
499 | scoped?: boolean;
500 | scrolling?: string;
501 | seamless?: boolean;
502 | selected?: boolean;
503 | shape?: string;
504 | size?: number;
505 | sizes?: string;
506 | slot?: string;
507 | span?: number;
508 | spellcheck?: boolean;
509 | src?: string;
510 | srcset?: string;
511 | srcDoc?: string;
512 | srcLang?: string;
513 | srcSet?: string;
514 | start?: number;
515 | step?: number | string;
516 | style?: string | { [key: string]: string | number };
517 | summary?: string;
518 | tabIndex?: number;
519 | target?: string;
520 | title?: string;
521 | type?: string;
522 | useMap?: string;
523 | value?: string | string[] | number;
524 | volume?: string | number;
525 | width?: number | string;
526 | wmode?: string;
527 | wrap?: string;
528 |
529 | // RDFa Attributes
530 | about?: string;
531 | datatype?: string;
532 | inlist?: unknown;
533 | prefix?: string;
534 | property?: string;
535 | resource?: string;
536 | typeof?: string;
537 | vocab?: string;
538 |
539 | // Microdata Attributes
540 | itemProp?: string;
541 | itemScope?: boolean;
542 | itemType?: string;
543 | itemID?: string;
544 | itemRef?: string;
545 | };
546 |
547 | // Note: SVG elements will also need HTMLAttributes and GenericEventAttributes
548 | export declare type SVGAttrs = {
549 | accentHeight?: number | string;
550 | accumulate?: 'none' | 'sum';
551 | additive?: 'replace' | 'sum';
552 | alignmentBaseline?:
553 | | 'auto'
554 | | 'baseline'
555 | | 'before-edge'
556 | | 'text-before-edge'
557 | | 'middle'
558 | | 'central'
559 | | 'after-edge'
560 | | 'text-after-edge'
561 | | 'ideographic'
562 | | 'alphabetic'
563 | | 'hanging'
564 | | 'mathematical'
565 | | 'inherit';
566 | allowReorder?: 'no' | 'yes';
567 | alphabetic?: number | string;
568 | amplitude?: number | string;
569 | arabicForm?: 'initial' | 'medial' | 'terminal' | 'isolated';
570 | ascent?: number | string;
571 | attributeName?: string;
572 | attributeType?: string;
573 | autoReverse?: number | string;
574 | azimuth?: number | string;
575 | baseFrequency?: number | string;
576 | baselineShift?: number | string;
577 | baseProfile?: number | string;
578 | bbox?: number | string;
579 | begin?: number | string;
580 | bias?: number | string;
581 | by?: number | string;
582 | calcMode?: number | string;
583 | capHeight?: number | string;
584 | clip?: number | string;
585 | clipPath?: string;
586 | clipPathUnits?: number | string;
587 | clipRule?: number | string;
588 | colorInterpolation?: number | string;
589 | colorInterpolationFilters?: 'auto' | 'sRGB' | 'linearRGB' | 'inherit';
590 | colorProfile?: number | string;
591 | colorRendering?: number | string;
592 | contentScriptType?: number | string;
593 | contentStyleType?: number | string;
594 | cursor?: number | string;
595 | cx?: number | string;
596 | cy?: number | string;
597 | d?: string;
598 | decelerate?: number | string;
599 | descent?: number | string;
600 | diffuseConstant?: number | string;
601 | direction?: number | string;
602 | display?: number | string;
603 | divisor?: number | string;
604 | dominantBaseline?: number | string;
605 | dur?: number | string;
606 | dx?: number | string;
607 | dy?: number | string;
608 | edgeMode?: number | string;
609 | elevation?: number | string;
610 | enableBackground?: number | string;
611 | end?: number | string;
612 | exponent?: number | string;
613 | externalResourcesRequired?: number | string;
614 | fill?: string;
615 | fillOpacity?: number | string;
616 | fillRule?: 'nonzero' | 'evenodd' | 'inherit';
617 | filter?: string;
618 | filterRes?: number | string;
619 | filterUnits?: number | string;
620 | floodColor?: number | string;
621 | floodOpacity?: number | string;
622 | focusable?: number | string;
623 | fontFamily?: string;
624 | fontSize?: number | string;
625 | fontSizeAdjust?: number | string;
626 | fontStretch?: number | string;
627 | fontStyle?: number | string;
628 | fontVariant?: number | string;
629 | fontWeight?: number | string;
630 | format?: number | string;
631 | from?: number | string;
632 | fx?: number | string;
633 | fy?: number | string;
634 | g1?: number | string;
635 | g2?: number | string;
636 | glyphName?: number | string;
637 | glyphOrientationHorizontal?: number | string;
638 | glyphOrientationVertical?: number | string;
639 | glyphRef?: number | string;
640 | gradientTransform?: string;
641 | gradientUnits?: string;
642 | hanging?: number | string;
643 | horizAdvX?: number | string;
644 | horizOriginX?: number | string;
645 | ideographic?: number | string;
646 | imageRendering?: number | string;
647 | in2?: number | string;
648 | in?: string;
649 | intercept?: number | string;
650 | k1?: number | string;
651 | k2?: number | string;
652 | k3?: number | string;
653 | k4?: number | string;
654 | k?: number | string;
655 | kernelMatrix?: number | string;
656 | kernelUnitLength?: number | string;
657 | kerning?: number | string;
658 | keyPoints?: number | string;
659 | keySplines?: number | string;
660 | keyTimes?: number | string;
661 | lengthAdjust?: number | string;
662 | letterSpacing?: number | string;
663 | lightingColor?: number | string;
664 | limitingConeAngle?: number | string;
665 | local?: number | string;
666 | markerEnd?: string;
667 | markerHeight?: number | string;
668 | markerMid?: string;
669 | markerStart?: string;
670 | markerUnits?: number | string;
671 | markerWidth?: number | string;
672 | mask?: string;
673 | maskContentUnits?: number | string;
674 | maskUnits?: number | string;
675 | mathematical?: number | string;
676 | mode?: number | string;
677 | numOctaves?: number | string;
678 | offset?: number | string;
679 | opacity?: number | string;
680 | operator?: number | string;
681 | order?: number | string;
682 | orient?: number | string;
683 | orientation?: number | string;
684 | origin?: number | string;
685 | overflow?: number | string;
686 | overlinePosition?: number | string;
687 | overlineThickness?: number | string;
688 | paintOrder?: number | string;
689 | panose1?: number | string;
690 | pathLength?: number | string;
691 | patternContentUnits?: string;
692 | patternTransform?: number | string;
693 | patternUnits?: string;
694 | pointerEvents?: number | string;
695 | points?: string;
696 | pointsAtX?: number | string;
697 | pointsAtY?: number | string;
698 | pointsAtZ?: number | string;
699 | preserveAlpha?: number | string;
700 | preserveAspectRatio?: string;
701 | primitiveUnits?: number | string;
702 | r?: number | string;
703 | radius?: number | string;
704 | refX?: number | string;
705 | refY?: number | string;
706 | renderingIntent?: number | string;
707 | repeatCount?: number | string;
708 | repeatDur?: number | string;
709 | requiredExtensions?: number | string;
710 | requiredFeatures?: number | string;
711 | restart?: number | string;
712 | result?: string;
713 | rotate?: number | string;
714 | rx?: number | string;
715 | ry?: number | string;
716 | scale?: number | string;
717 | seed?: number | string;
718 | shapeRendering?: number | string;
719 | slope?: number | string;
720 | spacing?: number | string;
721 | specularConstant?: number | string;
722 | specularExponent?: number | string;
723 | speed?: number | string;
724 | spreadMethod?: string;
725 | startOffset?: number | string;
726 | stdDeviation?: number | string;
727 | stemh?: number | string;
728 | stemv?: number | string;
729 | stitchTiles?: number | string;
730 | stopColor?: string;
731 | stopOpacity?: number | string;
732 | strikethroughPosition?: number | string;
733 | strikethroughThickness?: number | string;
734 | string?: number | string;
735 | stroke?: string;
736 | strokeDasharray?: string | number;
737 | strokeDashoffset?: string | number;
738 | strokeLinecap?: 'butt' | 'round' | 'square' | 'inherit';
739 | strokeLinejoin?: 'miter' | 'round' | 'bevel' | 'inherit';
740 | strokeMiterlimit?: string;
741 | strokeOpacity?: number | string;
742 | strokeWidth?: number | string;
743 | surfaceScale?: number | string;
744 | systemLanguage?: number | string;
745 | tableValues?: number | string;
746 | targetX?: number | string;
747 | targetY?: number | string;
748 | textAnchor?: string;
749 | textDecoration?: number | string;
750 | textLength?: number | string;
751 | textRendering?: number | string;
752 | to?: number | string;
753 | transform?: string;
754 | u1?: number | string;
755 | u2?: number | string;
756 | underlinePosition?: number | string;
757 | underlineThickness?: number | string;
758 | unicode?: number | string;
759 | unicodeBidi?: number | string;
760 | unicodeRange?: number | string;
761 | unitsPerEm?: number | string;
762 | vAlphabetic?: number | string;
763 | values?: string;
764 | vectorEffect?: number | string;
765 | version?: string;
766 | vertAdvY?: number | string;
767 | vertOriginX?: number | string;
768 | vertOriginY?: number | string;
769 | vHanging?: number | string;
770 | vIdeographic?: number | string;
771 | viewBox?: string;
772 | viewTarget?: number | string;
773 | visibility?: number | string;
774 | vMathematical?: number | string;
775 | widths?: number | string;
776 | wordSpacing?: number | string;
777 | writingMode?: number | string;
778 | x1?: number | string;
779 | x2?: number | string;
780 | x?: number | string;
781 | xChannelSelector?: string;
782 | xHeight?: number | string;
783 | xlinkActuate?: string;
784 | xlinkArcrole?: string;
785 | xlinkHref?: string;
786 | xlinkRole?: string;
787 | xlinkShow?: string;
788 | xlinkTitle?: string;
789 | xlinkType?: string;
790 | xmlBase?: string;
791 | xmlLang?: string;
792 | xmlns?: string;
793 | xmlnsXlink?: string;
794 | xmlSpace?: string;
795 | y1?: number | string;
796 | y2?: number | string;
797 | y?: number | string;
798 | yChannelSelector?: string;
799 | z?: number | string;
800 | zoomAndPan?: string;
801 | };
802 |
--------------------------------------------------------------------------------
/publish/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "haptic",
3 | "version": "0.10.1",
4 | "description": "Reactive TSX library in 1.6kb with no compiler, no magic, and no virtual DOM.",
5 | "type": "module",
6 | "main": "./index.js",
7 | "types": "./index.d.ts",
8 | "license": "MIT",
9 | "author": "Gen Hames",
10 | "exports": {
11 | ".": {
12 | "import": "./index.js",
13 | "require": "./index.cjs"
14 | },
15 | "./dom": {
16 | "import": "./dom/index.js",
17 | "require": "./dom/index.cjs"
18 | },
19 | "./state": {
20 | "import": "./state/index.js",
21 | "require": "./state/index.cjs"
22 | },
23 | "./stdlib": {
24 | "import": "./stdlib/index.js",
25 | "require": "./stdlib/index.cjs"
26 | }
27 | },
28 | "keywords": [
29 | "reactive",
30 | "dom",
31 | "tsx",
32 | "frontend",
33 | "framework"
34 | ],
35 | "repository": {
36 | "type": "git",
37 | "url": "https://github.com/heyheyhello/haptic"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/publish/state/index.d.ts:
--------------------------------------------------------------------------------
1 | declare type Signal = {
2 | /** Read value */
3 | (): T;
4 | /** Write value; notifying wires */
5 | (value: T): void;
6 | /** Read value & subscribe */
7 | ($: SubToken): T;
8 | /** Wires subscribed to this signal */
9 | wires: Set>;
10 | /** Transaction value; set and deleted on commit */
11 | next?: T;
12 | /** If this is a computed-signal, this is its wire */
13 | cw?: Wire;
14 | /** To check "if x is a signal" */
15 | $signal: 1;
16 | };
17 |
18 | declare type Wire = {
19 | /** Run the wire */
20 | (): T;
21 | /** Signals read-subscribed last run */
22 | sigRS: Set>;
23 | /** Signals read-passed last run */
24 | sigRP: Set>;
25 | /** Signals inherited from computed-signals, for consistent two-way linking */
26 | sigIC: Set>;
27 | /** Post-run tasks */
28 | tasks: Set<(nextValue: T) => void>;
29 | /** Wire that created this wire (parent of this child) */
30 | upper: Wire | undefined;
31 | /** Wires created during this run (children of this parent) */
32 | lower: Set>;
33 | /** FSM state 3-bit bitmask: [RUNNING][SKIP_RUN_QUEUE][NEEDS_RUN] */
34 | state: WireState;
35 | /** Run count */
36 | run: number;
37 | /** If part of a computed signal, this is its signal */
38 | cs?: Signal;
39 | /** To check "if x is a wire" */
40 | $wire: 1;
41 | };
42 |
43 | declare type SubToken = {
44 | /** Allow $(...signals) to return an array of read values */
45 | unknown>>(...args: U): {
46 | [P in keyof U]: U[P] extends Signal ? R : never;
47 | };
48 | /** Wire to subscribe to */
49 | wire: Wire;
50 | /** To check "if x is a subscription token" */
51 | $$: 1;
52 | };
53 |
54 | /** 3 bits: [RUNNING][SKIP_RUN_QUEUE][NEEDS_RUN] */
55 | declare type WireStateFields = {
56 | S_RUNNING: 4,
57 | S_SKIP_RUN_QUEUE: 2,
58 | S_NEEDS_RUN: 1,
59 | };
60 | /** 3 bits: [RUNNING][SKIP_RUN_QUEUE][NEEDS_RUN] */
61 | declare type WireState = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
62 | declare type X = any;
63 |
64 | /**
65 | * Void subcription token. Used when a function demands a token but you don't
66 | * want to consent to any signal subscriptions. */
67 | declare const v$: SubToken;
68 |
69 | /**
70 | * Create a wire. Activate the wire by running it (function call). Any signals
71 | * that read-subscribed during the run will re-run the wire later when written
72 | * to. Wires can be run anytime manually. They're pausable and are resumed when
73 | * called; resuming will avoid a wire run if the wire is not stale. Wires are
74 | * named by their function's name and a counter. */
75 | declare const createWire: (fn: ($: SubToken) => T) => Wire;
76 |
77 | /**
78 | * Removes two-way subscriptions between its signals and itself. This also turns
79 | * off the wire until it is manually re-run. */
80 | declare const wireReset: (wire: Wire) => void;
81 |
82 | /**
83 | * Pauses a wire so signal writes won't cause runs. Affects nested wires */
84 | declare const wirePause: (wire: Wire) => void;
85 |
86 | /**
87 | * Resumes a paused wire. Affects nested wires but skips wires belonging to
88 | * computed-signals. Returns true if any runs were missed during the pause */
89 | declare const wireResume: (wire: Wire) => boolean;
90 |
91 | /**
92 | * Creates signals for each object entry. Signals are read/write variables which
93 | * hold a list of subscribed wires. When a value is written those wires are
94 | * re-run. Writing a wire into a signal creates a lazy computed-signal. Signals
95 | * are named by the key of the object entry and a global counter. */
96 | declare const createSignal: {
97 | (obj: T): { [K in keyof T]: Signal ? R : T[K]>; };
98 | anon: (value: T_1, id?: string) => Signal;
99 | };
100 | /**
101 | * Batch signal writes so only the last write per signal is applied. Values are
102 | * committed at the end of the function call. */
103 | declare const transaction: (fn: () => T) => T;
104 |
105 | /**
106 | * Run a function within the context of a wire. Nested children wires are
107 | * adopted (see wire.lower). Also affects signal read consistency checks for
108 | * read-pass (signal.sigRP) and read-subscribe (signal.sigRS). */
109 | declare const wireAdopt: (wire: Wire | undefined, fn: () => T) => void;
110 |
111 | export {
112 | createSignal as signal,
113 | createWire as wire,
114 | wireReset,
115 | wirePause,
116 | wireResume,
117 | wireAdopt,
118 | transaction,
119 | v$
120 | };
121 | export type { Signal, Wire, WireState, WireStateFields, SubToken };
122 |
--------------------------------------------------------------------------------
/publish/stdlib/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { Wire, SubToken } from '../state/index.js';
2 | import type { El } from '../dom/index.js';
3 |
4 | /** Switches DOM content when signals of the condition wire are written to */
5 | declare const when: (
6 | condition: ($: SubToken) => T,
7 | views: { [k in T]?: (() => El) | undefined; }
8 | ) => Wire;
9 |
10 | export { when };
11 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Haptic
2 |
3 | Reactive web rendering in TSX with no virtual DOM, no compilers, no
4 | dependencies, and no magic.
5 |
6 | It's less than 1600 bytes min+gz.
7 |
8 | ```tsx
9 | import { h } from 'haptic';
10 | import { signal, wire } from 'haptic/state';
11 | import { when } from 'haptic/stdlib';
12 |
13 | const state = signal({
14 | text: '',
15 | count: 0,
16 | });
17 |
18 | const Page = () =>
19 |
In {wire($ => 5 - state.count($))} clicks the content will change
31 | {when($ => state.count($) > 5 ? "T" : "F", {
32 | T: () => There are over 5 clicks!,
33 | F: () =>
Clicks: {wire(state.count)}
,
34 | })}
35 |
;
36 |
37 | document.body.appendChild();
38 | ```
39 |
40 | Haptic is small and explicit because it was born out of JavaScript Fatigue. It
41 | runs in vanilla JS environments and renders using the DOM. Embrace the modern
42 | web; step away from compilers, customs DSLs, and DOM diffing.
43 |
44 | Developers often drown in the over-engineering of their own tools, raising the
45 | barrier to entry for new developers and wasting time. Instead, Haptic focuses on
46 | a modern and reliable developer experience:
47 |
48 | - __Writing in the editor__ leverages TypeScript to provide strong type feedback
49 | and verify code before it's even run. JSDoc comments also supply documentation
50 | when hovering over all exports.
51 |
52 | - __Testing at runtime__ behaves as you'd expect; a div is a div. It's also
53 | nicely debuggable with good error messages and by promoting code styles that
54 | naturally name items in ways that show up in console logs and stacktraces.
55 | It's subtle, but it's especially helpful for reviewing reactive subscriptions.
56 | You'll thank me later.
57 |
58 | - __Optimizing code__ is something you can do by hand. Haptic let's you write
59 | modern reactive web apps and still understand every part of the code. You
60 | don't need to know how Haptic works to use it, but you're in good company if
61 | you ever look under the hood. It's only ~600 lines of well-documented source
62 | code; 340 of which is the single-file reactive state engine.
63 |
64 | ## Install
65 |
66 | ```
67 | npm install --save haptic
68 | ```
69 |
70 | Alternatively link directly to the module bundle on Skypack or UNPKG such as
71 | https://unpkg.com/haptic?module for an unbundled ESM script.
72 |
73 | ## Packages
74 |
75 | Haptic is a small collection of packages. This keeps things lightweight and
76 | helps you only import what you'd like. Each package can be used on its own.
77 |
78 | The `haptic` package is simply a wrapper of `haptic/dom` that's configured to
79 | use `haptic/state` for reactivity; it's really only 150 characters minified.
80 |
81 | Rendering is handled in `haptic/dom` and supports any reactive library including
82 | none at all. Reactivity and state is provided by `haptic/state`. Framework
83 | features are part of the standard library in `haptic/stdlib`.
84 |
85 | ### [haptic/dom](./src/dom/readme.md)
86 |
87 | ### [haptic/state](./src/state/readme.md)
88 |
89 | ### [haptic/stdlib](./src/stdlib/readme.md)
90 |
91 | ## Motivation
92 |
93 | Haptic started as a port of Sinuous to TS that used TSX instead of HTML tag
94 | templates. The focus shifted to type safety, debugging, leveraging the editor,
95 | and eventually designing a new reactive state engine from scratch after
96 | influence from Sinuous, Solid, S.js, Reactor.js, and Dipole.
97 |
98 | Hyperscript code is still largely borrowed from Sinuous and Haptic maintains the
99 | same modular API with the new addition of `api.patch`.
100 |
--------------------------------------------------------------------------------
/src/bundle.ts:
--------------------------------------------------------------------------------
1 | // For testing bundle size with `npm run bundlesize`. This assumes they're using
2 | // the default `api` and won't use `svg()`.
3 | export { h } from './index.js';
4 | export { signal, wire } from './state/index.js';
5 |
6 | /*
7 | > esbuild
8 | --bundle src/bundle.ts
9 | --format=esm
10 | --minify
11 | --define:S_RUNNING=4
12 | --define:S_SKIP_RUN_QUEUE=2
13 | --define:S_NEEDS_RUN=1 | gzip -9 | wc -c
14 | */
15 |
--------------------------------------------------------------------------------
/src/dom/h.ts:
--------------------------------------------------------------------------------
1 | import { api } from './index.js';
2 |
3 | import type { GenericEventAttrs, HTMLAttrs, SVGAttrs, HTMLElements, SVGElements } from '../jsx';
4 |
5 | type El = Element | Node | DocumentFragment;
6 | type Tag = El | Component | [] | string;
7 | type Component = (...args: unknown[]) => El | undefined;
8 |
9 | function h(tag: Tag, props?: unknown, ...children: unknown[]): El | undefined
10 | function h(tag: Tag, ...args: unknown[]): El | undefined {
11 | if (typeof tag === 'function') {
12 | return tag(...args);
13 | }
14 | let el: El, arg: unknown;
15 | if (typeof tag === 'string') {
16 | el = api.ns
17 | ? document.createElementNS(api.ns, tag)
18 | : document.createElement(tag);
19 | }
20 | else if (Array.isArray(tag)) {
21 | el = document.createDocumentFragment();
22 | // Using unshift(tag) is -1b gz smaller but is an extra loop iteration
23 | args.unshift(...tag);
24 | }
25 | // Hopefully Element, Node, DocumentFragment, but could be anything...
26 | else {
27 | el = tag;
28 | }
29 | while (args.length) {
30 | arg = args.shift();
31 | // eslint-disable-next-line eqeqeq
32 | if (arg == null) {}
33 | else if (typeof arg === 'string' || arg instanceof Node) {
34 | // Direct add fast path
35 | api.add(el, arg);
36 | }
37 | else if (Array.isArray(arg)) {
38 | args.unshift(...arg);
39 | }
40 | else if (typeof arg === 'object') {
41 | // eslint-disable-next-line no-implicit-coercion
42 | api.property(el, arg, null, !!api.ns);
43 | }
44 | else if (api.patch(arg)) {
45 | // Last parameter, endMark, is a Text('') node; see nodeAdd.js#Frag
46 | api.insert(el, arg, api.add(el, '') as Text);
47 | }
48 | else {
49 | // Default case, cast as string and add
50 | // eslint-disable-next-line no-implicit-coercion,@typescript-eslint/restrict-plus-operands
51 | api.add(el, '' + arg);
52 | }
53 | }
54 | return el;
55 | }
56 |
57 | export { h };
58 | export type { Component, El, Tag };
59 |
60 | // JSX namespace must be bound into a function() next to its definition
61 | declare namespace h {
62 | export namespace JSX {
63 | type Element = HTMLElement | SVGElement | DocumentFragment;
64 |
65 | interface ElementAttributesProperty { props: unknown; }
66 | interface ElementChildrenAttribute { children: unknown; }
67 |
68 | // Prevent children on components that don't declare them
69 | interface IntrinsicAttributes { children?: never; }
70 |
71 | // Allow children on all DOM elements (not components, see above)
72 | // ESLint will error for children on void elements like
73 | type DOMAttributes
74 | = GenericEventAttrs & { children?: unknown };
75 |
76 | type HTMLAttributes
77 | = HTMLAttrs & DOMAttributes;
78 |
79 | type SVGAttributes
80 | = SVGAttrs & HTMLAttributes;
81 |
82 | type IntrinsicElements =
83 | & { [El in keyof HTMLElements]: HTMLAttributes; }
84 | & { [El in keyof SVGElements]: SVGAttributes; };
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/dom/index.ts:
--------------------------------------------------------------------------------
1 | import { h as _h } from './h.js';
2 |
3 | import { add } from './nodeAdd.js';
4 | import { insert } from './nodeInsert.js';
5 | import { property } from './nodeProperty.js';
6 | import { remove } from './nodeRemove.js';
7 |
8 | import { svg } from './svg.js';
9 |
10 | import type { Component, El, Tag } from './h.js';
11 |
12 | // This API should be compatible with community libraries that extend Sinuous
13 | const api: {
14 | /** Hyperscript reviver */
15 | h: typeof _h;
16 | // Customizable internal methods for h()
17 | add: typeof add;
18 | insert: typeof insert;
19 | property: typeof property;
20 | // Renamed for compatibility with Sinuous' community libraries
21 | rm: typeof remove;
22 | /** DOM patcher. Receives unknown JSX elements and attributes. To mark the DOM
23 | location as reactive, return true. Call patchDOM() anytime to update. */
24 | patch: (
25 | value: unknown,
26 | // Reactivity could be from Haptic, Sinuous, MobX, Hyperactiv, etc
27 | patchDOM?: (value: unknown) => void,
28 | // Element being patched
29 | el?: Node,
30 | // If this is patching an element property, this is the attribute
31 | attribute?: string
32 | ) => boolean,
33 | /** Element namespace URL such as SVG or MathML */
34 | ns?: string;
35 | } = {
36 | h: _h,
37 | add,
38 | insert,
39 | property,
40 | rm: remove,
41 | patch: () => false,
42 | };
43 |
44 | // Reference the latest internal h() allowing others to customize the call
45 | const h: typeof _h = (...args) => api.h(...args);
46 |
47 | export { api, h, svg };
48 | export type { Component, El, Tag };
49 |
--------------------------------------------------------------------------------
/src/dom/nodeAdd.ts:
--------------------------------------------------------------------------------
1 | import { api } from './index.js';
2 |
3 | type Value = Node | string | number;
4 | type Frag = { _startMark: Text };
5 | type FragReturn = Frag | Node | undefined;
6 |
7 | const asNode = (value: unknown): Text | Node | DocumentFragment => {
8 | if (typeof value === 'string') {
9 | return document.createTextNode(value);
10 | }
11 | // Note that a DocumentFragment is an instance of Node
12 | if (!(value instanceof Node)) {
13 | // Passing an empty array creates a DocumentFragment
14 | // Note this means api.add is not purely a subcall of api.h; it can nest
15 | return api.h([], value) as DocumentFragment;
16 | }
17 | return value;
18 | };
19 |
20 | const maybeFragOrNode = (value: Text | Node | DocumentFragment): FragReturn => {
21 | const { childNodes } = value;
22 | // eslint-disable-next-line eqeqeq
23 | if (value.nodeType != 11 /* DOCUMENT_FRAGMENT_NODE */) return;
24 | if (childNodes.length < 2) return childNodes[0];
25 | // For a fragment of 2 elements or more add a startMark. This is required for
26 | // multiple nested conditional computeds that return fragments.
27 |
28 | // It looks recursive here but the next call's fragOrNode is only Text('')
29 | return { _startMark: api.add(value, '', childNodes[0]) as Text };
30 | };
31 |
32 | /** Add a node before a reference node or at the end. */
33 | const add = (parent: Node, value: Value | Value[], endMark?: Node) => {
34 | value = asNode(value);
35 | const fragOrNode = maybeFragOrNode(value) || value;
36 |
37 | // If endMark is `null`, value will be added to the end of the list.
38 | parent.insertBefore(value, (endMark && endMark.parentNode && endMark) as Node | null);
39 | return fragOrNode;
40 | };
41 |
42 | export { add };
43 |
--------------------------------------------------------------------------------
/src/dom/nodeInsert.ts:
--------------------------------------------------------------------------------
1 | import { api } from './index.js';
2 |
3 | type Frag = { _startMark: Text };
4 |
5 | /** Insert a node into an existing node. */
6 | const insert = (el: Node, value: unknown, endMark?: Node, current?: Node | Frag, startNode?: ChildNode | null) => {
7 | // This is needed if the el is a DocumentFragment initially.
8 | el = (endMark && endMark.parentNode) || el;
9 |
10 | // Save startNode of current. In clear() endMark.previousSibling is not always
11 | // accurate if content gets pulled before clearing.
12 | startNode = (startNode || current instanceof Node && current) as ChildNode | null;
13 |
14 | if (value === current) {}
15 | else if (
16 | (!current || typeof current === 'string')
17 | // @ts-ignore Doesn't like `value += ''`
18 | // eslint-disable-next-line no-implicit-coercion
19 | && (typeof value === 'string' || (typeof value === 'number' && (value += '')))
20 | ) {
21 | // Block optimized for string insertion
22 | // eslint-disable-next-line eqeqeq
23 | if ((current as unknown) == null || !el.firstChild) {
24 | if (endMark) {
25 | api.add(el, value, endMark);
26 | } else {
27 | // Using textContent is a lot faster than append -> createTextNode
28 | el.textContent = value as string; // Because value += ''
29 | }
30 | } else {
31 | if (endMark) {
32 | // @ts-expect-error Illegal `data` property
33 | (endMark.previousSibling || el.lastChild).data = value;
34 | } else {
35 | // @ts-expect-error Illegal `data` property
36 | el.firstChild.data = value;
37 | }
38 | }
39 | // @ts-expect-error Reusing the variable but doesn't match the signature
40 | current = value;
41 | }
42 | else if (
43 | api.patch(value, (v) =>
44 | current = api.insert(el, v, endMark, current, startNode), el)
45 | ) {}
46 | else {
47 | // Block for Node, Fragment, Array, Functions, etc. This stringifies via h()
48 | if (endMark) {
49 | // `current` can't be `0`, it's coerced to a string in insert.
50 | if (current) {
51 | if (!startNode) {
52 | // Support fragments
53 | startNode = (
54 | (current as { _startMark?: Text })._startMark
55 | && (current as Frag)._startMark.nextSibling
56 | ) || endMark.previousSibling;
57 | }
58 | api.rm(el, startNode, endMark);
59 | }
60 | } else {
61 | el.textContent = '';
62 | }
63 | current = value && value !== true
64 | ? api.add(el, value as string | number, endMark)
65 | : undefined;
66 | }
67 | return current;
68 | };
69 |
70 | export { insert };
71 |
--------------------------------------------------------------------------------
/src/dom/nodeProperty.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable eqeqeq */
2 | import { api } from './index.js';
3 |
4 | type EventHandler = (ev: Event) => unknown;
5 | type NodeEvented = Node & { $l?: { [name: string]: EventHandler } };
6 |
7 | // Note that isAttr is never set. It exists mostly to maintain API compatibility
8 | // with Sinuous and its community packages. However, it's also possible to wrap
9 | // the api.property function and set isAttr from there if needed
10 |
11 | /** Set attributes and propeties on a node. */
12 | export const property = (el: Node, value: unknown, name: string | null, isAttr?: boolean, isCss?: boolean) => {
13 | if (value == null) {}
14 | else if (!name) {
15 | for (name in value as { [k: string]: unknown }) {
16 | api.property(el, (value as { [k: string]: unknown })[name], name, isAttr, isCss);
17 | }
18 | }
19 | // Functions added as event handlers are not executed on render
20 | // There's only one event listener per type
21 | else if (name[0] == 'o' && name[1] == 'n') {
22 | const listeners = (el as NodeEvented).$l || ((el as NodeEvented).$l = {});
23 | name = name.slice(2).toLowerCase();
24 | // Remove the previous function
25 | if (listeners[name]) {
26 | el.removeEventListener(name, listeners[name] as EventHandler); // TS bug
27 | delete listeners[name];
28 | }
29 | el.addEventListener(name, value as EventHandler);
30 | listeners[name] = value as EventHandler;
31 | }
32 | else if (
33 | api.patch(value, (v) => api.property(el, v, name, isAttr, isCss), el, name)
34 | ) {}
35 | else if (isCss) {
36 | (el as HTMLElement | SVGElement).style.setProperty(name, value as string);
37 | }
38 | else if (
39 | isAttr
40 | || name.slice(0, 5) == 'data-'
41 | || name.slice(0, 5) == 'aria-'
42 | ) {
43 | (el as HTMLElement | SVGElement).setAttribute(name, value as string);
44 | }
45 | else if (name == 'style') {
46 | if (typeof value === 'string') {
47 | (el as HTMLElement | SVGElement).style.cssText = value;
48 | } else {
49 | api.property(el, value, null, isAttr, true);
50 | }
51 | }
52 | else {
53 | // Default case; add as a property
54 | // @ts-expect-error
55 | el[name == 'class' ? name + 'Name' : name] = value;
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/src/dom/nodeRemove.ts:
--------------------------------------------------------------------------------
1 | /** Removes nodes from `startNode` (inclusive) to `endMark` (exclusive). */
2 | const remove = (parent: Node, startNode: ChildNode | null, endMark: Node) => {
3 | while (startNode && startNode !== endMark) {
4 | const n = startNode.nextSibling;
5 | // Is needed in case the child was pulled out the parent before clearing.
6 | if (parent === startNode.parentNode) {
7 | parent.removeChild(startNode);
8 | }
9 | startNode = n;
10 | }
11 | };
12 |
13 | export { remove };
14 |
--------------------------------------------------------------------------------
/src/dom/readme.md:
--------------------------------------------------------------------------------
1 | # Hyperscript/TSX reviver
2 |
3 | This is a fork of the `sinuous/h` package from Sinuous. It was ported to
4 | TypeScript, simplified in a few places, and now uses a general `api.patch()`
5 | method to support DOM updates with any reactive library. There's no reactivity
6 | baked into `haptic/dom`. Haptic configures reactivity in the primary `haptic`
7 | package where it pairs this reviver with `haptic/state`.
8 |
9 | It's 964 bytes min+gzip on its own.
10 |
11 | Designed to be explicit, type safe, and interoperable with the Sinuous
12 | ecosystem. The Sinuous repository lists some [community packages][1].
13 |
14 | This `haptic/dom` package exports a vanilla JSX namespace that doesn't expect or
15 | support any reactive functions as elements or attributes. To use a reactive
16 | library of your choice, repeat how the `haptic` package configures `api.patch`
17 | and the JSX namespace for `haptic/state`.
18 |
19 | ## `h(tag: Tag, props?: unknown, ...children: unknown[]): El | undefined`
20 |
21 | ```ts
22 | type El = Element | Node | DocumentFragment;
23 | type Tag = El | Component | [] | string;
24 | type Component = (...args: unknown[]) => El | undefined;
25 | ```
26 |
27 | The hyperscript reviver. This is really standard. It's how trees of elements are
28 | defined in most frameworks; both JSX and traditional/transpiled `h()`-based
29 | system alike.
30 |
31 | The only notable difference from other frameworks is that functions inlined in
32 | JSX will be serialized to strings. This is a deviation from Sinuous' reviver
33 | which automatically converts any function to a `computed()` (their version of a
34 | computed-signal) in order to support DOM updates. Haptic's reviver explicitly
35 | only targets wires inlined in JSX - any non-wire function is skipped.
36 |
37 | The following two uses of `h()` are equivalent:
38 |
39 | ```tsx
40 | import { h } from 'haptic/dom';
41 |
42 | document.body.appendChild(
43 | h('div', { style: 'margin-top: 10px;' },
44 | h('p', 'This is content.')
45 | )
46 | );
47 |
48 | document.body.appendChild(
49 |
50 |
This is content
51 |
52 | );
53 | ```
54 |
55 | ## `api: { ... }`
56 |
57 | The internal API that connects the functions of the reviver allowing you to
58 | replace or configure them. It was ported from Sinuous, and maintains most of
59 | their API-compatibility, meaning Sinuous community plugins should work.
60 |
61 | Read `./src/dom/index.ts` for the available methods/configurations.
62 |
63 | Typically the usecase is to override methods with wrappers so work can be done
64 | during the render. For example, I wrote a package called _sinuous-lifecycle_
65 | that provides `onAttach` and `onDetach` lifecycle hooks and works by listening
66 | to API calls to check for components being added or removed.
67 |
68 | Here's a simple example:
69 |
70 | ```tsx
71 | import { api } from 'haptic';
72 |
73 | const hPrev = api.h;
74 | api.h = (...rest) => {
75 | console.log('api.h:\t', rest);
76 | return hPrev(...rest);
77 | };
78 |
79 | const addPrev = api.add;
80 | api.add = (...rest) => {
81 | console.log('api.add:\t', rest);
82 | return addPrev(...rest);
83 | };
84 |
85 |