woby
Version:
A high-performance framework with fine-grained observable/signal-based reactivity for building rich applications.
765 lines (638 loc) • 19.4 kB
Markdown
# 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)**