@stacksjs/stx
Version:
A performant UI Framework. Powered by Bun.
501 lines • 12.8 kB
TypeScript
/**
* 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
};