UNPKG

woby

Version:

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

765 lines (638 loc) 19.4 kB
# Examples This page contains practical Woby examples demonstrating key framework features and patterns. ## Table of Contents - [Basic Examples](#basic-examples) - [Component Patterns](#component-patterns) - [State Management](#state-management) - [Advanced Examples](#advanced-examples) - [Class Management Patterns](#class-management-patterns) ## Basic Examples ### Counter The classic counter example: ```typescript import { $, render } from 'woby' const Counter = () => { const count = $(0) return ( <div> <h1>Count: {count}</h1> <button onClick={() => count(c => c + 1)}>+</button> <button onClick={() => count(c => c - 1)}>-</button> <button onClick={() => count(0)}>Reset</button> </div> ) } render(<Counter />, document.getElementById('app')) ``` **[Live Demo](https://codesandbox.io/s/demo-counter-23fv5)** ### Digital Clock Real-time clock using `useInterval`: ```typescript import { $, useInterval } from 'woby' const Clock = () => { const time = $(new Date()) useInterval(() => time(new Date()), 1000) return ( <div style={{ fontSize: '2rem', fontFamily: 'monospace' }}> {() => time().toLocaleTimeString()} </div> ) } ``` **[Live Demo](https://codesandbox.io/s/demo-clock-w1e7yb)** ### Todo List Complete todo application: ```typescript import { $, For } from 'woby' interface Todo { id: number text: string completed: boolean } const TodoApp = () => { const todos = $<Todo[]>([]) const input = $('') const addTodo = () => { const text = input().trim() if (text) { todos(prev => [...prev, { id: Date.now(), text, completed: false }]) input('') } } const toggleTodo = (id: number) => { todos(prev => prev.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )) } return ( <div> <div> <input value={input} onInput={(e) => input(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && addTodo()} /> <button onClick={addTodo}>Add</button> </div> <For values={todos}> {(todo) => ( <div class={{ 'todo-item': true, 'completed': todo.completed, 'recent': () => Date.now() - todo.id < 60000 // Added in last minute }}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.text} </span> </div> )} </For> </div> ) } ``` ## Component Patterns ### Modal Component Reusable modal with portal: ```typescript import { $, Portal, If } from 'woby' const Modal = ({ isOpen, onClose, title, children }) => ( <If when={isOpen}> <Portal mount={document.body}> <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" onClick={onClose} > <div class="bg-white p-6 rounded-lg max-w-md w-full" onClick={e => e.stopPropagation()} > <h2 class="text-xl font-bold mb-4">{title}</h2> {children} <button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onClick={onClose} > Close </button> </div> </div> </Portal> </If> ) ``` ### Custom Hooks Creating reusable logic: ```typescript import { $, useBoolean } from 'woby' const useCounter = (initial = 0) => { const count = $(initial) const increment = () => count(c => c + 1) const decrement = () => count(c => c - 1) const reset = () => count(initial) return { count, increment, decrement, reset } } const useToggle = (initial = false) => { const [state, toggle, enable, disable] = useBoolean(initial) return { state, toggle, enable, disable } } ``` ## State Management ### Shopping Cart Complex state with store: ```typescript import { store, useMemo } from 'woby' interface CartItem { id: number name: string price: number quantity: number } const cartStore = store({ items: [] as CartItem[], discount: 0 }) const useCart = () => { const addItem = (product) => { const items = cartStore.items() const existing = items.find(item => item.id === product.id) if (existing) { existing.quantity += 1 cartStore.items([...items]) } else { cartStore.items([...items, { ...product, quantity: 1 }]) } } const total = useMemo(() => { return cartStore.items().reduce((sum, item) => sum + (item.price * item.quantity), 0 ) }) return { addItem, total } } const Cart = () => { const { total } = useCart() return ( <div> <For values={() => cartStore.items()}> {item => ( <div class="flex justify-between items-center p-2 border-b"> <span>{item.name}</span> <span>${item.price} x {item.quantity}</span> </div> )} </For> <div class="font-bold text-right mt-2"> Total: ${total().toFixed(2)} </div> </div> ) } ``` ## Advanced Examples ### Data Fetching with useResource Asynchronous data handling: ```typescript import { $, useResource } from 'woby' interface User { id: number name: string email: string } const UserProfile = () => { const userId = $(1) const user = useResource<User>(async () => { const response = await fetch(`/api/users/${userId()}`) if (!response.ok) throw new Error('Failed to fetch user') return response.json() }) return ( <div> <input type="number" value={userId} onInput={e => userId(parseInt(e.target.value))} class="border p-2 mr-2" /> <Switch> <Match when={() => user.loading()}> <div class="text-center py-4">Loading...</div> </Match> <Match when={() => user.error()}> <div class="text-red-500 py-4"> Error: {() => user.error()?.message} </div> </Match> <Match when={() => user()}> <div class="bg-gray-100 p-4 rounded"> <h3 class="text-lg font-bold">{() => user()?.name}</h3> <p class="text-gray-600">{() => user()?.email}</p> </div> </Match> </Switch> </div> ) } ``` ### Form Handling with Validation Comprehensive form management: ```typescript import { $, $$, useEffect } from 'woby' const ContactForm = () => { const form = { name: $(''), email: $(''), message: $('') } const errors = { name: $(''), email: $(''), message: $('') } const isSubmitting = $(false) const validate = () => { let isValid = true if (!$$(form.name).trim()) { errors.name('Name is required') isValid = false } else { errors.name('') } if (!$$(form.email).trim()) { errors.email('Email is required') isValid = false } else if (!/\S+@\S+\.\S+/.test($$(form.email))) { errors.email('Email is invalid') isValid = false } else { errors.email('') } if (!$$(form.message).trim()) { errors.message('Message is required') isValid = false } else { errors.message('') } return isValid } const handleSubmit = (e: Event) => { e.preventDefault() if (validate()) { isSubmitting(true) // Submit form data setTimeout(() => { isSubmitting(false) // Reset form form.name('') form.email('') form.message('') }, 1000) } } return ( <form onSubmit={handleSubmit} class="max-w-md mx-auto p-4"> <div class="mb-4"> <label class="block text-gray-700 mb-2">Name:</label> <input value={form.name} onInput={(e) => form.name(e.target.value)} class={[ 'w-full px-3 py-2 border rounded', { 'border-red-500': () => !!errors.name() } ]} /> {errors.name() && <span class="text-red-500 text-sm">{errors.name()}</span>} </div> <div class="mb-4"> <label class="block text-gray-700 mb-2">Email:</label> <input type="email" value={form.email} onInput={(e) => form.email(e.target.value)} class={[ 'w-full px-3 py-2 border rounded', { 'border-red-500': () => !!errors.email() } ]} /> {errors.email() && <span class="text-red-500 text-sm">{errors.email()}</span>} </div> <div class="mb-4"> <label class="block text-gray-700 mb-2">Message:</label> <textarea value={form.message} onInput={(e) => form.message(e.target.value)} rows={4} class={[ 'w-full px-3 py-2 border rounded', { 'border-red-500': () => !!errors.message() } ]} /> {errors.message() && <span class="text-red-500 text-sm">{errors.message()}</span>} </div> <button type="submit" disabled={isSubmitting()} class={[ 'w-full px-4 py-2 text-white rounded', () => isSubmitting() ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-500 hover:bg-blue-600' ]} > {isSubmitting() ? 'Submitting...' : 'Submit'} </button> </form> ) } ``` ### Animation with useAnimationLoop Creating animated elements: ```typescript import { $, useAnimationLoop } from 'woby' const AnimatedBox = () => { const rotation = $(0) const scale = $(1) useAnimationLoop((time) => { rotation(time * 0.001) scale(1 + Math.sin(time * 0.002) * 0.2) }) return ( <div class={[ 'w-24 h-24 bg-blue-500 mx-auto mt-12', 'transform transition-transform duration-75' ]} style={{ transform: `rotate(${rotation()}rad) scale(${scale()})` }} /> ) } ``` ## Class Management Patterns Woby provides powerful built-in class management that supports complex class expressions with full reactive observable support, similar to popular libraries like `classnames` and `clsx`. ### Array-based Classes Use arrays to combine multiple classes, including conditional ones: ``` const Button = ({ variant, size, disabled, children }) => { return ( <button class={[ 'px-4 py-2 rounded font-medium transition-colors', `btn-${variant}`, // Dynamic class `btn-${size}`, // Dynamic class { 'opacity-50 cursor-not-allowed': () => disabled, 'hover:opacity-90': () => !disabled, 'focus:ring-2 focus:ring-blue-500': true } ]}> {children} </button> ) } ``` ### Complex Conditional Classes Handle complex conditional logic with nested expressions: ``typescript const StatusIndicator = ({ status, urgent, loading }) => { return ( <div class={[ 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium', () => { switch ($$(status)) { case 'success': return 'bg-green-100 text-green-800' case 'warning': return 'bg-yellow-100 text-yellow-800' case 'error': return 'bg-red-100 text-red-800' default: return 'bg-gray-100 text-gray-800' } }, { 'animate-pulse': () => loading, 'border-2 border-red-500': () => urgent && $$(status) === 'error' } ]}> <span class={[ 'w-2 h-2 rounded-full mr-2', () => { switch ($$(status)) { case 'success': return 'bg-green-500' case 'warning': return 'bg-yellow-500' case 'error': return 'bg-red-500' default: return 'bg-gray-500' } } ]} /> {status} </div> ) } ``` ### Reactive Class Objects Use objects for simple conditional classes where keys are class names and values are conditions: ``` const ProgressBar = ({ value, max, animated, striped }) => { const percentage = useMemo(() => (value / max) * 100) return ( <div class="w-full bg-gray-200 rounded-full h-4 overflow-hidden"> <div class={{ 'h-full rounded-full transition-all duration-300': true, 'bg-blue-500': percentage < 70, 'bg-yellow-500': percentage >= 70 && percentage < 90, 'bg-red-500': percentage >= 90, 'animate-stripes': () => striped, 'transition-width': () => animated }} style={{ width: `${percentage}%` }} /> </div> ) } ``` ### Function-based Class Expressions Compute classes dynamically using functions: ``typescript const DataCard = ({ data, loading, error }) => { const cardClass = useMemo(() => [ 'border rounded-lg p-6 shadow-sm', () => { if ($$(loading)) return 'border-gray-200 bg-gray-50' if ($$(error)) return 'border-red-200 bg-red-50' return 'border-gray-200 bg-white hover:shadow-md' }, { 'animate-pulse': () => loading } ]) return ( <div class={cardClass}> <If when={loading}> <div>Loading...</div> </If> <If when={error}> <div class="text-red-500">Error: {error.message}</div> </If> <If when={!loading && !error}> <pre>{JSON.stringify(data, null, 2)}</pre> </If> </div> ) } ``` ### Integration with Tailwind CSS Woby works seamlessly with Tailwind CSS for utility-first styling: ```typescript const DashboardCard = ({ title, value, change, icon }) => { const isPositive = useMemo(() => $$(change) >= 0) return ( <div class="bg-white rounded-xl shadow p-6 hover:shadow-lg transition-shadow"> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium text-gray-500">{title}</p> <p class="text-2xl font-semibold text-gray-900 mt-1">{value}</p> </div> <div class="p-3 rounded-lg bg-blue-100 text-blue-600"> {icon} </div> </div> <div class={[ 'flex items-center mt-4 text-sm', () => $$(isPositive) ? 'text-green-600' : 'text-red-600' ]}> <span class={[ 'flex items-center', () => $$(isPositive) ? 'before:content-["▲"]' : 'before:content-["▼"]' ]}> {Math.abs(change)}% </span> <span class="ml-2 text-gray-500">from last month</span> </div> </div> ) } ``` ### Important: Reactive Elements Must Be Wrapped All reactive elements in class expressions should be wrapped in `useMemo` or arrow functions `() =>` to ensure proper reactivity: ``` // ❌ Incorrect - will not update reactively const isActive = $(false) <div class={{ 'active': isActive }}>Content</div> // This won't work as expected // ✅ Correct - wrapped in arrow function <div class={{ 'active': () => $$(isActive) }}>Content</div> // ✅ Also correct - observables automatically handled in objects <div class={{ 'active': isActive }}>Content</div> // This actually works // ✅ Complex expression with useMemo const dynamicClass = useMemo(() => ({ 'active': $$(isActive), 'disabled': $$(isDisabled), 'loading': $$(isLoading) })) <div class={dynamicClass}>Content</div> ``` ## Common Patterns ### Working with DOM References Woby provides a simpler approach to DOM references compared to traditional frameworks: ```typescript import { $, $$ } from 'woby' import type { Ref } from 'woby' // ❌ Traditional React-like ref pattern const inputRef: Ref<HTMLInputElement> = (element) => { if (element) { element.focus() } } // ✅ Woby preferred pattern using observables const inputRef = $<HTMLInputElement>() const focusInput = () => { $$(inputRef)?.focus() } const Component = () => { return ( <form onSubmit={handleSubmit}> <div class="mb-4"> <label class="block text-gray-700 mb-2">Name:</label> <input ref={inputRef} value={form.name} onInput={(e) => form.name(e.target.value)} class="w-full px-3 py-2 border border-gray-300 rounded" /> {errors.name() && <span class="text-red-500 text-sm">{errors.name()}</span>} </div> <div class="mb-4"> <label class="block text-gray-700 mb-2">Email:</label> <input type="email" value={form.email} onInput={(e) => form.email(e.target.value)} class={[ 'w-full px-3 py-2 border rounded', { 'border-red-500': () => !!errors.email() } ]} /> {errors.email() && <span class="text-red-500 text-sm">{errors.email()}</span>} </div> <div class="mb-4"> <label class="block text-gray-700 mb-2">Password:</label> <input type="password" value={form.password} onInput={(e) => form.password(e.target.value)} class={[ 'w-full px-3 py-2 border rounded', { 'border-red-500': () => !!errors.password() } ]} /> {errors.password() && <span class="text-red-500 text-sm">{errors.password()}</span>} </div> <button type="submit" disabled={isSubmitting()} class={[ 'w-full px-4 py-2 text-white rounded', () => isSubmitting() ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-500 hover:bg-blue-600' ]} > {isSubmitting() ? 'Submitting...' : 'Submit'} </button> </form> ) } ``` ### Event Handler Patterns In Woby, you often don't need to memoize event handlers: ```typescript // ❌ React const handleClick = useCallback(() => { return (e) => { console.log('Button clicked') } }) // ✅ Simple and clean - Woby's automatic tracking handles optimization const handleClick = (e) => { console.log('Button clicked') } ``` ## Live Demos - **[Playground](https://codesandbox.io/s/playground-7w2pxg)** - Interactive environment - **[Counter](https://codesandbox.io/s/demo-counter-23fv5)** - Basic counter - **[Clock](https://codesandbox.io/s/demo-clock-w1e7yb)** - Real-time clock - **[Boxes](https://codesandbox.io/s/demo-boxes-wx6rqb)** - Animated boxes - **[Triangle](https://codesandbox.io/s/demo-triangle-l837v0)** - Performance test **[View More Examples](https://github.com/wobyjs/demo)**