/*! MIT License Copyright (c) 2024 Max J. Polster Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * Common interface for components. */ interface Component { (props: Props): Content; } /** * Utility for defining phantom typed context key-value pairs. * * @example * ```ts * import { ContextKey } from "rvx"; * * const key = Symbol("example") as SymbolFor<"exampleValue">; * * inject(key, "exampleValue", () => { * const value = extract(key); // type = "exampleValue" * }); * ``` */ type ContextKey = symbol & { PHANTOM_CONTEXT_KEY_FOR: V & never; }; /** * The value type for a specific type of key. */ type ContextValue = K extends ContextKey ? V : unknown; /** * Interface for a context that should not be modified. * * Note that this is always a {@link Map} instance. */ interface ReadonlyContext { get(key: K): ContextValue | undefined; has(key: unknown): boolean; readonly size: number; } /** * Interface for a context that may be modified. * * Note that this is always a {@link Map} instance. */ interface Context extends ReadonlyContext { clear(): void; delete(key: unknown): boolean; set(key: K, value: ContextValue | undefined): void; } /** * Create a new empty context. */ declare function createContext(): Context; /** * Get the current context. * * @returns The current context or undefined if there is no context. */ declare function getContext(): ReadonlyContext | undefined; /** * Get a value from the current context. * * @param key The key to find. * @returns The value or undefined if not found. */ declare function extract(key: K): ContextValue | undefined; /** * Run a function within a copy of the current context that also contains an additional entry. * * + For injecting multiple entries prefer using {@link deriveContext}. * * @param value The key value pair or instance to inject. * @param fn The function to run. * @returns The function's return value. */ declare function inject(key: K, value: ContextValue | undefined, fn: () => R): R; /** * Inject an entry. * * @example * ```tsx * import { Inject, extract } from "rvx"; * * * {() =>

{extract("message")}

} *
* ``` */ declare function Inject(props: { key: K; value: ContextValue | undefined; children: Component; }): unknown; /** * Run a function within a copy of the current context. * * @param fn The function to run. * @returns The function's return value. */ declare function deriveContext(fn: (context: Context, parent?: ReadonlyContext) => R): R; /** * Render content with a copy of the current context. * * @example * ```tsx * import { DeriveContext, extract } from "rvx"; * * * {context => { * context.set("message", "Hello World!"); * return

{extract("message")}

; * }} *
* ``` */ declare function DeriveContext(props: { /** * A function to render content. */ children: (context: Context, parent?: ReadonlyContext) => unknown; }): unknown; /** * Run a function within the specified or without a context. * * @param context The context or undefined to use no context. * @param fn The function to run. * @returns The function's return value. */ declare function runInContext(context: ReadonlyContext | undefined, fn: () => R): R; /** * Wrap a function to be run with the current context. * * @param fn The function to wrap. * @returns The wrapper. */ declare function wrapContext any>(fn: T): T; /** * A function used in signals to determine if the signal should update during a value assignment. */ interface SignalEqualsFn { /** * @param previous The previous value. * @param current The current value. * @returns False to update. */ (previous: T, current: T): boolean; } /** * Represents a value that changes over time. */ declare class Signal { #private; /** * Create a new signal. * * @param value The initial value. * @param equals True to skip updates when an assigned value is strictly equal to the previous one or a function to determine of the values are equal. Default is true. */ constructor(value: T, equals?: SignalEqualsFn | boolean); /** * Access the current value. */ get value(): T; /** * Set the current value. * * @example * ```tsx * import { sig, watch } from "rvx"; * * const count = sig(0); * * watch(count, count => { * console.log("Count:", count); * }); * * count.value++; * ``` */ set value(value: T); /** * Update the current value in place. * * @param fn A function to update the value. If false is returned, dependants are not notified. * * @example * ```tsx * import { sig, watch } from "rvx"; * * const items = sig([]); * * watch(items, items => { * console.log("Items:", items); * }); * * items.update(items => { * items.push("foo"); * items.push("bar"); * }); * ``` */ update(fn: (value: T) => void | boolean): void; /** * Check if this signal has any active dependants. */ get active(): boolean; /** * Manually access this signal. */ access(): void; /** * Manually notify dependants. */ notify(): void; /** * Pass this signal to a function and get it's result. * * @example * ```tsx * const value = sig(42); * * * ``` */ pipe(fn: (self: this, ...args: A) => R, ...args: A): R; } /** * Create a new signal. * * @param value The initial value. * @param equals True to skip updates when an assigned value is strictly equal to the previous one or a function to determine if the values are equal. Default is true. * @returns The signal. */ declare function sig(): Signal; declare function sig(value: T, equals?: SignalEqualsFn | boolean): Signal; /** * A value, signal or function to get a value. * * @example * ```tsx * import { sig, watch } from "rvx"; * * const message = sig("Example"); * * // Not reactive: * watch(message.value, message => { * console.log("A:", message); * }); * * // Reactive: * watch(message, message => { * console.log("B:", message); * }); * * // Reactive: * watch(() => message.value, message => { * console.log("C:", message); * }); * * message.value = "Hello World!"; * ``` */ type Expression = T | Signal | (() => T); /** * Type for the result of an expression. */ type ExpressionResult = T extends Expression ? R : never; /** * Watch an expression until the current lifecycle is disposed. * * @param expr The expression to watch. * @param fn The function to call with the expression result. This is guaranteed to be called at least once immediately. Lifecycle hooks are called before the next function call or when the current lifecycle is disposed. * * @example * ```tsx * import { sig, watch } from "rvx"; * * const count = sig(0); * * // Capture teardown hooks registered by "watch": * const dispose = capture(() => { * // Start watching: * watch(count, count => { * console.log("Count:", count); * }); * }); * * count.value = 1; * * // Stop watching: * dispose(); * * count.value = 2; * ``` */ declare function watch(expr: Expression, fn: (value: T) => void): void; /** * Watch an expression until the current lifecycle is disposed. * * @param expr The expression to watch. * @param fn The function to call with the expression result when any updates occur. * @returns The first expression result. */ declare function watchUpdates(expr: Expression, fn: (value: T) => void): T; /** * Run and watch a function until the current lifecycle is disposed. * * Note, that this doesn't separate signal accesses from side effects which makes it easier to accidentally cause infinite loops. If possible, use {@link watch} or {@link watchUpdates} instead. * * @param fn The function to run. Lifecycle hooks are called before the next function call or when the current lifecycle is disposed. */ declare function effect(fn: () => void): void; /** * Defer signal updates until a function finishes. * * + When nesting batches, updates are processed after the most outer batch has completed. * + When updates cause immediate side effects, these side effects will run as part of the batch. * * @param fn The function to run. * @returns The function's return value. * * @example * The example below outputs `5` and `9` once. Without batching the output would be `5, 7, 9`. * ```tsx * import { batch, sig, watch } from "rvx"; * * const a = sig(2); * const b = sig(3); * * watch(() => a.value + b.value, value => { * console.log("Sum:", value); * }); * * batch(() => { * a.value = 4; * b.value = 5; * }); * ``` */ declare function batch(fn: () => T): T; /** * Watch an expression and create a function to reactively access it's latest result. * * This is similar to {@link lazy}, but the expression is always evaluated and then updates it's dependants. * * @param expr The expression to watch. * @param equals True to skip updates when a result is strictly equal to the previous one or a function to determine if the results are equal. Default is true. * @returns A function to access the latest result. * * @example * ```tsx * import { sig, memo, watch } from "rvx"; * * const count = sig(42); * * const computed = memo(() => someExpensiveComputation(count.value)); * * watch(computed, count => { * console.log("Count:", count); * }); * ``` */ declare function memo(expr: Expression, equals?: SignalEqualsFn | boolean): () => T; /** * Run a function while not tracking signal accesses. * * This is the opposite of {@link track}. * * @param fn The function to run. * @returns The function's return value. * * @example * ```tsx * import { sig, untrack, watch } from "rvx"; * * const a = sig(2); * const b = sig(3); * * watch(() => a.value + untrack(() => b.value), sum => { * console.log("Sum:", sum); * }); * * a.value = 4; * b.value = 5; * ``` */ declare function untrack(fn: () => T): T; /** * Run a function while tracking signal accesses. This is the default behavior. * * This is the opposite of {@link untrack}. * * @param fn The function to run. * @returns The function's return value. */ declare function track(fn: () => T): T; /** * Check if a currently evaluating expression is tracking signal accesses. */ declare function isTracking(): boolean; interface TriggerPipe { (expr: Expression): T; } /** * Create an expression evaluator pipe that calls a function once when any accessed signals from the latest evaluated expression are updated. * * + When the lifecycle at which the pipe was created is disposed, the callback function will not be called anymore. * + It is guaranteed that the function is called before any other observers like {@link watch} or {@link effect} are notified. * + If pipes are nested, the callback for the most inner one is called first. * * @param fn The callback to invoke when a signal is updated. * @returns The pipe to evaluate expressions. */ declare function trigger(fn: () => void): TriggerPipe; /** * Evaulate an expression. * * This can be used to access reactive and non reactive inputs. * * @param expr The expression to evaluate. * @returns The expression result. * * @example * ```tsx * import { sig, get } from "rvx"; * * const count = sig(42); * * get(42) // 42 * get(count) // 42 * get(() => 42) // 42 * ``` */ declare function get(expr: Expression): T; type MapFn = (input: I) => O; /** * Map an expression value while preserving if the expression is static or not. * * @example * ```tsx * import { sig, map, get } from "rvx"; * * const count = sig(42); * const doubleCount = map(count, value => value * 2); * * get(doubleCount) // 84 * ``` */ declare function map(input: Expression, mapFn: MapFn): Expression; /** * Map an expression value to strings. * * See {@link map}. * * @example * ```tsx * import { string } from "rvx"; * *
; //
*
; //
*
; //
* ``` */ declare function string(input: Expression): Expression; /** * Map an expression value to strings unless it's null or undefined. * * See {@link map}. * * @example * ```tsx * import { optionalString } from "rvx"; * *
; //
*
; //
* ``` */ declare function optionalString(input: Expression): Expression>>; /** * Namespace URI for HTML elements. */ declare const HTML = "http://www.w3.org/1999/xhtml"; /** * Namespace URI for SVG elements. */ declare const SVG = "http://www.w3.org/2000/svg"; /** * Namespace URI for MathML elements. */ declare const MATHML = "http://www.w3.org/1998/Math/MathML"; type XMLNS = typeof HTML | typeof SVG | typeof MATHML; /** * Key for setting the namespace URI for newly created elements. * * @example * ```tsx * import { XMLNS, SVG, Inject } from "rvx"; * * * {() => ...} * * ``` */ declare const XMLNS: ContextKey; type ClassValue = Expression> | ClassValue[]>; type HyphenCase = T extends `${infer A}${infer B}` ? `${A extends Capitalize ? "-" : ""}${Lowercase}${HyphenCase}` : T; type StyleMap = { [K in keyof CSSStyleDeclaration as HyphenCase]?: Expression; } & { [K in string]?: Expression; }; type StyleValue = Expression; type EventListener = (event: E) => void; /** * **This is experimental API.** */ declare const NODE: unique symbol; /** * **This is experimental API.** */ interface NodeTarget { [NODE]: Node; } /** * A function that can be called to dispose something. */ type TeardownHook = () => void; /** * Run a function while capturing teardown hooks. * * + If an error is thrown by the specified function, teardown hooks are called in reverse registration order and the error is re-thrown. * + If an error is thrown by a teardown hook, remaining ones are not called and the error is re-thrown. * * @param fn The function to run. * @returns A function to run all captured teardown hooks in reverse registration order. */ declare function capture(fn: () => void): TeardownHook; /** * Run a function while capturing teardown hooks that may dispose itself. * * + If an error is thrown by the specified function, teardown hooks are called in reverse registration order and the error is re-thrown. * + If an error is thrown by a teardown hook, remaining ones are not called and the error is re-thrown. * * @param fn The function to run. * @returns The function's return value. */ declare function captureSelf(fn: (dispose: TeardownHook) => T): T; /** * Run a function without capturing any teardown hooks. * * This is the opposite of {@link capture}. * * @param fn The function to run. * @returns The function's return value. */ declare function uncapture(fn: () => T): T; /** * Run a function and explicitly un-support teardown hooks. * * Teardown hooks are still supported when using {@link capture}, {@link captureSelf} or {@link uncapture} inside of the function. * * This should be used in places where lifecycle side are never expected. * * @param fn The function to run. * @returns The function's return value. */ declare function nocapture(fn: () => T): T; /** * Run a function within an error isolation boundary. * * + If an error is thrown, teardown hooks are immediately called in reverse registration order and the error is re-thrown. * + If no error is thrown, teardown hooks are registered in the outer context. * * @param fn The function to run. * @returns The function's return value. */ declare function isolate(fn: () => T): T; /** * Register a teardown hook. * * This has no effect if teardown hooks are not captured in the current context. * * @param hook The hook to register. This may be called multiple times. * @throws An error if teardown hooks are {@link nocapture explicitly un-supported}. */ declare function teardown(hook: TeardownHook): void; type Falsy = null | undefined | false | 0 | 0n | ""; type TagNameMap = HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap; declare class ElementBuilder implements NodeTarget { elem: E; get [NODE](): Node; constructor(elem: E); on(name: K, listener: EventListener, options?: AddEventListenerOptions): this; on(name: string, listener: EventListener, options?: AddEventListenerOptions): this; style(value: StyleValue): Omit; class(value: ClassValue): Omit; set(name: string, value: Expression): this; prop(name: K, value: Expression): this; append(...content: unknown[]): this; } /** * Create a new element builder. * * @param tagName The tag name. * @returns The builder. */ declare function e(tagName: K): ElementBuilder; declare function e(tagName: string): ElementBuilder; interface EventFn { (...args: T): void; } interface Event$1 { /** * Subscribe to this event until the current lifecycle is disposed. */ (listener: EventFn): void; } /** * An emitter for a single event type. * * @example * ```tsx * import { Emitter } from "rvx"; * * const emitter = new Emitter<[address: string, port: number]>(); * * emitter.event((address, port) => { * console.log("Connected:", address, port); * }); * * emitter.emit("127.0.0.1", 12345); * ``` */ declare class Emitter { #private; /** * Subscribe to this event until the current lifecycle is disposed. */ event: Event$1; /** * Emit this event. */ emit(...args: T): void; } /** * Allocate an ID that is unique in the current thread. * * @returns The unique id in the form `rvx_123`. */ declare function uniqueId(): string; /** * A component that provides a unique id in the form `rvx_123` to it's children. * * @example * ```tsx * import { UseUniqueId } from "rvx"; * * * {id => <> * * * } * * ``` */ declare function UseUniqueId(props: { children: Component; }): unknown; /** * A function that is called when the view boundary may have been changed. */ interface ViewBoundaryOwner { /** * @param first The current first node. * @param last The current last node. */ (first: Node, last: Node): void; } /** * A function that must be called after the view boundary has been changed. */ interface ViewSetBoundaryFn { /** * @param first The first node if changed. * @param last The last node if changed. */ (first: Node | undefined, last: Node | undefined): void; } /** * A function that is called once to initialize a view instance. * * View creation will fail if no first or last node has been set during initialization. */ interface ViewInitFn { /** * @param setBoundary A function that must be called after the view boundary has been changed. * @param self The current view itself. This can be used to keep track of the current boundary and parent nodes. */ (setBoundary: ViewSetBoundaryFn, self: View): void; } /** * Represents a sequence of at least one DOM node. * * Consumers of the view API need to guarantee that: * + The sequence of nodes is not modified from the outside. * + If there are multiple nodes, all nodes must have a common parent node at all time. */ declare class View { #private; /** * Create a new view. * * View implementations need to guarantee that: * + The view doesn't break when the parent node is replaced or when a view consisting of only a single node is detached from it's parent. * + The boundary is updated immediately after the first or last node has been updated. * + If there are multiple nodes, all nodes remain in the current parent. * + If there are multiple nodes, the initial nodes must have a common parent. */ constructor(init: ViewInitFn); /** * The current first node of this view. * * Note, that this property is not reactive. */ get first(): Node; /** * The current last node of this view. * * Note, that this property is not reactive. */ get last(): Node; /** * The current parent node or undefined if there is none. * * Note, that this property is not reactive. */ get parent(): Node | undefined; /** * Set the boundary owner for this view until the current lifecycle is disposed. * * @throws An error if there currently is a boundary owner. */ setBoundaryOwner(owner: ViewBoundaryOwner): void; /** * Get all nodes of this view as a single node for moving them into a new place. * * If there are multiple nodes, a document fragment containing all nodes of this view is returned. */ take(): Node | DocumentFragment; /** * Detach all nodes of this view from the current parent if there is one. * * If there are multiple nodes, they are moved into a new document fragment to allow the view implementation to stay alive. */ detach(): void; } /** * Get an iterator over all current top level nodes of a view. * * @param view The view. * @returns The iterator. * * @example * ```tsx * import { render, viewNodes } from "rvx"; * * const view = render(<> *

Hello World!

* ); * * for (const node of viewNodes(view)) { * console.log(node); * } * ``` */ declare function viewNodes(view: View): IterableIterator; /** * A component that renders content depending on an expression. * * + If an error is thrown by the expression or component during initialization, the error is re-thrown. * + If an error is thrown by the expression or component during a signal update, the previously rendered content is kept and the error is re-thrown. * * @example * ```tsx * import { Nest, sig } from "rvx"; * * const count = sig(0); * * * {() => { * const value = count.value; * return () => <>{value}; * }} * * ``` */ declare function Nest(props: { /** * An expression that returns a function to create content or null or undefined to render nothing. */ children: Expression; }): View; /** * A component that renders conditional content. * * + Content is only re-rendered if the expression result is not strictly equal to the previous one. If this behavior is undesired, use {@link Nest} instead. * + If an error is thrown by the expression or component during initialization, the error is re-thrown. * + If an error is thrown by the expression or component during a signal update, the previously rendered content is kept and the error is re-thrown. * * @example * ```tsx * import { sig, Show } from "rvx"; * * const message = sig("Hello World!"); * * <>No message...}> * {value =>

{value}

} *
* ``` */ declare function Show(props: { /** * The expression to evaluate. */ when: Expression; /** * A function to create content if the value is truthy. */ children: Component; /** * An optional function to create content if the value is falsy. */ else?: Component; }): View; /** * A function to create content for a specific value. */ interface ForContentFn { /** * @param value The value. * @param index An expression to get the current index. * @returns The content. */ (value: T, index: () => number): unknown; } /** * A component that renders content for each unique value in an iterable. * * If an error is thrown by iterating or by rendering an item, the update is stopped as if the previous item was the last one and the error is re-thrown. * * @example * ```tsx * import { ForUnique, sig } from "rvx"; * * const items = sig([1, 2, 3]); * * * {value =>
  • {value}
  • } *
    * ``` */ declare function For(props: { /** * The expression. */ each: Expression>; /** * A function to create content for a specific value. */ children: ForContentFn; }): View; /** * A function to create content for a specific index and value. */ interface IndexForContentFn { /** * @param value The value. * @param index The index. * @returns The content. */ (value: T, index: number): unknown; } /** * A component that renders content for each value in an iterable, keyed by index and value. * * If an error is thrown by iterating or by rendering an item, the update is stopped as if the previous item was the last one and the error is re-thrown. * * @example * ```tsx * import { IndexFor, sig } from "rvx"; * * const items = sig([1, 2, 3]); * * * {value =>
  • {value}
  • } *
    * ``` */ declare function IndexFor(props: { /** * The expression. */ each: Expression>; /** * A function to create content for a specific index and value. */ children: IndexForContentFn; }): View; /** * A wrapper that can be used for moving and reusing views. */ declare class MovableView { #private; constructor(view: View); /** * Create a new view that contains the wrapped view until it is moved again or detached. */ move(): View; /** * Detach the wrapped view if attached. */ detach(): void; } /** * Render and wrap arbitrary content so that it can be moved and reused. */ declare function movable(content: unknown): MovableView; /** * A component that attaches or detaches content depending on an expression. * * Content is kept alive when detached. * * @example * ```tsx * import { sig, Attach } from "rvx"; * * const showMessage = sig(true); * * *

    Hello World!

    *
    * ``` */ declare function Attach(props: { /** * The expression to evaluate. */ when: Expression; /** * The content to attach when the expression is truthy. */ children?: unknown; }): View; /** * Render arbitrary content. * * Supported content types are: * + Null and undefined (not displayed). * + Arbitrarily nested arrays/fragments of content. * + DOM nodes. Children will be removed from document fragments. * + {@link View Views}. * + Anything created with rvx's jsx runtime. * + Anything else is displayed as text. * * @param content The content to render. * @returns A view instance or the content itself if it's already a view. * * @example * ```tsx * import { render, sig } from "rvx"; * * // Not displayed: * render(null); * render(undefined); * * // Arbitrarily nested arrays/fragments of content: * render([["Hello"], " World!"]); * render(<>{<>"Hello"}{" World!"}); * * // DOM nodes: * render(

    Hello World!

    ); * render(document.createElement("input")); * render(document.createTextNode("Hello World!")); * render(someTemplate.content.cloneNode(true)); * * // Views: * render(render("Hello World!")); * render(when(true, () => "Hello World!")); * render({() => "Hello World!"}); * * // Text: * render("Hello World!"); * render(() => "Hello World!"); * render(42); * render(sig(42)); * ``` */ declare function render(content: unknown): View; /** * Render arbitrary content and append it to the specified parent until the current lifecycle is disposed. * * @param parent The parent node. * @param content The content to render. See {@link render} for supported types. * @returns The view instance. * * @example * ```tsx * import { mount } from "rvx"; * * mount( * document.body, *

    Hello World!

    * ); * ``` * * Since the content is removed when the current lifecycle is disposed, this can also be used to temporarily append * content to different elements while some component is rendered: * ```tsx * import { mount } from "rvx"; * * function Popover(props: { text: unknown, children: unknown }) { * const visible = sig(false); * * mount( * document.body, * * {props.children} * * ); * * return ; * } * * mount( * document.body, * * Hello World! * * ); * ``` */ declare function mount(parent: Node, content: unknown): View; export { Attach, type ClassValue, type Component, type Context, type ContextKey, type ContextValue, DeriveContext, ElementBuilder, Emitter, type Event$1 as Event, type EventFn, type EventListener, type Expression, type ExpressionResult, For, type ForContentFn, HTML, IndexFor, type IndexForContentFn, Inject, MATHML, type MapFn, MovableView, NODE, Nest, type NodeTarget, type ReadonlyContext, SVG, Show, Signal, type SignalEqualsFn, type StyleMap, type StyleValue, type TeardownHook, type TriggerPipe, UseUniqueId, View, type ViewBoundaryOwner, type ViewInitFn, type ViewSetBoundaryFn, XMLNS, batch, capture, captureSelf, createContext, deriveContext, e, effect, extract, get, getContext, inject, isTracking, isolate, map, memo, mount, movable, nocapture, optionalString, render, runInContext, sig, string, teardown, track, trigger, uncapture, uniqueId, untrack, viewNodes, watch, watchUpdates, wrapContext };