UNPKG

woby

Version:

A high-performance framework with fine-grained observable/signal-based reactivity for building rich applications.

523 lines (427 loc) 11.2 kB
# Built-in Components Woby provides several built-in components that handle common UI patterns efficiently. These components are optimized for Woby's reactive system and provide fine-grained updates. ## Table of Contents - [Control Flow](#control-flow) - [Lists and Iteration](#lists-and-iteration) - [Conditional Rendering](#conditional-rendering) - [Async Components](#async-components) - [Utility Components](#utility-components) ## Control Flow ### If Conditionally renders content based on a boolean condition. **Signature:** ```typescript interface IfProps { when: ObservableMaybe<unknown> fallback?: JSX.Element children: JSX.Element } ``` **Usage:** ```typescript import { $, If } from 'woby' const Component = () => { const isLoggedIn = $(false) return ( <div> <If when={isLoggedIn} fallback={<div>Please log in</div>}> <div>Welcome back!</div> </If> <button onClick={() => isLoggedIn(!isLoggedIn())}> Toggle Login </button> </div> ) } ``` ### Switch Renders different content based on multiple conditions (like switch/case). **Signature:** ```typescript interface SwitchProps { fallback?: JSX.Element children: JSX.Element[] } interface MatchProps { when: ObservableMaybe<unknown> children: JSX.Element } ``` **Usage:** ```typescript import { $, Switch, Match } from 'woby' const Component = () => { const status = $<'loading' | 'success' | 'error'>('loading') return ( <div> <Switch fallback={<div>Unknown status</div>}> <Match when={() => status() === 'loading'}> <div>Loading...</div> </Match> <Match when={() => status() === 'success'}> <div>Success!</div> </Match> <Match when={() => status() === 'error'}> <div>Error occurred</div> </Match> </Switch> <button onClick={() => status('success')}>Set Success</button> <button onClick={() => status('error')}>Set Error</button> </div> ) } ``` ### Ternary A more concise conditional rendering component. **Signature:** ```typescript interface TernaryProps { when: ObservableMaybe<unknown> children: [JSX.Element, JSX.Element] // [truthy, falsy] } ``` **Usage:** ```typescript import { $, Ternary } from 'woby' const Component = () => { const isVisible = $(true) return ( <div> <Ternary when={isVisible}> <div>I'm visible!</div> <div>I'm hidden!</div> </Ternary> <button onClick={() => isVisible(!isVisible())}> Toggle </button> </div> ) } ``` ## Lists and Iteration ### For Efficiently renders lists with fine-grained updates. **Signature:** ```typescript interface ForProps<T> { values: ObservableMaybe<readonly T[]> fallback?: JSX.Element children: (item: T, index: Observable<number>) => JSX.Element } ``` **Usage:** ```typescript import { $, For } from 'woby' const TodoList = () => { const todos = $([ { id: 1, text: 'Learn Woby', done: false }, { id: 2, text: 'Build an app', done: false } ]) return ( <ul> <For values={todos} fallback={<li>No todos</li>}> {(todo, index) => ( <li> {index() + 1}. {todo.text} <input type="checkbox" checked={todo.done} onChange={(e) => { todo.done = e.target.checked todos([...todos()]) // Trigger update }} /> </li> )} </For> </ul> ) } ``` ### ForValue Similar to For, but optimized for primitive values. **Signature:** ```typescript interface ForValueProps<T> { values: ObservableMaybe<readonly T[]> fallback?: JSX.Element children: (value: T, index: Observable<number>) => JSX.Element } ``` **Usage:** ```typescript import { $, ForValue } from 'woby' const NumberList = () => { const numbers = $([1, 2, 3, 4, 5]) return ( <div> <ForValue values={numbers}> {(number, index) => ( <span style={{ margin: '0 5px' }}> {number} (#{index() + 1}) </span> )} </ForValue> </div> ) } ``` ### ForIndex Renders based on array indices, useful for fixed-size lists. **Signature:** ```typescript interface ForIndexProps<T> { values: ObservableMaybe<readonly T[]> fallback?: JSX.Element children: (value: () => T, index: number) => JSX.Element } ``` **Usage:** ```typescript import { $, ForIndex } from 'woby' const Grid = () => { const grid = $(Array(9).fill(0).map((_, i) => i + 1)) return ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}> <ForIndex values={grid}> {(value, index) => ( <div style={{ border: '1px solid #ccc', padding: '10px', textAlign: 'center' }} onClick={() => { const newGrid = [...grid()] newGrid[index] = newGrid[index] * 2 grid(newGrid) }} > Cell {index}: {value()} </div> )} </ForIndex> </div> ) } ``` ## Conditional Rendering ### Dynamic Dynamically renders different components based on a value. **Signature:** ```typescript interface DynamicProps { component: ObservableMaybe<Component | string> [key: string]: unknown // Props to pass to the dynamic component } ``` **Usage:** ```typescript import { $, Dynamic } from 'woby' const Button = ({ children, ...props }) => ( <button {...props}>{children}</button> ) const Link = ({ children, href, ...props }) => ( <a href={href} {...props}>{children}</a> ) const Component = () => { const componentType = $<'button' | 'link'>('button') const componentMap = { button: Button, link: Link } return ( <div> <Dynamic component={() => componentMap[componentType()]} onClick={() => console.log('Clicked!')} href="https://example.com" > Dynamic Content </Dynamic> <button onClick={() => componentType('link')}> Switch to Link </button> <button onClick={() => componentType('button')}> Switch to Button </button> </div> ) } ``` ## Async Components ### Suspense Handles loading states for async components. **Signature:** ```typescript interface SuspenseProps { fallback: JSX.Element children: JSX.Element } ``` **Usage:** ```typescript import { lazy, Suspense } from 'woby' const HeavyComponent = lazy(() => import('./HeavyComponent')) const App = () => ( <div> <h1>My App</h1> <Suspense fallback={<div>Loading heavy component...</div>}> <HeavyComponent /> </Suspense> </div> ) ``` ### ErrorBoundary Catches and handles errors in component trees. **Signature:** ```typescript interface ErrorBoundaryProps { fallback: (error: Error, reset: () => void) => JSX.Element onError?: (error: Error) => void children: JSX.Element } ``` **Usage:** ```typescript import { ErrorBoundary } from 'woby' const BuggyComponent = () => { throw new Error('Something went wrong!') } const App = () => ( <div> <h1>My App</h1> <ErrorBoundary fallback={(error, reset) => ( <div> <h2>Something went wrong:</h2> <p>{error.message}</p> <button onClick={reset}>Try again</button> </div> )} onError={(error) => console.error('Caught error:', error)} > <BuggyComponent /> </ErrorBoundary> </div> ) ``` ## Utility Components ### Portal Renders content into a different part of the DOM tree. **Signature:** ```typescript interface PortalProps { mount?: Element children: JSX.Element } ``` **Usage:** ```typescript import { $, Portal } from 'woby' const Modal = ({ isOpen, onClose, children }) => ( <If when={isOpen}> <Portal mount={document.body}> <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={onClose} > <div style={{ backgroundColor: 'white', padding: '20px', borderRadius: '8px' }} onClick={(e) => e.stopPropagation()} > {children} </div> </div> </Portal> </If> ) const App = () => { const modalOpen = $(false) return ( <div> <button onClick={() => modalOpen(true)}> Open Modal </button> <Modal isOpen={modalOpen} onClose={() => modalOpen(false)} > <h2>Modal Content</h2> <p>This is rendered in a portal!</p> </Modal> </div> ) } ``` ### Fragment Groups multiple elements without adding extra DOM nodes. **Signature:** ```typescript interface FragmentProps { children: JSX.Element[] } ``` **Usage:** ```typescript import { Fragment } from 'woby' const Component = () => ( <div> <Fragment> <h1>Title</h1> <p>Description</p> <button>Action</button> </Fragment> </div> ) // Or use the short syntax const ComponentShort = () => ( <div> <> <h1>Title</h1> <p>Description</p> <button>Action</button> </> </div> ) ``` ## Performance Optimization ### Key Considerations 1. **For vs ForValue vs ForIndex**: Choose the right iteration component based on your data type and update patterns. 2. **Conditional Rendering**: Use `If` for simple conditions, `Switch` for multiple conditions, and `Ternary` for inline conditions. 3. **Dynamic Components**: Use `Dynamic` when you need to switch between different component types dynamically. 4. **Error Handling**: Wrap potentially failing components with `ErrorBoundary` to prevent app crashes. 5. **Async Loading**: Use `Suspense` with `lazy` for code splitting and better loading experiences. ### Best Practices ```typescript // Good: Using the right component for the job <For values={complexObjects}> {(item) => <ComplexItem data={item} />} </For> <ForValue values={simpleValues}> {(value) => <span>{value}</span>} </ForValue> // Good: Proper fallback handling <If when={user} fallback={<LoginForm />}> <UserDashboard user={user} /> </If> // Good: Error boundaries around risky components <ErrorBoundary fallback={(error, reset) => <ErrorUI error={error} onRetry={reset} />}> <DataFetchingComponent /> </ErrorBoundary> ``` For more patterns and optimization techniques, see our [Performance Guide](./Performance.md).