UNPKG

rvx

Version:

A signal based rendering library

539 lines (538 loc) 16.3 kB
import { Expression, ExpressionResult } from "./signals.js"; import type { Component, Content, Falsy } from "./types.js"; /** * 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 } from "rvx"; * * // Not displayed: * render(null); * render(undefined); * * // Arbitrarily nested arrays/fragments of content: * render([["Hello"], " World!"]); * render(<>{<>"Hello"</>}{" World!"}</>); * * // DOM nodes: * render(<h1>Hello World!</h1>); * 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(<When value={true}>{() => "Hello World!"}</When>); * * // Text: * render("Hello World!"); * render(() => "Hello World!"); * render(42); * render($(42)); * ``` */ export declare function render(content: Content): 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, * <h1>Hello World!</h1> * ); * ``` * * 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, Content } from "rvx"; * * function Popover(props: { text: Content, children: Content }) { * const visible = $(false); * * mount( * document.body, * <Show when={visible}> * {props.children} * </Show> * ); * * return <button on:click={() => { visible.value = !visible.value; }}> * {props.text} * </button>; * } * * mount( * document.body, * <Popover text="Click me!"> * Hello World! * </Popover> * ); * ``` */ export declare function mount(parent: Node, content: Content): View; /** * A function that is called when the view boundary may have been changed. */ export 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. */ export interface ViewSetBoundaryFn { /** * @param first The first node if changed. * @param last The last node if changed. */ (first: Node | undefined, last: Node | undefined): void; } interface UninitViewProps { /** * The current first node of this view. * * + This may be undefined while the view is not fully initialized. * + This property is not reactive. */ get first(): Node | undefined; /** * The current last node of this view. * * + This may be undefined while the view is not fully initialized. * + This property is not reactive. */ get last(): Node | undefined; } export type UninitView = UninitViewProps & Omit<View, keyof UninitViewProps>; /** * 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. */ export 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: UninitView): 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. */ export 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; /** * Append all nodes of this view to the specified parent. * * @param parent The parent to append to. */ appendTo(parent: Node): void; /** * Insert all nodes of this view before a reference child of the specified parent. * * @param parent The parent to insert into. * @param ref The reference child to insert before. If this is null, the nodes are appended to the parent. */ insertBefore(parent: Node, ref: Node | null): void; /** * 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. * * @returns The single removed node or the document fragment they have been moved into. */ detach(): Node | DocumentFragment; } /** * 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(<> * <h1>Hello World!</h1> * </>); * * for (const node of viewNodes(view)) { * console.log(node); * } * ``` */ export declare function viewNodes(view: View): IterableIterator<Node>; /** * Watch an expression and render content from it's result. * * + If an error is thrown during initialization, the error is re-thrown. * + If an error is thrown during a signal update, the previously rendered content is kept in place and the error is re-thrown. * + Content returned from the component can be directly reused within the same `nest` instance. * * See {@link Nest `<Nest>`} when using JSX. * * @param expr The expression to watch. * @param component The component to render with the expression result. If the expression returns a component, null or undefined, this can be omitted. * * @example * ```tsx * import { $, nest, e } from "rvx"; * * const count = $(0); * * nest(count, count => { * switch (count) { * case 0: return e("h1").append("Hello World!"); * case 1: return "Something else..."; * } * }) * ``` */ export declare function nest(expr: Expression<Component | null | undefined>): View; export declare function nest<T>(expr: Expression<T>, component: Component<T>): View; /** * Watch an expression and render content from it's result. * * + If an error is thrown during initialization, the error is re-thrown. * + If an error is thrown during a signal update, the previously rendered content is kept in place and the error is re-thrown. * + Content returned from the component can be directly reused within the same `<Nest>` instance. * * See {@link nest} when not using JSX. * * @example * ```tsx * import { $, Nest } from "rvx"; * * const count = $(0); * * <Nest watch={count}> * {count => { * switch (count) { * case 0: return <h1>Hello World!</h1>; * case 1: return "Something else..."; * } * }} * </Nest> * ``` */ export declare function Nest<T>(props: { /** * The expression to watch. */ watch: T; /** * The component to render with the expression result. * * If the expression returns a component, null or undefined, this can be omitted. */ children: Component<ExpressionResult<T>>; } | { /** * The expression to watch. */ watch: Expression<Component | null | undefined>; }): View; /** * Render 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. * * See {@link Show `<Show>`} when using JSX. * * @param condition The condition to watch. * @param truthy The component to render when the condition result is truthy. * @param falsy An optional component to render when the condition is falsy. * * @example * ```tsx * import { $, when, e } from "rvx"; * * const message = $<null | string>("Hello World!"); * * when(message, value => e("h1").append(value), () => "No message...") * ``` */ export declare function when<T>(condition: Expression<T | Falsy>, truthy: Component<T>, falsy?: Component): View; /** * Render 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. * * See {@link when} when not using JSX. * * @example * ```tsx * import { $, Show } from "rvx"; * * const message = $<null | string>("Hello World!"); * * <Show when={message} else={() => <>No message...</>}> * {value => <h1>{value}</h1>} * </Show> * ``` */ export declare function Show<T>(props: { /** * The condition to watch. */ when: Expression<T | Falsy>; /** * The component to render when the condition result is truthy. */ children: Component<T>; /** * An optional component to render when the condition result is falsy. */ else?: Component; }): View; export interface ForContentFn<T> { /** * @param value The value. * @param index An expression to get the current index. * @returns The content. */ (value: T, index: () => number): Content; } /** * Render content for each unique value in an iterable. * * If an error is thrown while iterating or while rendering an item, the update is stopped as if the previous item was the last one and the error is re-thrown. * * See {@link For `<For>`} for use with JSX. * * @param each The expression to watch. Note, that signals accessed during iteration will also trigger updates. * @param component The component to render for each unique value. * * @example * ```tsx * import { $, forEach, e } from "rvx"; * * const items = $([1, 2, 3]); * * forEach(items, value => e("li").append(value)) * ``` */ export declare function forEach<T>(each: Expression<Iterable<T>>, component: ForContentFn<T>): View; /** * Render content for each unique value in an iterable. * * If an error is thrown while iterating or while rendering an item, the update is stopped as if the previous item was the last one and the error is re-thrown. * * See {@link forEach} when not using JSX. * * @example * ```tsx * import { $, For } from "rvx"; * * const items = $([1, 2, 3]); * * <For each={items}> * {value => <li>{value}</li>} * </For> * ``` */ export declare function For<T>(props: { /** * The expression to watch. Note, that signals accessed during iteration will also trigger updates. */ each: Expression<Iterable<T>>; /** * The component to render for each unique value. */ children: ForContentFn<T>; }): View; export interface IndexContentFn<T> { /** * @param value The value. * @param index The index. * @returns The content. */ (value: T, index: number): Content; } /** * Render 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. * * See {@link Index `<Index>`} when using JSX. * * @param each The expression to watch. Note, that signals accessed during iteration will also trigger updates. * @param component The component to render for each value/index pair. * * @example * ```tsx * import { $, indexEach, e } from "rvx"; * * const items = $([1, 2, 3]); * * indexEach(items, value => e("li").append(value)) * ``` */ export declare function indexEach<T>(each: Expression<Iterable<T>>, component: IndexContentFn<T>): View; /** * Render 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. * * See {@link indexEach} when not using JSX. * * @example * ```tsx * import { $, Index } from "rvx"; * * const items = $([1, 2, 3]); * * <Index each={items}> * {value => <li>{value}</li>} * </Index> * ``` */ export declare function Index<T>(props: { /** * The expression to watch.. * * Note, that signals accessed during iteration will also trigger updates. */ each: Expression<Iterable<T>>; /** * The component to render for each value/index pair. */ children: IndexContentFn<T>; }): View; /** * A wrapper that can be used for moving and reusing views. */ export declare class MovableView { #private; constructor(view: View); /** * Create a new view that contains the wrapped view until it is moved again or detached. * * If the lifecycle in which `move` is called is disposed, the created view no longer updates it's boundary and nodes may be silently removed. */ move: Component<void, View>; /** * Detach content from the currently active view. */ detach(): void; } /** * Render and wrap arbitrary content so that it can be moved and reused. */ export declare function movable(content: Content): MovableView; /** * Attach or detach content depending on an expression. * * Content is kept alive when detached. * * See {@link Attach `<Attach>`} when using JSX. * * @param condition The condition to watch. * @param content The content to attach when the condition result is truthy. * * @example * ```tsx * import { $, attach } from "rvx"; * * const showMessage = $(true); * * attachWhen(showMessage, e("h1").append("Hello World!")) * ``` */ export declare function attachWhen(condition: Expression<boolean>, content: Content): View; /** * Attach or detach content depending on an expression. * * Content is kept alive when detached. * * See {@link attachWhen} when not using JSX. * * @example * ```tsx * import { $, Attach } from "rvx"; * * const showMessage = $(true); * * <Attach when={showMessage}> * <h1>Hello World!</h1> * </Attach> * ``` */ export declare function Attach(props: { /** * The condition to watch. */ when: Expression<boolean>; /** * The content to attach when the condition result is truthy. */ children?: Content; }): View; export {};