Version: 1.0.0 Advanced state management patterns for complex applications.
A shopping cart demonstrating computed values (subtotal, discount, tax, total).
app.component("ShoppingCart", {
setup({ signal }) {
const items = signal([
{ id: 1, name: "T-Shirt", price: 25, quantity: 2 },
{ id: 2, name: "Jeans", price: 50, quantity: 1 },
{ id: 3, name: "Sneakers", price: 80, quantity: 1 }
]);
const discount = signal(0);
const taxRate = 0.08; // 8% tax
// Computed values as functions
function getSubtotal() {
return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
function getDiscountAmount() {
return getSubtotal() * (discount.value / 100);
}
function getTax() {
return (getSubtotal() - getDiscountAmount()) * taxRate;
}
function getTotal() {
return getSubtotal() - getDiscountAmount() + getTax();
}
function getItemCount() {
return items.value.reduce((sum, item) => sum + item.quantity, 0);
}
function updateQuantity(id, delta) {
items.value = items.value.map(item => {
if (item.id === id) {
const newQty = Math.max(0, item.quantity + delta);
return { ...item, quantity: newQty };
}
return item;
}).filter(item => item.quantity > 0);
}
function removeItem(id) {
items.value = items.value.filter(item => item.id !== id);
}
function applyDiscount(code) {
// Simple discount codes
const discounts = { "SAVE10": 10, "SAVE20": 20, "HALF": 50 };
discount.value = discounts[code.toUpperCase()] || 0;
}
return {
items, discount,
getSubtotal, getDiscountAmount, getTax, getTotal, getItemCount,
updateQuantity, removeItem, applyDiscount
};
},
template: (ctx) => `
<div class="shopping-cart">
<h2>Shopping Cart (${ctx.getItemCount()} items)</h2>
${ctx.items.value.length === 0 ? `
<p class="empty-cart">Your cart is empty</p>
` : `
<div class="cart-items">
${ctx.items.value.map(item => `
<div key="${item.id}" class="cart-item">
<div class="item-info">
<h4>${item.name}</h4>
<p>$${item.price.toFixed(2)} each</p>
</div>
<div class="quantity-controls">
<button @click="() => updateQuantity(${item.id}, -1)">−</button>
<span>${item.quantity}</span>
<button @click="() => updateQuantity(${item.id}, 1)">+</button>
</div>
<div class="item-total">
$${(item.price * item.quantity).toFixed(2)}
</div>
<button class="remove-btn" @click="() => removeItem(${item.id})">×</button>
</div>
`).join("")}
</div>
<div class="discount-section">
<input
type="text"
placeholder="Discount code"
@keyup="(e) => e.key === 'Enter' && applyDiscount(e.target.value)"
/>
<button @click="(e) => applyDiscount(e.target.previousElementSibling.value)">
Apply
</button>
${ctx.discount.value > 0 ? `<span class="discount-applied">${ctx.discount.value}% off applied!</span>` : ""}
</div>
<div class="cart-summary">
<div class="summary-row">
<span>Subtotal:</span>
<span>$${ctx.getSubtotal().toFixed(2)}</span>
</div>
${ctx.discount.value > 0 ? `
<div class="summary-row discount">
<span>Discount (${ctx.discount.value}%):</span>
<span>-$${ctx.getDiscountAmount().toFixed(2)}</span>
</div>
` : ""}
<div class="summary-row">
<span>Tax (8%):</span>
<span>$${ctx.getTax().toFixed(2)}</span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span>$${ctx.getTotal().toFixed(2)}</span>
</div>
</div>
<button class="checkout-btn">Proceed to Checkout</button>
`}
</div>
`,
style: `
.shopping-cart { max-width: 500px; margin: 0 auto; }
.cart-item { display: flex; align-items: center; gap: 15px; padding: 15px 0; border-bottom: 1px solid #eee; }
.item-info { flex: 1; }
.item-info h4 { margin: 0 0 5px 0; }
.item-info p { margin: 0; color: #666; font-size: 14px; }
.quantity-controls { display: flex; align-items: center; gap: 10px; }
.quantity-controls button { width: 30px; height: 30px; border: 1px solid #ddd; background: white; cursor: pointer; border-radius: 4px; }
.item-total { font-weight: bold; min-width: 80px; text-align: right; }
.remove-btn { background: none; border: none; color: #dc3545; font-size: 20px; cursor: pointer; }
.discount-section { display: flex; gap: 10px; margin: 20px 0; align-items: center; }
.discount-section input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.discount-applied { color: #28a745; font-size: 14px; }
.cart-summary { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }
.summary-row { display: flex; justify-content: space-between; margin-bottom: 10px; }
.summary-row.discount { color: #28a745; }
.summary-row.total { font-size: 1.25rem; font-weight: bold; border-top: 1px solid #ddd; padding-top: 10px; margin-top: 10px; }
.checkout-btn { width: 100%; padding: 15px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
`
});
Key Concepts:
State management with undo/redo functionality.
app.component("CounterWithHistory", {
setup({ signal }) {
const count = signal(0);
const history = signal([0]);
const historyIndex = signal(0);
function increment() {
const newCount = count.value + 1;
pushHistory(newCount);
}
function decrement() {
const newCount = count.value - 1;
pushHistory(newCount);
}
function pushHistory(newValue) {
// Remove any future history if we're not at the end
const newHistory = history.value.slice(0, historyIndex.value + 1);
newHistory.push(newValue);
history.value = newHistory;
historyIndex.value = newHistory.length - 1;
count.value = newValue;
}
function undo() {
if (historyIndex.value > 0) {
historyIndex.value--;
count.value = history.value[historyIndex.value];
}
}
function redo() {
if (historyIndex.value < history.value.length - 1) {
historyIndex.value++;
count.value = history.value[historyIndex.value];
}
}
function reset() {
count.value = 0;
history.value = [0];
historyIndex.value = 0;
}
const canUndo = () => historyIndex.value > 0;
const canRedo = () => historyIndex.value < history.value.length - 1;
return { count, history, historyIndex, increment, decrement, undo, redo, reset, canUndo, canRedo };
},
template: (ctx) => `
<div class="counter-history">
<div class="count-display">${ctx.count.value}</div>
<div class="controls">
<button @click="decrement">−</button>
<button @click="increment">+</button>
</div>
<div class="history-controls">
<button @click="undo" ${!ctx.canUndo() ? 'disabled' : ''}>← Undo</button>
<button @click="redo" ${!ctx.canRedo() ? 'disabled' : ''}>Redo →</button>
<button @click="reset">Reset</button>
</div>
<div class="history-timeline">
<p>History: ${ctx.history.value.map((val, i) => `
<span key="${i}" class="${i === ctx.historyIndex.value ? 'current' : ''}">${val}</span>
`).join(' → ')}</p>
</div>
</div>
`,
style: `
.counter-history { text-align: center; max-width: 400px; margin: 0 auto; }
.count-display { font-size: 4rem; font-weight: bold; margin: 20px 0; }
.controls button { font-size: 2rem; width: 60px; height: 60px; margin: 0 10px; cursor: pointer; }
.history-controls { margin-top: 20px; }
.history-controls button { margin: 0 5px; padding: 8px 16px; }
.history-controls button:disabled { opacity: 0.5; cursor: not-allowed; }
.history-timeline { margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; }
.history-timeline span { padding: 2px 8px; border-radius: 4px; }
.history-timeline span.current { background: #007bff; color: white; }
`
});
Managing complex form state with explicit states.
app.component("FormStateMachine", {
setup({ signal }) {
// States: idle, editing, validating, submitting, success, error
const state = signal("idle");
const formData = signal({ email: "", password: "" });
const errors = signal({});
function transition(newState) {
console.log(`State: ${state.value} → ${newState}`);
state.value = newState;
}
function startEditing() {
if (state.value === "idle" || state.value === "success" || state.value === "error") {
transition("editing");
}
}
function updateField(field, value) {
formData.value = { ...formData.value, [field]: value };
if (state.value !== "editing") {
transition("editing");
}
}
async function submit() {
// Validate
transition("validating");
const newErrors = {};
if (!formData.value.email.includes("@")) {
newErrors.email = "Invalid email";
}
if (formData.value.password.length < 6) {
newErrors.password = "Password too short";
}
if (Object.keys(newErrors).length > 0) {
errors.value = newErrors;
transition("error");
return;
}
errors.value = {};
transition("submitting");
try {
// Simulate API call
await new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) resolve();
else reject(new Error("Server error"));
}, 1500);
});
transition("success");
} catch (err) {
errors.value = { submit: err.message };
transition("error");
}
}
function reset() {
formData.value = { email: "", password: "" };
errors.value = {};
transition("idle");
}
const isDisabled = () => state.value === "validating" || state.value === "submitting";
return { state, formData, errors, updateField, submit, reset, isDisabled };
},
template: (ctx) => `
<div class="state-machine-form">
<div class="state-indicator">
Current State: <strong>${ctx.state.value}</strong>
</div>
${ctx.state.value === "success" ? `
<div class="success-message">
<h3>Success!</h3>
<p>Form submitted successfully.</p>
<button @click="reset">Submit Another</button>
</div>
` : `
<form @submit="(e) => { e.preventDefault(); submit(); }">
<div class="form-group">
<label>Email</label>
<input
type="email"
value="${ctx.formData.value.email}"
@input="(e) => updateField('email', e.target.value)"
${ctx.isDisabled() ? 'disabled' : ''}
/>
${ctx.errors.value.email ? `<span class="error">${ctx.errors.value.email}</span>` : ''}
</div>
<div class="form-group">
<label>Password</label>
<input
type="password"
value="${ctx.formData.value.password}"
@input="(e) => updateField('password', e.target.value)"
${ctx.isDisabled() ? 'disabled' : ''}
/>
${ctx.errors.value.password ? `<span class="error">${ctx.errors.value.password}</span>` : ''}
</div>
${ctx.errors.value.submit ? `
<div class="error-message">${ctx.errors.value.submit}</div>
` : ''}
<button type="submit" ${ctx.isDisabled() ? 'disabled' : ''}>
${ctx.state.value === "submitting" ? "Submitting..." : "Submit"}
</button>
</form>
`}
<div class="state-diagram">
<p>States: idle → editing → validating → submitting → success/error</p>
</div>
</div>
`,
style: `
.state-machine-form { max-width: 400px; margin: 0 auto; }
.state-indicator { padding: 10px; background: #e9ecef; border-radius: 4px; margin-bottom: 20px; text-align: center; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; }
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
.error { color: #dc3545; font-size: 14px; }
.error-message { background: #f8d7da; color: #721c24; padding: 10px; border-radius: 4px; margin-bottom: 15px; }
.success-message { text-align: center; padding: 30px; background: #d4edda; border-radius: 8px; }
button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:disabled { background: #6c757d; }
.state-diagram { margin-top: 20px; padding: 10px; background: #f8f9fa; border-radius: 4px; font-size: 12px; color: #666; }
`
});
State management for a multi-step form wizard.
app.component("FormWizard", {
setup({ signal }) {
const currentStep = signal(1);
const totalSteps = 3;
const formData = signal({
// Step 1
firstName: "",
lastName: "",
// Step 2
email: "",
phone: "",
// Step 3
plan: "basic",
terms: false
});
function updateField(field, value) {
formData.value = { ...formData.value, [field]: value };
}
function nextStep() {
if (currentStep.value < totalSteps) {
currentStep.value++;
}
}
function prevStep() {
if (currentStep.value > 1) {
currentStep.value--;
}
}
function goToStep(step) {
if (step >= 1 && step <= totalSteps) {
currentStep.value = step;
}
}
function submit() {
console.log("Submitted:", formData.value);
alert("Form submitted! Check console for data.");
}
const progress = () => ((currentStep.value - 1) / (totalSteps - 1)) * 100;
return { currentStep, totalSteps, formData, updateField, nextStep, prevStep, goToStep, submit, progress };
},
template: (ctx) => `
<div class="form-wizard">
<div class="progress-bar">
<div class="progress" style="width: ${ctx.progress()}%"></div>
</div>
<div class="steps-indicator">
${[1, 2, 3].map(step => `
<div
key="${step}"
class="step ${step === ctx.currentStep.value ? 'active' : ''} ${step < ctx.currentStep.value ? 'completed' : ''}"
@click="() => goToStep(${step})"
>
${step}
</div>
`).join('')}
</div>
<div class="step-content">
${ctx.currentStep.value === 1 ? `
<h3>Personal Information</h3>
<div class="form-group">
<label>First Name</label>
<input
type="text"
value="${ctx.formData.value.firstName}"
@input="(e) => updateField('firstName', e.target.value)"
/>
</div>
<div class="form-group">
<label>Last Name</label>
<input
type="text"
value="${ctx.formData.value.lastName}"
@input="(e) => updateField('lastName', e.target.value)"
/>
</div>
` : ctx.currentStep.value === 2 ? `
<h3>Contact Details</h3>
<div class="form-group">
<label>Email</label>
<input
type="email"
value="${ctx.formData.value.email}"
@input="(e) => updateField('email', e.target.value)"
/>
</div>
<div class="form-group">
<label>Phone</label>
<input
type="tel"
value="${ctx.formData.value.phone}"
@input="(e) => updateField('phone', e.target.value)"
/>
</div>
` : `
<h3>Choose Your Plan</h3>
<div class="plan-options">
${['basic', 'pro', 'enterprise'].map(plan => `
<label key="${plan}" class="plan-option ${ctx.formData.value.plan === plan ? 'selected' : ''}">
<input
type="radio"
name="plan"
value="${plan}"
${ctx.formData.value.plan === plan ? 'checked' : ''}
@change="() => updateField('plan', '${plan}')"
/>
${plan.charAt(0).toUpperCase() + plan.slice(1)}
</label>
`).join('')}
</div>
<label class="terms">
<input
type="checkbox"
${ctx.formData.value.terms ? 'checked' : ''}
@change="(e) => updateField('terms', e.target.checked)"
/>
I agree to the terms and conditions
</label>
`}
</div>
<div class="wizard-actions">
<button
@click="prevStep"
${ctx.currentStep.value === 1 ? 'disabled' : ''}
>
Previous
</button>
${ctx.currentStep.value < ctx.totalSteps ? `
<button class="primary" @click="nextStep">Next</button>
` : `
<button class="primary" @click="submit" ${!ctx.formData.value.terms ? 'disabled' : ''}>
Submit
</button>
`}
</div>
</div>
`,
style: `
.form-wizard { max-width: 500px; margin: 0 auto; }
.progress-bar { height: 4px; background: #e9ecef; border-radius: 2px; margin-bottom: 30px; }
.progress { height: 100%; background: #007bff; border-radius: 2px; transition: width 0.3s; }
.steps-indicator { display: flex; justify-content: center; gap: 20px; margin-bottom: 30px; }
.step { width: 40px; height: 40px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; cursor: pointer; font-weight: bold; }
.step.active { background: #007bff; color: white; }
.step.completed { background: #28a745; color: white; }
.step-content { min-height: 200px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; }
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
.plan-options { display: flex; gap: 10px; margin-bottom: 20px; }
.plan-option { flex: 1; padding: 20px; border: 2px solid #ddd; border-radius: 8px; text-align: center; cursor: pointer; }
.plan-option.selected { border-color: #007bff; background: #f0f7ff; }
.plan-option input { display: none; }
.terms { display: flex; align-items: center; gap: 10px; }
.wizard-actions { display: flex; justify-content: space-between; margin-top: 30px; }
.wizard-actions button { padding: 10px 30px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; }
.wizard-actions button.primary { background: #007bff; color: white; border-color: #007bff; }
.wizard-actions button:disabled { opacity: 0.5; cursor: not-allowed; }
`
});
| Pattern | Use Case | Key Technique |
|---|---|---|
| Shopping Cart | Derived calculations | Computed functions |
| Undo/Redo | Reversible actions | History array + index |
| State Machine | Complex form flows | Explicit state transitions |
| Multi-Step Wizard | Progressive disclosure | Step-based rendering |
| ← Project Structure | Batching & Performance → |