UNPKG

@stacksjs/stx

Version:

A performant UI Framework. Powered by Bun.

501 lines 12.8 kB
/** * Creates a reactive state signal. * * State signals are the foundation of STX reactivity. They hold a value that, * when changed, automatically updates any derived signals or effects that * depend on it. * * @param initialValue - The initial value for the signal * @returns A signal that can be read, set, and subscribed to * * @example Basic usage * ```typescript * const count = state(0) * * // Read the value * console.log(count()) // 0 * * // Set a new value * count.set(5) * console.log(count()) // 5 * * // Update based on current value * count.update(n => n + 1) * console.log(count()) // 6 * ``` * * @example With objects * ```typescript * const user = state({ name: 'Alice', age: 30 }) * * // Update the whole object * user.set({ name: 'Bob', age: 25 }) * * // Or update a property (creates new object for immutability) * user.update(u => ({ ...u, age: u.age + 1 })) * ``` * * @example With arrays * ```typescript * const items = state<string[]>([]) * * // Add an item * items.update(arr => [...arr, 'new item']) * * // Remove an item * items.update(arr => arr.filter(i => i !== 'remove me')) * ``` */ export declare function state<T>(initialValue: T): Signal<T>; /** * Creates a derived signal that automatically computes its value from other signals. * * Derived signals are lazy - they only recompute when read after a dependency * has changed. They're perfect for computed values that depend on other state. * * @param compute - A function that computes the derived value * @returns A read-only signal with the computed value * * @example Basic derived value * ```typescript * const count = state(5) * const doubled = derived(() => count() * 2) * * console.log(doubled()) // 10 * count.set(10) * console.log(doubled()) // 20 * ``` * * @example Combining multiple signals * ```typescript * const firstName = state('John') * const lastName = state('Doe') * * const fullName = derived(() => `${firstName()} ${lastName()}`) * console.log(fullName()) // "John Doe" * ``` * * @example Filtering and transforming * ```typescript * const items = state([1, 2, 3, 4, 5]) * const filter = state('even') * * const filteredItems = derived(() => { * const list = items() * return filter() === 'even' * ? list.filter(n => n % 2 === 0) * : list.filter(n => n % 2 !== 0) * }) * ``` */ export declare function derived<T>(compute: () => T): DerivedSignal<T>; /** * Creates a side effect that runs when its dependencies change. * * Effects automatically track which signals they read and re-run whenever * those signals change. They're perfect for syncing state with external * systems, logging, or triggering side effects. * * @param fn - The effect function to run * @param options - Optional configuration * @returns A cleanup function to stop the effect * * @example Basic effect * ```typescript * const count = state(0) * * effect(() => { * console.log(`Count changed to: ${count()}`) * }) * * count.set(1) // Logs: "Count changed to: 1" * count.set(2) // Logs: "Count changed to: 2" * ``` * * @example Effect with cleanup * ```typescript * const isActive = state(true) * * effect(() => { * if (isActive()) { * const interval = setInterval(() => console.log('tick'), 1000) * // Return cleanup function * return () => clearInterval(interval) * } * }) * ``` * * @example Fetching data * ```typescript * const userId = state(1) * const user = state(null) * * effect(async () => { * const id = userId() * const response = await fetch(`/api/users/${id}`) * user.set(await response.json()) * }) * ``` */ export declare function effect(fn: () => void | CleanupFn, options?: EffectOptions): CleanupFn; /** * Batches multiple signal updates into a single effect run. * * Use this when updating multiple signals at once to avoid redundant * effect executions. * * @param fn - Function containing multiple signal updates * * @example * ```typescript * const firstName = state('John') * const lastName = state('Doe') * * // Without batch: effect runs twice * // With batch: effect runs once * batch(() => { * firstName.set('Jane') * lastName.set('Smith') * }) * ``` */ export declare function batch(fn: () => void): void; /** * Registers a callback to run when the component mounts (is inserted into the DOM). * * @param callback - Function to run on mount. Can return a cleanup function. * * @example * ```typescript * onMount(() => { * console.log('Component is now in the DOM') * * // Optional: return cleanup function * return () => console.log('Cleanup on unmount') * }) * ``` * * @example Fetching initial data * ```typescript * const data = state(null) * * onMount(async () => { * data.set(await fetchInitialData()) * }) * ``` */ export declare function onMount(callback: LifecycleCallback): void; /** * Registers a callback to run when the component is destroyed (removed from the DOM). * * @param callback - Function to run on destroy * * @example * ```typescript * onDestroy(() => { * console.log('Component is being removed') * // Clean up subscriptions, timers, etc. * }) * ``` */ export declare function onDestroy(callback: LifecycleCallback): void; /** * Checks if a value is a signal. * * @param value - Value to check * @returns True if the value is a signal * * @example * ```typescript * const count = state(0) * isSignal(count) // true * isSignal(5) // false * ``` */ export declare function isSignal(value: unknown): value is Signal<unknown>; /** * Checks if a value is a derived signal. * * @param value - Value to check * @returns True if the value is a derived signal */ export declare function isDerived(value: unknown): value is DerivedSignal<unknown>; /** * Unwraps a signal to get its raw value. * If the value is not a signal, returns it as-is. * * @param value - A signal or plain value * @returns The unwrapped value * * @example * ```typescript * const count = state(5) * untrack(count) // 5 * untrack(10) // 10 * ``` */ export declare function untrack<T>(value: T | Signal<T> | DerivedSignal<T>): T; /** * Reads a signal's value without tracking it as a dependency. * * Use this when you need to read a signal inside an effect but don't want * the effect to re-run when that signal changes. * * @param fn - Function to run without tracking * @returns The function's return value * * @example * ```typescript * const count = state(0) * const other = state(0) * * effect(() => { * // This effect only re-runs when `count` changes * console.log(count()) * * // Reading `other` without tracking * const otherValue = peek(() => other()) * }) * ``` */ export declare function peek<T>(fn: () => T): T; /** * Generates the browser runtime for STX signals. * * This runtime is automatically injected into pages that use signals. * It provides the full reactivity system and template binding. * * @returns Minified JavaScript runtime code */ export declare function generateSignalsRuntime(): string; /** * Generates readable (non-minified) runtime for development. * * @returns Human-readable JavaScript runtime code */ export declare function generateSignalsRuntimeDev(): string; /** * STX Signals - Reactive State Management * ========================================= * * A simple, intuitive reactivity system for STX templates. * Signals provide fine-grained reactivity with automatic dependency tracking * and seamless template integration. * * ## Why Signals? * * - **No `.value` needed** - Read with `count()`, write with `count.set(5)` * - **Automatic tracking** - Dependencies are tracked automatically in effects * - **Fine-grained updates** - Only affected DOM nodes update, not the whole component * - **Simple API** - Just `state`, `derived`, and `effect` * - **Seamless syntax** - Same `@if`, `@for` directives work on server and client * * ## Quick Start * * ```html * <script> * const count = state(0) * const items = state(['Apple', 'Banana', 'Cherry']) * const showList = state(true) * * function increment() { * count.update(n => n + 1) * } * * function addItem() { * items.update(list => [...list, 'New Item']) * } * </script> * * <button @click="increment">Count: {{ count }}</button> * * <button @click="showList.set(!showList())">Toggle List</button> * * @if="showList()" * <ul> * <li @for="item in items()">{{ item }}</li> * </ul> * @endif * * <button @click="addItem">Add Item</button> * ``` * * ## Core Concepts * * ### State * A state signal holds a reactive value. Read it by calling it as a function, * write it using `.set()` or `.update()`. * * ```typescript * const count = state(0) // Create with initial value * console.log(count()) // Read: 0 * count.set(5) // Write: set to 5 * count.update(n => n + 1) // Update: increment by 1 * ``` * * ### Derived * A derived signal computes its value from other signals. It automatically * updates when its dependencies change. * * ```typescript * const firstName = state('John') * const lastName = state('Doe') * const fullName = derived(() => `${firstName()} ${lastName()}`) * * console.log(fullName()) // "John Doe" * firstName.set('Jane') * console.log(fullName()) // "Jane Doe" * ``` * * ### Effect * Effects run side effects when their dependencies change. They're perfect * for logging, API calls, or DOM manipulation. * * ```typescript * const searchQuery = state('') * * effect(() => { * // This runs whenever searchQuery changes * fetchResults(searchQuery()) * }) * ``` * * ## Template Syntax (all use @ prefix) * * All directives work seamlessly on both server-side and client-side. * Server-side directives are processed at build time. Reactive directives * (those using signals) are handled by the client runtime. * * ### Text Interpolation * ```html * <p>{{ message }}</p> * <p>{{ user.name }}</p> * <p>{{ items().length }} items</p> * ``` * * ### Conditional Rendering * ```html * <div @if="isVisible()">Shown when true</div> * <div @if="user()">Welcome, {{ user().name }}</div> * ``` * * ### List Rendering * ```html * <ul> * <li @for="item in items()">{{ item.name }}</li> * </ul> * * <div @for="item, index in items()"> * {{ index }}: {{ item }} * </div> * ``` * * ### Visibility Toggle * ```html * <!-- @show keeps element in DOM but toggles display --> * <p @show="hasContent()">Toggles visibility</p> * ``` * * ### Attribute Binding * ```html * <img @bind:src="imageUrl()" @bind:alt="imageAlt()"> * <button @bind:disabled="isLoading()">Submit</button> * <div @class="{ active: isActive(), hidden: !visible() }"> * ``` * * ### Event Handling * ```html * <button @click="handleClick">Click me</button> * <input @input="updateValue" @keydown.enter="submit"> * <form @submit.prevent="handleSubmit"> * ``` * * ### Two-Way Binding * ```html * <input @model="username"> * <textarea @model="message"></textarea> * <select @model="selectedOption"> * ``` * * ### Text and HTML Content * ```html * <span @text="message()"></span> * <div @html="richContent()"></div> * ``` * * ## Lifecycle Hooks * * ```typescript * onMount(() => { * console.log('Component mounted') * }) * * onDestroy(() => { * console.log('Component destroyed') * }) * ``` * * @module signals */ /** * A reactive state signal. * * Call it to read the value, use `.set()` to write, or `.update()` to transform. * * @example * ```typescript * const count = state(0) * count() // Read: 0 * count.set(5) // Write: 5 * count.update(n => n + 1) // Update: 6 * ``` */ export declare interface Signal<T> { (): T set(value: T): void update(fn: (current: T) => T): void subscribe(callback: (value: T, prev: T) => void): () => void _isSignal: true } /** * A derived (computed) signal that automatically updates when dependencies change. * * @example * ```typescript * const doubled = derived(() => count() * 2) * doubled() // Read the computed value * ``` */ export declare interface DerivedSignal<T> { (): T _isDerived: true } /** * Options for creating effects. */ export declare interface EffectOptions { immediate?: boolean name?: string } /** * Cleanup function returned by effects. */ export type CleanupFn = () => void /** * Lifecycle hook callback. */ export type LifecycleCallback = () => void | CleanupFn | Promise<void> export default { state, derived, effect, batch, onMount, onDestroy, isSignal, isDerived, untrack, peek, generateSignalsRuntime, generateSignalsRuntimeDev };