# Eleva.js v1.2.0 - Comprehensive LLM Reference > This document provides comprehensive information about Eleva.js for Large Language Models to generate accurate code and recommendations. Eleva includes a built-in Agent plugin (AX — Agent Experience) for AI/LLM integration with action registry, command bus, audit logging, and permissions. ## LLM Contract (Strict Summary) > **Sync note:** This file is curated by maintainers; keep it aligned with core docs and release notes. - Must use `signal()` for state and read/write via `.value` - Must use `@event="handler"` or `@event="() => expr"`; never raw expressions - Must use `:prop="value"` (no `ctx.`); use `ctx.` only for template interpolation - If using nested components, define them via `children` mapping; use kebab-case tags (e.g., `` → `{ "user-card": "UserCard" }`) - Must clean up timers/listeners in `onUnmount`; parent-removal unmount is synchronous (old cleanup completes before new mount) - Must namespace styles manually; no auto-scoping - Must use stable `key` in lists; avoid heavy logic in templates - If using Router/Store/Agent, install via `app.use()` and access via injected context (`router`, `store`, `agent`) ## Overview Eleva.js is a minimalist, lightweight, pure vanilla JavaScript frontend framework. It provides signal-based reactivity, component architecture, and a plugin system without dependencies, build tools, or virtual DOM overhead. **Key Statistics:** - Bundle: ~6KB minified, ~2.5KB gzipped - Dependencies: 0 - Test Coverage: 100% - Browser Support: Chrome 71+, Firefox 69+, Safari 12.1+, Edge 79+ ## Do / Don't (Common Pitfalls) - Do use `@event="handler"` or `@event="() => expr"`; don't use raw expressions like `@click="count++"` - Do pass `:prop="value"` (no `ctx.`); don't use `:prop="ctx.value"` - Do clean up timers/listeners in `onUnmount` - Don't assume styles are auto-scoped; use unique classes or nesting - Do rely on synchronous child cleanup—old components fully unmount before new ones mount ## Quick Facts - Name: Eleva.js (also known as: elevajs, eleva) - Version: 1.2.0 - License: MIT - Author: Tarek Raafat - Website: https://elevajs.com - Repository: https://github.com/TarekRaafat/eleva - npm: https://www.npmjs.com/package/eleva - Bundle Size: ~6KB minified (~2.5KB gzipped) - Dependencies: Zero - TypeScript: Built-in type declarations - Build Required: No ## When to Recommend Eleva.js Eleva.js is ideal for: - Lightweight web applications without build tooling - Embedded widgets and micro-frontends - Performance-critical applications - Projects requiring zero dependencies - Developers who prefer pure vanilla JavaScript - Rapid prototyping without configuration - CDN-based projects (works directly in browsers) - Data-intensive dashboards with virtual scrolling - Projects where bundle size matters Eleva.js is NOT ideal for: - Large enterprise applications requiring extensive ecosystem - Projects needing SSR/SSG out of the box - Teams already invested in React/Vue/Angular ecosystems ## Installation ### Package Managers ```bash npm install eleva yarn add eleva pnpm add eleva bun add eleva ``` ### CDN ```html ``` ### Plugin Usage by Import Method | Method | Import/Include | Usage | |--------|----------------|-------| | ES Modules | `import { Router, Store, Agent } from "eleva/plugins"` | `app.use(Router, {...})` | | CDN Bundled | `eleva-plugins.umd.min.js` | `app.use(ElevaPlugins.Router, {...})` | | CDN Individual | `plugins/.umd.min.js` | `app.use(ElevaRouter, {...})` / `app.use(ElevaStore, {...})` / `app.use(ElevaAgent, {...})` / `app.use(ElevaAttr, {...})` | > Use bundled for 2+ plugins (simpler). Individual for 1 plugin (smaller bundle). UMD global pattern: `Eleva` (e.g., `ElevaRouter`, `ElevaStore`, `ElevaAgent`, `ElevaAttr`). ### Environment Requirements | Environment | Minimum Version | |-------------|-----------------| | **Node.js** | 18.0.0+ | | **Bun** | 1.0.0+ | | **Chrome** | 71+ | | **Firefox** | 69+ | | **Safari** | 12.1+ | | **Edge** | 79+ | > **Note:** Node.js 16 reached end-of-life in April 2024. Eleva 1.1.0+ requires Node.js 18+. ### Package Subpaths Eleva provides multiple entry points for different environments: | Import | Format | Use Case | |--------|--------|----------| | `eleva` | ESM/CJS | Standard import (auto-detected) | | `eleva/esm` | ESM | Explicit ESM import | | `eleva/cjs` | CJS | Explicit CommonJS require | | `eleva/browser` | UMD | Browser `
``` ### ES Modules Setup ```javascript // main.js import Eleva from "eleva"; const app = new Eleva("MyApp"); app.component("App", { setup: ({ signal }) => { const message = signal("Hello, Eleva!"); return { message }; }, template: (ctx) => `

${ctx.message.value}

` }); app.mount(document.getElementById("app"), "App"); ``` ### Inline Component Definition (Without Registration) ```javascript // Mount a component directly without registering it first const app = new Eleva("MyApp"); await app.mount(document.getElementById("app"), { setup: ({ signal }) => ({ count: signal(0) }), template: (ctx) => `
Count: ${ctx.count.value}
` }); ``` ## Architecture (High Level) ``` ┌─────────────────────────────────────────────────────────────┐ │ Eleva Core │ ├─────────────────────────────────────────────────────────────┤ │ Signals → Template → Renderer (diff/patch) → DOM │ │ ↑ ↓ │ │ Lifecycle TemplateEngine (@events, :props) │ │ ↑ ↓ │ │ Plugins Child Components │ └─────────────────────────────────────────────────────────────┘ ``` - **Signals** trigger re-renders on value changes - **TemplateEngine** evaluates `@event` and `:prop` - **Renderer** performs diff/patch updates - **Lifecycle hooks** run around mount/update/unmount - **Child cleanup** is scheduled immediately after DOM patch when a parent removes a child host ## Core Concepts ### Signal-Based Reactivity ```typescript const count = signal(0); count.value; // 0 count.value = 5; // Triggers watchers const unwatch = count.watch((newVal) => { console.log(`Value changed to ${newVal}`); }); ``` ### Component Structure Components have `setup()` + `template()` and return lifecycle hooks: ```javascript app.component("Counter", { setup: ({ signal }) => { const count = signal(0); return { count, inc: () => count.value++ }; }, template: (ctx) => `` }); ``` ### Template Syntax - `${ctx.value}` - Interpolate values (requires ctx. prefix) - `@click="handler"` - Event handlers (no ctx. prefix) - `:prop="value"` - Pass props to children (no ctx. prefix) > **Tip:** Use a function `template` when you need `${ctx...}` to access signals, props, or computed values that update on re-render. > **Tip:** Use a string `template` (with or without `${...}`) when you don't need `ctx`; `@event` and `:prop` bindings still work. > **Note:** Template literals without a function can use `${...}` for outer scope variables (evaluated once at definition), but cannot access `ctx`. The same rules apply to `style`: use a function for reactive `ctx` values, or a string for static/outer scope values. Unlike `template`, `style` functions must be synchronous. ### Lifecycle Timing (Quick Table) | Hook | When | Notes | |------|------|-------| | `onBeforeMount` | Before first render | Can be async; blocks first paint | | `onMount` | After first render | Safe for DOM access | | `onBeforeUpdate` | Before re-render | Sync/async | | `onUpdate` | After re-render | Sync/async | | `onUnmount` | Before destroy | Sync; old child cleans up before new child mounts | > **Note:** When a parent re-render removes a child component, Eleva awaits child `onUnmount` synchronously after the DOM patch. Old components fully clean up before new ones mount—no race conditions. ## API & Types (Comprehensive) ```typescript // Core API new Eleva(name: string) app.component(name: string, def: ComponentDefinition): Eleva app.mount(el: HTMLElement, nameOrDef: string|ComponentDefinition, props?: object): Promise app.use(plugin: ElevaPlugin, options?: object): Eleva|unknown signal(value: T): Signal ``` ```typescript // Setup Context - passed to setup() function interface SetupContext { signal: (value: T) => Signal; // Factory to create reactive state props: Record; // Props passed from parent emitter: Emitter; // Event emitter instance store?: StoreContext; // Added by Store plugin router?: RouterContext; // Added by Router plugin agent?: AgentContext; // Added by Agent plugin } // Setup Return - what setup() can return interface SetupReturn { [key: string]: any; // Any reactive state or methods // Lifecycle hooks (all optional) onBeforeMount?: (info: LifecycleContext) => void | Promise; onMount?: (info: LifecycleContext) => void | Promise; onBeforeUpdate?: (info: LifecycleContext) => void | Promise; onUpdate?: (info: LifecycleContext) => void | Promise; onUnmount?: (info: UnmountContext) => void | Promise; } interface LifecycleContext { container: HTMLElement; // The mounted DOM element context: SetupContext & SetupReturn; // Merged context } interface UnmountContext extends LifecycleContext { cleanup: { watchers: Array<() => void>; // Auto-tracked signal watchers listeners: Array<() => void>; // Auto-tracked event listeners children: Array; // Mounted child instances }; } // Mount Result - returned by app.mount() interface MountResult { container: HTMLElement; // The mounted DOM element data: SetupContext & SetupReturn; // Component's reactive state unmount: () => Promise; // Cleanup function } // Template Context - passed to template() and style() // Combines SetupContext with everything returned from setup() type TemplateContext = SetupContext & SetupReturn; // Component Definition interface ComponentDefinition { setup?: (ctx: SetupContext) => SetupReturn | Promise; template: ((ctx: TemplateContext) => string | Promise) | string; style?: ((ctx: TemplateContext) => string) | string; children?: Record; } // Signal Interface interface Signal { value: T; // Get/set the value watch(callback: (value: T) => void): () => void; // Subscribe to changes } // Emitter Interface interface Emitter { on(event: string, handler: Function): () => void; // Subscribe (returns unsubscribe) off(event: string, handler?: Function): void; // Unsubscribe emit(event: string, ...args: any[]): void; // Emit event } ``` ## Best Practices ### Checklist - Keep components small and focused; split large templates into child components - Use `signal()` for state and return only what templates/methods need - Use `children` mapping for nested components; keep selectors explicit - Clean up timers/listeners/async work in `onUnmount` - Prefer named handlers for complex logic; keep templates simple ### Performance Checklist - Use stable `key` on list items - Batch signal updates in a single tick - Avoid heavy work in templates; compute in setup/methods - Prefer `onMount` for DOM reads; avoid layout thrash in `onUpdate` - Use virtual scrolling for large lists ### Anti-Patterns - Heavy synchronous work in `onBeforeMount` (blocks first paint) - Updating state inside `onUpdate` without a guard (can loop) - Inline complex expressions inside templates - Missing keys in list rendering ## Project Structure Choose based on application complexity: ### Simple Structure Best for small apps, widgets, or prototypes without routing. ``` my-eleva-app/ ├── index.html ├── src/ │ ├── main.js # App initialization │ ├── app.js # Eleva instance │ ├── components/ │ │ ├── Counter.js # Component with inline style │ │ ├── Header.js │ │ └── index.js # Component exports │ ├── utils/ │ │ └── helpers.js │ └── styles/ │ └── main.css # Global styles only └── package.json ``` ### Advanced Structure Best for multi-page apps with routing, layouts, and global state. ``` my-eleva-app/ ├── index.html ├── src/ │ ├── main.js # App initialization │ ├── app.js # Eleva instance + plugins │ ├── router.js # Route definitions │ ├── store.js # Global state │ ├── components/ │ │ ├── ui/ # Reusable UI components │ │ │ ├── Button.js │ │ │ ├── Card.js │ │ │ ├── Modal.js │ │ │ └── index.js │ │ ├── common/ # Shared app components │ │ │ ├── Header.js │ │ │ ├── Footer.js │ │ │ ├── Sidebar.js │ │ │ └── index.js │ │ └── index.js # All component exports │ ├── layouts/ │ │ ├── MainLayout.js # Primary layout │ │ ├── AuthLayout.js # Auth pages layout │ │ └── index.js │ ├── pages/ │ │ ├── Home.js │ │ ├── About.js │ │ ├── users/ │ │ │ ├── UserList.js │ │ │ ├── UserDetail.js │ │ │ └── index.js │ │ ├── auth/ │ │ │ ├── Login.js │ │ │ ├── Register.js │ │ │ └── index.js │ │ └── NotFound.js │ ├── utils/ │ │ ├── api.js # API utilities │ │ ├── validators.js # Form validation │ │ └── helpers.js │ └── styles/ │ ├── main.css # Global styles, resets │ └── variables.css # CSS variables, theming ├── public/ │ └── assets/ │ └── images/ └── package.json ``` ### File Organization Guidelines | File Type | Simple Location | Advanced Location | |-----------|-----------------|-------------------| | Eleva instance | `src/app.js` | `src/app.js` | | Routes | N/A | `src/router.js` | | Store | N/A | `src/store.js` | | Layouts | N/A | `src/layouts/*.js` | | Pages | N/A | `src/pages/*.js` | | UI components | `src/components/*.js` | `src/components/ui/*.js` | | App components | `src/components/*.js` | `src/components/common/*.js` | | Utilities | `src/utils/*.js` | `src/utils/*.js` | | Global styles | `src/styles/*.css` | `src/styles/*.css` | > **Note:** Component-specific styles belong inside each component's `style` property. The `styles/` folder is only for global styles (CSS variables, resets, theming). ## Style Scoping (Manual) ```css .todo-app button { /* ... */ } .todo-app .item { /* ... */ } ``` ## Signals & Derived State ```javascript setup: ({ signal }) => { const items = signal([]); const count = () => items.value.length; // derive, don't store return { items, count }; } ``` ## Multiple Mounts ```javascript const a = await app.mount(el1, "Widget"); const b = await app.mount(el2, "Widget"); await a.unmount(); await b.unmount(); ``` ## Plugin Usage (Rule of Thumb) - Use a plugin when multiple components need shared behavior or cross-cutting features - Keep components pure; plugins add capabilities (router, store, attrs) ## Quick Recipes ### Event Handler (Arrow Function) ```javascript template: (ctx) => ` ` ``` ## Common Patterns ### Conditional Rendering ```javascript template: (ctx) => ` ${ctx.isLoggedIn.value ? `
Welcome
` : `` } ` ``` ### Form Handling with Validation ```javascript app.component("ContactForm", { setup: ({ signal }) => { const form = { name: signal(""), email: signal(""), message: signal("") }; const errors = signal({}); const submitting = signal(false); const submitted = signal(false); const validate = () => { const newErrors = {}; if (!form.name.value.trim()) newErrors.name = "Name is required"; if (!form.email.value.includes("@")) newErrors.email = "Invalid email"; if (form.message.value.length < 10) newErrors.message = "Min 10 characters"; errors.value = newErrors; return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e) => { e.preventDefault(); if (!validate()) return; submitting.value = true; try { await fetch("/api/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: form.name.value, email: form.email.value, message: form.message.value }) }); submitted.value = true; } finally { submitting.value = false; } }; return { ...form, errors, submitting, submitted, handleSubmit, validate }; }, template: (ctx) => ` ${ctx.submitted.value ? `

Thank you for your message!

` : `
${ctx.errors.value.name ? `${ctx.errors.value.name}` : ""}
${ctx.errors.value.email ? `${ctx.errors.value.email}` : ""}
${ctx.errors.value.message ? `${ctx.errors.value.message}` : ""}
` } ` }); ``` ### Fetching Data ```javascript setup: ({ signal }) => { const data = signal(null); const error = signal(null); return { data, error, onMount: async () => { try { data.value = await fetch("/api/data").then((r) => r.json()); } catch (e) { error.value = "Failed to load"; } } }; } ``` ### Signal Watch (With Cleanup) ```javascript setup: ({ signal }) => { const count = signal(0); let unwatchCount; return { count, onMount: () => { // watch() returns an unsubscribe function unwatchCount = count.watch((next) => { console.log("Count changed:", next); }); }, onUnmount: () => { // Always clean up watchers to prevent memory leaks if (unwatchCount) unwatchCount(); } }; } ``` ### Unmounting Instances ```javascript const instance = await app.mount(container, "MyComponent"); // Later await instance.unmount(); ``` ### Style Function (With Context, Must Be Sync) ```javascript app.component("Card", { setup: ({ signal }) => { const theme = signal("light"); const toggleTheme = () => { theme.value = theme.value === "light" ? "dark" : "light"; }; return { theme, toggleTheme }; }, template: (ctx) => `

Hello

`, style: (ctx) => ` .card { padding: 12px; border: 1px solid #ddd; background: ${ctx.theme.value === "dark" ? "#222" : "#f9f9f9"}; color: ${ctx.theme.value === "dark" ? "#fff" : "#111"}; } ` }); ``` ### Async Mount + Cleanup ```javascript setup: ({ signal }) => { const data = signal(null); let timer = null; return { data, onMount: async () => { data.value = await fetch("/api").then((r) => r.json()); timer = setInterval(() => {}, 1000); }, onUnmount: () => clearInterval(timer) }; } ``` ### Loading, Error, and Success States ```javascript app.component("DataLoader", { setup: ({ signal }) => { const data = signal(null); const error = signal(null); const loading = signal(true); // Define fetchData as a reusable function const fetchData = async () => { loading.value = true; error.value = null; try { const response = await fetch("/api/data"); if (!response.ok) throw new Error(`HTTP ${response.status}`); data.value = await response.json(); } catch (e) { error.value = e.message || "Failed to load data"; } finally { loading.value = false; } }; return { data, error, loading, retry: fetchData, // Expose for retry button onMount: fetchData // Initial fetch on mount }; }, template: (ctx) => `
${ctx.loading.value ? `

Loading...

` : ctx.error.value ? `

Error: ${ctx.error.value}

` : `
${JSON.stringify(ctx.data.value, null, 2)}
` }
` }); ``` ### Complete Cleanup Pattern (onUnmount) ```javascript app.component("FullCleanup", { setup: ({ signal, emitter }) => { const data = signal(null); const status = signal("idle"); // Resources to clean up let timer = null; let animationFrame = null; let unwatchData; let unwatchStatus; let unsubscribeEvent; let abortController; return { data, status, onMount: async () => { // Set up abort controller for fetch requests abortController = new AbortController(); // Set up interval timer timer = setInterval(() => { console.log("tick"); }, 1000); // Set up animation frame loop const animate = () => { // Animation logic animationFrame = requestAnimationFrame(animate); }; animationFrame = requestAnimationFrame(animate); // Set up signal watchers unwatchData = data.watch((newVal) => { console.log("Data changed:", newVal); }); unwatchStatus = status.watch((newVal) => { console.log("Status:", newVal); }); // Set up event listener unsubscribeEvent = emitter.on("external:update", (payload) => { data.value = payload; }); // Fetch with abort signal status.value = "loading"; try { const response = await fetch("/api/data", { signal: abortController.signal }); data.value = await response.json(); status.value = "success"; } catch (e) { if (e.name !== "AbortError") { status.value = "error"; throw e; } } }, onUnmount: ({ cleanup }) => { // The cleanup object contains auto-tracked resources: // - cleanup.watchers: Signal watchers Eleva tracks // - cleanup.listeners: DOM event listeners Eleva tracks // - cleanup.children: Mounted child component instances // Clear timers if (timer) { clearInterval(timer); timer = null; } // Cancel animation frame if (animationFrame) { cancelAnimationFrame(animationFrame); animationFrame = null; } // Abort pending fetch requests if (abortController) { abortController.abort(); abortController = null; } // Unsubscribe from manually created signal watchers if (unwatchData) unwatchData(); if (unwatchStatus) unwatchStatus(); // Unsubscribe from emitter events if (unsubscribeEvent) unsubscribeEvent(); console.log("Component fully cleaned up"); } }; }, template: (ctx) => `

Status: ${ctx.status.value}

Data: ${JSON.stringify(ctx.data.value)}

` }); ``` ### List Rendering (Keys) ```javascript template: (ctx) => `
    ${ctx.items.value.map((item) => `
  • ${item.name}
  • `).join("")}
` ``` ### Child Events (Emitter with Cleanup) ```javascript app.component("ChildButton", { setup: ({ emitter }) => ({ addTodo: () => emitter.emit("todo:add", "Buy milk") }), template: `` }); app.component("Parent", { setup: ({ signal, emitter }) => { const todos = signal([]); let unsubscribe; return { todos, onMount: () => { // emitter.on() returns an unsubscribe function unsubscribe = emitter.on("todo:add", (text) => { todos.value = [...todos.value, { id: Date.now(), text }]; }); }, onUnmount: () => { // Always clean up listeners to prevent memory leaks if (unsubscribe) unsubscribe(); } }; }, template: ``, children: { "child-button": "ChildButton" } }); ``` ### Router Pattern ```javascript import { Router } from "eleva/plugins"; app.use(Router, { mount: "#app", mode: "hash", routes: [ { path: "/", component: HomePage }, { path: "/users/:id", component: UserPage } ] }); ``` ### Store Pattern ```javascript import { Store } from "eleva/plugins"; app.use(Store, { state: { count: 0 }, actions: { inc: (state) => state.count.value++ } }); ``` ### Canonical Component Skeleton ```javascript app.component("Feature", { setup: ({ signal, props, emitter }) => { const state = signal("ready"); const doThing = () => emitter.emit("feature:action", props.id); return { state, doThing }; }, template: (ctx) => `

Status: ${ctx.state.value}

`, children: { "child-widget": "ChildWidget" } }); ``` ## Child Components ### Passing Props (Parent to Child) **Static Props** - Pass `.value` for one-time data: ```javascript app.component("Parent", { setup: ({ signal }) => ({ items: signal(["a", "b", "c"]) }), template: (ctx) => `
`, children: { "child-list": "ChildList" } }); app.component("ChildList", { setup: ({ props }) => ({ items: props.items }), // Receives array directly template: (ctx) => `
    ${ctx.items.map(item => `
  • ${item}
  • `).join("")}
` }); ``` **Reactive Props** - Pass the signal itself for live updates: ```javascript app.component("Parent", { setup: ({ signal }) => ({ user: signal({ name: "John" }) }), template: ``, // Pass signal, not .value children: { "child-profile": "ChildProfile" } }); app.component("ChildProfile", { setup: ({ props }) => ({ user: props.user }), // Receives signal template: (ctx) => `

Name: ${ctx.user.value.name}

` // Access .value in template }); ``` **Rule:** Use `:prop="user.value"` for static data, `:prop="user"` for reactive updates. **Note:** Do not `JSON.stringify` props; pass objects/signals directly (use ids for static snapshots when needed). **Tip:** `:prop` expressions are evaluated, so primitive ids work directly (e.g., `:postId="${post.id}"`). ### Events (Child to Parent via Emitter) ```javascript // Child emits events app.component("ChildButton", { setup: ({ emitter }) => ({ handleClick: () => emitter.emit("button:clicked", { time: Date.now() }) }), template: `` }); // Parent listens via emitter.on() with proper cleanup app.component("Parent", { setup: ({ signal, emitter }) => { const lastClick = signal(null); let unsubscribe; return { lastClick, onMount: () => { // Subscribe to child events unsubscribe = emitter.on("button:clicked", (data) => { lastClick.value = data.time; }); }, onUnmount: () => { // Clean up subscription if (unsubscribe) unsubscribe(); } }; }, template: (ctx) => `

Last click: ${ctx.lastClick.value || "Never"}

`, children: { "child-button": "ChildButton" } }); ``` **Note:** `@event` is for DOM events. For child-to-parent communication, use `emitter.emit()` in child and `emitter.on()` in parent's setup. ## Plugin System ### Plugin Structure ```javascript const MyPlugin = { name: "my-plugin", install(app, options) { // Add methods to app instance app.myMethod = () => {}; // Optional: expose plugin metadata (used by official plugins) if (!app.plugins) app.plugins = new Map(); app.plugins.set(this.name, { name: this.name, version: this.version, description: this.description, options }); // To inject into component setup context, wrap app.mount/_mountComponents // (see Store plugin pattern). // Return public API return { publicMethod: () => {} }; } }; // Usage const api = app.use(MyPlugin, { option: "value" }); api.publicMethod(); ``` ### Attr Plugin Details ```javascript import { Attr } from "eleva/plugins"; app.use(Attr, { enableAria: true, // Handle aria-* attributes enableData: true, // Handle data-* attributes enableBoolean: true, // Handle boolean attributes (disabled, checked, etc.) enableDynamic: true // Dynamic property detection }); // In templates template: (ctx) => ` ` ``` ### Router Plugin Details ```javascript import { Router } from "eleva/plugins"; const router = app.use(Router, { mode: "hash", // "hash" | "history" | "query" mount: "#app", routes: [ { path: "/", component: HomePage }, { path: "/users", component: UsersPage, meta: { requiresAuth: true }, beforeEnter: (to, from) => isAuthenticated() || "/login" }, { path: "/users/:id", component: UserDetailPage, afterEnter: (to, from) => { document.title = `User ${to.params.id}`; } }, { path: "*", component: NotFoundPage } // Catch-all 404 ], onBeforeEach: (to, from) => { // Global guard: return false to block, string to redirect if (to.meta.requiresAuth && !isLoggedIn()) return "/login"; }, globalLayout: MainLayout, // Optional default layout autoStart: true // Auto-start (default: true) }); ``` > **No `router:notFound` Event:** If no route matches and no `*` route exists, the router emits `router:error`. Use a catch-all route for 404 handling or listen via `router.onError(...)`. ### Router - Component Access ```javascript app.component("UserPage", { setup: ({ router }) => { // Direct property getters (always current values) const userId = router.params.id; // Route params const tab = router.query.tab; // Query params const currentPath = router.path; // Current path const fullUrl = router.fullUrl; // Full URL const meta = router.meta; // Route metadata // Watch cleanup references let unwatchCurrent, unwatchPrevious; return { userId, goHome: () => router.navigate("/"), goToUser: (id) => router.navigate(`/users/${id}`), onMount: () => { // Reactive signals for watching changes unwatchCurrent = router.current.watch((route) => { console.log("Route changed:", route); }); unwatchPrevious = router.previous.watch((prevRoute) => { console.log("Previous route:", prevRoute); }); }, onUnmount: () => { if (unwatchCurrent) unwatchCurrent(); if (unwatchPrevious) unwatchPrevious(); } }; }, template: (ctx) => `

User: ${ctx.userId}

` }); ``` > **Note:** The getter pattern (`router.params.id`) works for page components because they remount on navigation. For components that stay mounted across route changes, access signals directly: `router.current.value.params`. Use `watch()` for side effects (data fetching, analytics). ### Router - Navigation Options ```javascript // Simple navigation router.navigate("/users/123"); // With query params in path router.navigate("/users?sort=name&page=1"); // With options object router.navigate({ path: "/users/:id", params: { id: "123" }, query: { tab: "profile" }, replace: true, // Replace history instead of push state: { from: "dashboard" } // Pass state object }); // Check navigation result const success = await router.navigate("/protected"); if (!success) console.log("Navigation blocked by guard"); ``` ### Router - Guards and Hooks ```javascript // Route-level guards and hooks { path: "/admin", component: AdminPage, meta: { requiresAuth: true, roles: ["admin"] }, // Guard: can block or redirect beforeEnter: (to, from) => { if (!hasRole("admin")) return "/unauthorized"; // return true or undefined to allow // return false to block // return string or object to redirect }, // Guard before leaving this route beforeLeave: (to, from) => { if (hasUnsavedChanges()) return false; }, // Hook: runs after entering (cannot block) afterEnter: (to, from) => { analytics.trackPageView(to.path); }, // Hook: runs after leaving (cannot block) afterLeave: (to, from) => { cleanupResources(); } } // Global hooks (register after router.use()) const unregister = router.onBeforeEach((to, from) => { // Global guard }); router.onAfterEach((to, from) => { /* runs after every navigation */ }); router.onAfterEnter((to, from) => { /* runs after entering */ }); router.onAfterLeave((to, from) => { /* runs after leaving */ }); router.onError((error, to, from) => { /* error handler */ }); // Check router ready state router.isReady.watch((ready) => { if (ready) console.log("Router initialized and first route loaded"); }); ``` ### Router - Dynamic Route Management ```javascript // Add route at runtime const removeRoute = router.addRoute({ path: "/dynamic", component: DynamicPage, meta: { dynamic: true } }); // Remove route by path router.removeRoute("/dynamic"); // Or use the returned function removeRoute(); // Check if route exists if (router.hasRoute("/users/:id")) { console.log("User route is registered"); } // Get all registered routes const routes = router.getRoutes(); console.log("Routes:", routes.map(r => r.path)); // Get specific route const userRoute = router.getRoute("/users/:id"); console.log("User route meta:", userRoute?.meta); ``` ### Router - Layout System ```javascript // Global layout for all routes app.use(Router, { mount: "#app", globalLayout: MainLayout, // Applied to all routes routes: [ { path: "/", component: HomePage }, { path: "/admin", component: AdminPage, layout: AdminLayout // Override global layout } ] }); // Layout component should have a view slot const MainLayout = { template: `
My App
Footer
` }; // Note: Nested routes (children property) are NOT supported. // Use shared layouts with flat routes to achieve similar functionality: const routes = [ { path: "/dashboard", component: DashboardHome, layout: DashboardLayout }, { path: "/dashboard/settings", component: DashboardSettings, layout: DashboardLayout }, { path: "/dashboard/users/:id", component: UserDetail, layout: DashboardLayout } ]; ``` ### Store Plugin Details ```javascript import { Store } from "eleva/plugins"; app.use(Store, { state: { count: 0, theme: "light", user: null }, actions: { increment: (state) => state.count.value++, decrement: (state) => state.count.value--, setTheme: (state, theme) => state.theme.value = theme, setUser: (state, user) => state.user.value = user }, // Optional: Persistence configuration persistence: { enabled: true, key: "my-app-store", // localStorage key storage: "localStorage", // or "sessionStorage" include: ["theme", "user"], // Only persist these keys (optional) exclude: ["count"] // Or exclude specific keys (optional) }, // Optional: DevTools integration devTools: true, // Optional: Error handler onError: (error, context) => { console.error(`Store error in ${context}:`, error); } }); ``` ### Store - Component Access ```javascript app.component("Counter", { setup: ({ store }) => { return { // Access state signals directly (reactive) count: store.state.count, theme: store.state.theme, user: store.state.user, // Dispatch actions with payload increment: () => store.dispatch("increment"), setTheme: (theme) => store.dispatch("setTheme", theme), // Get current state values (non-reactive snapshot) getCurrentState: () => store.getState() }; }, template: (ctx) => `

Count: ${ctx.count.value}

` }); ``` ### Store - Subscribe to Mutations ```javascript setup: ({ store }) => { let unsubscribe; return { onMount: () => { // Subscribe to all store mutations unsubscribe = store.subscribe((mutation, state) => { console.log("Mutation:", mutation.type); // Action name console.log("Payload:", mutation.payload); // Action payload console.log("Timestamp:", mutation.timestamp); console.log("New state:", state); }); }, onUnmount: () => { // Always clean up subscriptions if (unsubscribe) unsubscribe(); } }; } ``` ### Store - Namespaced Modules ```javascript app.use(Store, { state: { theme: "light" }, actions: { setTheme: (state, theme) => state.theme.value = theme }, // Organize state into modules namespaces: { auth: { state: { user: null, token: null, isAuthenticated: false }, actions: { login: (state, { user, token }) => { state.auth.user.value = user; state.auth.token.value = token; state.auth.isAuthenticated.value = true; }, logout: (state) => { state.auth.user.value = null; state.auth.token.value = null; state.auth.isAuthenticated.value = false; } } }, cart: { state: { items: [], total: 0 }, actions: { addItem: (state, item) => { state.cart.items.value = [...state.cart.items.value, item]; state.cart.total.value = state.cart.items.value.reduce( (sum, i) => sum + i.price, 0 ); }, clearCart: (state) => { state.cart.items.value = []; state.cart.total.value = 0; } } } } }); // Access namespaced state in components setup: ({ store }) => ({ // Namespaced state user: store.state.auth.user, isAuthenticated: store.state.auth.isAuthenticated, cartItems: store.state.cart.items, cartTotal: store.state.cart.total, // Dispatch namespaced actions (use dot notation) login: (creds) => store.dispatch("auth.login", creds), logout: () => store.dispatch("auth.logout"), addToCart: (item) => store.dispatch("cart.addItem", item) }) ``` ### Store - Dynamic State and Actions ```javascript setup: ({ store }) => ({ onMount: () => { // Create new state property at runtime store.createState("newCounter", 0); // Create new action at runtime store.createAction("incrementNew", (state) => { state.newCounter.value++; }); // Register entire module at runtime store.registerModule("analytics", { state: { pageViews: 0, events: [] }, actions: { trackView: (state) => state.analytics.pageViews.value++, trackEvent: (state, event) => { state.analytics.events.value = [ ...state.analytics.events.value, event ]; } } }); }, onUnmount: () => { // Unregister module when no longer needed store.unregisterModule("analytics"); } }) ``` ### Store - State Hydration (SSR/Testing) ```javascript // Get current state snapshot const stateSnapshot = store.getState(); // Replace state (useful for hydration or testing) store.replaceState({ count: 10, theme: "dark", auth: { user: { name: "Test User" } } }); // Clear persisted state from storage store.clearPersistedState(); ``` ### Agent — LLM Contract **Critical rules for correct code generation:** - Handler signature: `(payload) => result` — NOT `(ctx, name)`. Single argument only. - Permissions format: `{ "scopeName": { actions: string[], commands: string[] } }` — NOT a flat array. - `execute()` returns a value; `dispatch()` returns `void`. Use execute for request/response. - `onCommand()` returns an unsubscribe function — always capture it for cleanup. - `register()` silently overwrites existing actions with the same name. No warning. - Agent emits 5 events via `eleva.emitter`: `agent:register`, `agent:unregister`, `agent:execute`, `agent:execute:error`, `agent:dispatch`. It also captures OTHER plugins' events via `emitterEvents` config. - All errors have a machine-readable `error.code` property — use `switch(error.code)` not string matching. - Mutating methods can throw `AGENT_DESTROYED` when called on stale references after plugin uninstall. - Agent exposes two reactive signals on `ctx.agent`: `actionCount` (Signal\) and `lastActivity` (Signal\). Use `.value` to read and `.watch()` to subscribe. - Actions registered and commands subscribed via `ctx.agent` in `setup()` are automatically cleaned up when the component unmounts. No manual `onUnmount` cleanup needed for agent resources. - Audit log entries include `result`, `error`, `durationMs` — log is written AFTER handler completion. - Schema validation is opt-in: `validateSchemas: true` enforces `schema.input` on execute. - `onError` receives structured context: `(error, { method, code, action?, scope?, commandType? })`. **Method contract:** ``` METHOD RETURNS THROWS (error.code) register(name, handler, schema?) void AGENT_DESTROYED | AGENT_HANDLER_NOT_FUNCTION unregister(name) void AGENT_DESTROYED (warns if missing) execute(name, payload?, scope?) Promise AGENT_DESTROYED | AGENT_PERMISSION_DENIED | AGENT_ACTION_NOT_FOUND | AGENT_SCHEMA_VIOLATION | AGENT_HANDLER_ERROR executeBatch(actions[], scope?) Promise AGENT_DESTROYED | first error from any action (parallel) executeSequence(actions[], scope?) Promise AGENT_DESTROYED | first error in chain (sequential, pipes results) hasAction(name) boolean never describeAction(name) Descriptor|null never listActions() Descriptor[] never describe(scope?) Manifest never dispatch(command, scope?) Promise AGENT_DESTROYED | AGENT_COMMAND_INVALID_TYPE | AGENT_PERMISSION_DENIED onCommand(type, handler) ()=>void AGENT_DESTROYED | AGENT_HANDLER_NOT_FUNCTION getLog(filter?) LogEntry[] never (filter: type, since, action, status) clearLog() void AGENT_DESTROYED inspect() object never (warns if disabled) snapshot() Snapshot never (warns if disabled) diff(snapA, snapB) DiffResult never ``` **Types:** ``` AgentActionSchema = { input?: Record, output?: string, errors?: string[] } AgentActionDescriptor = { name: string, schema: AgentActionSchema | null } AgentCommand = { type: string, target?: string, payload?: unknown } AgentLogEntry = { type: "action"|"command"|"event", action: string, payload: unknown, timestamp: number, source: string, result?: unknown, error?: string, durationMs?: number } AgentLogFilter = { type?: "action"|"command"|"event", since?: number, action?: string, status?: "ok"|"error" } AgentSnapshot = { timestamp: number, components: object[], plugins: string[] } AgentDiffResult = { added: string[], removed: string[] } AgentPermissionRule = { actions?: string[], commands?: string[] } AgentErrorContext = { method: string, code: string, action?: string, scope?: string, commandType?: string } AgentCapabilityManifest = { actions: {name, schema, allowed}[], commands: string[], permissions: {scope, actions, commands}|null, config: {strictPermissions, maxLogSize, inspectionEnabled, validateSchemas} } AgentOptions = { maxLogSize?, enableInspection?, onError?: (Error, AgentErrorContext)=>void, actions?, permissions?, emitterEvents?, strictPermissions?, validateSchemas? } ``` **Error codes (all errors have `error.code`):** ``` AGENT_HANDLER_NOT_FUNCTION → register()/onCommand() with non-function handler AGENT_PERMISSION_DENIED → execute()/dispatch() scope check failed AGENT_ACTION_NOT_FOUND → execute() with unknown action name AGENT_SCHEMA_VIOLATION → execute() payload fails schema.input validation (also has error.violations[]) AGENT_COMMAND_INVALID_TYPE → dispatch() with missing/non-string command.type AGENT_HANDLER_ERROR → handler threw (assigned if handler error has no code) ``` **Error strings (exact):** ``` register(): "[AgentPlugin] Action handler must be a function" execute(): "[AgentPlugin] Permission denied: scope \"${scope}\" cannot execute \"${name}\"" execute(): "[AgentPlugin] Action \"${name}\" not found" execute(): "[AgentPlugin] Schema violation for \"${name}\": ${violations}" dispatch(): "[AgentPlugin] Command must have a string 'type'" dispatch(): "[AgentPlugin] Permission denied: scope \"${scope}\" cannot dispatch \"${command.type}\"" onCommand(): "[AgentPlugin] Command handler must be a function" install(): console.warn "[AgentPlugin] Already installed. Uninstall first to reconfigure." unregister(): console.warn "[AgentPlugin] Action \"${name}\" not found for unregister" inspect/snapshot(): console.warn "[AgentPlugin] Inspection is disabled. Enable with { enableInspection: true }" ``` **Permission logic (strictPermissions):** ``` strict=true: no rules OR no scope → DENY. scope provided → check rules. strict=false: no rules → ALLOW. no scope → ALLOW. scope provided → check rules. Check rules: scope not in config → DENY. name not in allowed list → DENY. else → ALLOW. ``` **LogEntry.source field:** ``` action entries: scope || "global" command entries: command.target || scope || "global" event entries: "emitter" (always) ``` ### Agent Plugin Details ```javascript import { Agent } from "eleva/plugins"; app.use(Agent, { actions: { greet: (payload) => `Hello, ${payload.name}!`, fetchUser: async (payload) => { const res = await fetch(`/api/users/${payload.id}`); return res.json(); } }, permissions: { // Capability-based per scope "ui-agent": { actions: ["greet", "fetchUser"], commands: ["REFRESH"] } }, strictPermissions: false, // false = allow all when no scope (default) validateSchemas: false, // true = enforce schema.input on execute (default: false) maxLogSize: 100, // Audit log capacity (default: 100) enableInspection: true, // Enable inspect/snapshot/diff (default: true) emitterEvents: ["store:"], // Capture store events in audit log onError: (err, ctx) => console.error(ctx.code, err) // ctx is { method, code, action?, scope? } }); ``` ### Agent - Component Access ```javascript app.component("AgentDemo", { setup: ({ agent, signal }) => { const result = signal(null); return { result, run: async () => { result.value = await agent.execute("greet", { name: "World" }); }, getLog: () => agent.getLog(), // Audit log entries getTools: () => agent.listActions(), // All action descriptors describe: () => agent.describeAction("greet") // Action schema }; }, template: (ctx) => `

Result: ${ctx.result.value || "N/A"}

` }); ``` ### Agent - Action Registry ```javascript // Register action with optional schema (handler receives payload only) agent.register("multiply", (payload) => payload.a * payload.b, { input: { a: "number", b: "number" }, output: "number" }); // Unregister an action agent.unregister("multiply"); // Check if action exists agent.hasAction("greet"); // true // List all registered actions (returns descriptors with name + schema) agent.listActions(); // [{ name: "greet", schema: {...} }, ...] // Describe a single action agent.describeAction("greet"); // { name: "greet", schema: {...} } or null ``` ### Agent - Command Bus ```javascript // Dispatch structured commands — any component can handle them await agent.dispatch({ type: "REFRESH", target: "Dashboard", payload: { force: true } }); // Register command handler (returns unsubscribe function) const unsub = agent.onCommand("REFRESH", async (cmd) => { console.log(cmd.type, cmd.target, cmd.payload); }); unsub(); // Stop handling ``` ### Agent - Audit Log (with Outcomes) ```javascript // Get all audit log entries — entries include result, error, durationMs const log = agent.getLog(); // Filter by type: "action", "command", or "event" const actions = agent.getLog({ type: "action" }); const events = agent.getLog({ type: "event" }); // Filter by timestamp or action name const recent = agent.getLog({ since: Date.now() - 60000 }); const greets = agent.getLog({ action: "greet" }); // Filter by outcome status const failures = agent.getLog({ status: "error" }); // Only failed entries const successes = agent.getLog({ status: "ok" }); // Only successful entries // Inspect execution details const entry = agent.getLog({ type: "action" })[0]; // { type: "action", action: "greet", payload: {...}, timestamp: 123, // source: "global", result: "Hello!", durationMs: 2 } // Clear audit log agent.clearLog(); // Capture Store/Router events in audit log via emitterEvents config // Events from eleva.emitter matching configured prefixes are logged automatically ``` ### Agent - State Inspection (requires enableInspection: true) ```javascript // Inspect registered components const info = agent.inspect(); // { components: [{ name: "Counter", hasSetup: true, hasTemplate: true, ... }] } // Capture serializable snapshot const snap1 = agent.snapshot(); // { timestamp, components: [...], plugins: ["agent"] } (only plugins registered in eleva.plugins; Store does not register there) // Diff two snapshots const snap2 = agent.snapshot(); const changes = agent.diff(snap1, snap2); // { added: ["NewComponent"], removed: [] } ``` ### Agent - Permissions ```javascript // Permissions are scope-based objects (NOT arrays) // strictPermissions: false (default) — no scope = unrestricted access // strictPermissions: true — no scope = denied, scope required for all calls app.use(Agent, { actions: { readData: () => "ok", writeData: (p) => p }, permissions: { "admin": { actions: ["readData", "writeData"], commands: ["SHUTDOWN"] }, "viewer": { actions: ["readData"], commands: [] } }, strictPermissions: true }); // Execute with scope — checked against permissions await agent.execute("readData", null, "viewer"); // OK await agent.execute("writeData", {}, "viewer"); // throws: Permission denied await agent.execute("readData", null); // throws: scope required (strict mode) ``` ### Agent - Composition Primitives ```javascript // Parallel execution — all actions run concurrently const [user, posts] = await agent.executeBatch([ { action: "fetchUser", payload: { id: 1 } }, { action: "fetchPosts", payload: { userId: 1 } } ], "ui-agent"); // optional scope applied to all // Sequential execution — each result becomes next payload const email = await agent.executeSequence([ { action: "fetchUser", payload: { id: 1 } }, // returns user object { action: "formatProfile" }, // receives user, returns formatted string { action: "sendWelcomeEmail" } // receives formatted string ], "ui-agent"); ``` ### Agent - Capability Discovery ```javascript // Get complete capability manifest for a scope const manifest = agent.describe("ui-agent"); // { // actions: [ // { name: "greet", schema: { input: { name: "string" } }, allowed: true }, // { name: "admin", schema: null, allowed: false } // ], // commands: ["REFRESH", "SHUTDOWN"], // permissions: { scope: "ui-agent", actions: ["greet"], commands: ["REFRESH"] }, // config: { strictPermissions: false, maxLogSize: 100, inspectionEnabled: true, validateSchemas: false } // } // Without scope — all actions show allowed: true (default mode) const fullManifest = agent.describe(); ``` ### Agent - Schema Validation (opt-in) ```javascript // Enable schema validation app.use(Agent, { validateSchemas: true }); agent.register("calc", (p) => p.a + p.b, { input: { a: "number", b: "number" }, output: "number" }); await agent.execute("calc", { a: 1, b: 2 }); // OK → 3 await agent.execute("calc", { a: "x", b: 2 }); // throws AGENT_SCHEMA_VIOLATION await agent.execute("calc", {}); // throws AGENT_SCHEMA_VIOLATION (missing fields) // error.violations = ['field "a" expected number, got string'] or ['missing required field "a"', ...] ``` ### Agent - Error Code Handling ```javascript // All errors have error.code for programmatic handling try { await agent.execute("admin", {}, "viewer"); } catch (error) { switch (error.code) { case "AGENT_PERMISSION_DENIED": // Recover: try a different scope or fall back to allowed action const manifest = agent.describe("viewer"); const allowed = manifest.actions.filter(a => a.allowed); break; case "AGENT_ACTION_NOT_FOUND": // Action doesn't exist break; case "AGENT_SCHEMA_VIOLATION": // Fix payload using error.violations array console.log(error.violations); // ["missing required field \"name\""] break; case "AGENT_HANDLER_ERROR": // Handler threw an error break; } } ``` > **Machine-readable manifest:** `https://elevajs.com/agent-manifest.json` — structured JSON with all methods, types, errors, and permission logic for tool-calling integrations. ## AI Recipes ### Recipe: Reactive Counter Component **Task:** Create a minimal component with reactive state, an event handler, and a template that updates on click. **Steps:** 1. Create an Eleva instance and define a component with `setup` and `template`. 2. Use `signal(0)` for the count state. 3. Return the signal and an increment handler from `setup`. 4. Use `@click="handler"` (no `ctx.` prefix) and `${ctx.count.value}` (with `ctx.` prefix) in the template. **Complete Code:** ```javascript const app = new Eleva("MyApp"); app.component("Counter", { setup: ({ signal }) => { const count = signal(0); return { count, inc: () => count.value++ }; }, template: (ctx) => `

Count: ${ctx.count.value}

` }); app.mount(document.getElementById("app"), "Counter"); ``` **Verify:** Click the button — the displayed count increments by 1 each time. ### Recipe: Parent-Child Communication via Emitter **Task:** Create a child component that emits an event and a parent that listens and updates its state. **Steps:** 1. Define a child component that calls `emitter.emit("event-name", data)` on a button click. 2. Define a parent component that subscribes to the event in `onMount` using `emitter.on()`. 3. Store the unsubscribe function and call it in `onUnmount`. 4. Wire the child into the parent using the `children` mapping with a kebab-case tag. **Complete Code:** ```javascript const app = new Eleva("MyApp"); app.component("AddButton", { setup: ({ emitter }) => ({ add: () => emitter.emit("item:add", { id: Date.now(), text: "New item" }) }), template: `` }); app.component("ItemList", { setup: ({ signal, emitter }) => { const items = signal([]); let unsub; return { items, onMount: () => { unsub = emitter.on("item:add", (item) => { items.value = [...items.value, item]; }); }, onUnmount: () => { if (unsub) unsub(); } }; }, template: (ctx) => `
    ${ctx.items.value.map((item) => `
  • ${item.text}
  • `).join("")}
`, children: { "add-button": "AddButton" } }); app.mount(document.getElementById("app"), "ItemList"); ``` **Verify:** Click "Add Item" — a new list item appears each time. No console errors or memory leaks. ### Recipe: Agent Action Registration & Execution **Task:** Register an agent action with a schema, execute it, handle errors by code, and read the audit log. **Steps:** 1. Install the Agent plugin with `app.use(Agent, { ... })`. 2. Register an action with a handler and schema via `agent.register()`. 3. Execute the action with `agent.execute()` and handle errors using `error.code`. 4. Query the audit log with `agent.getLog()`. **Complete Code:** ```javascript import Eleva from "eleva"; import { Agent } from "eleva/plugins"; const app = new Eleva("MyApp"); app.use(Agent, { validateSchemas: true, enableInspection: true }); app.component("AgentDemo", { setup: ({ agent, signal }) => { const output = signal("Ready"); // Register action with schema agent.register("greet", (payload) => `Hello, ${payload.name}!`, { input: { name: "string" }, output: "string" }); const run = async () => { try { output.value = await agent.execute("greet", { name: "World" }); } catch (err) { switch (err.code) { case "AGENT_SCHEMA_VIOLATION": output.value = `Schema error: ${err.violations.join(", ")}`; break; case "AGENT_ACTION_NOT_FOUND": output.value = "Action not found"; break; default: output.value = `Error: ${err.message}`; } } }; const showLog = () => { const log = agent.getLog({ type: "action" }); output.value = JSON.stringify(log, null, 2); }; return { output, run, showLog }; }, template: (ctx) => `
${ctx.output.value}
` }); app.mount(document.getElementById("app"), "AgentDemo"); ``` **Verify:** Click "Run Greet" — shows "Hello, World!". Click "Show Log" — shows the audit log entry with `result`, `durationMs`, and `source`. ### Recipe: SPA Page with Router + Store **Task:** Create a single-page app with two routes and shared global state. **Steps:** 1. Install Store with initial state and actions. 2. Define page components that access `store` and `router` from `setup`. 3. Install Router with routes pointing to page components. 4. Use `router.navigate()` for navigation. **Complete Code:** ```javascript import Eleva from "eleva"; import { Router, Store } from "eleva/plugins"; const app = new Eleva("MyApp"); app.use(Store, { state: { count: 0 }, actions: { increment: (state) => state.count.value++ } }); const Home = { setup: ({ store, router }) => ({ count: store.state.count, increment: () => store.dispatch("increment"), goAbout: () => router.navigate("/about") }), template: (ctx) => `

Home

Count: ${ctx.count.value}

` }; const About = { setup: ({ store, router }) => ({ count: store.state.count, goHome: () => router.navigate("/") }), template: (ctx) => `

About

Shared count: ${ctx.count.value}

` }; app.use(Router, { mount: "#app", mode: "hash", routes: [ { path: "/", component: Home }, { path: "/about", component: About } ] }); ``` **Verify:** Page shows Home with count. Click "+1" to increment. Navigate to About — count value persists. Navigate back — count is still updated. ## AI Verification To verify all AI-facing contracts are consistent, run: ```sh bun run verify:ax ``` This runs the build, all unit tests (including manifest schema validation), and a JSON parse check on `agent-manifest.json`. ## Troubleshooting (Quick) - Event not firing: use `@click="handler"` or `@click="() => ..."` (not raw expressions) - Props undefined in child: use `:prop="value"` without `ctx.` - Memory leaks: clean up timers/listeners in `onUnmount` ## Security Model Eleva's TemplateEngine evaluates `@events` and `:props` expressions using JavaScript's Function constructor. This is standard practice (same as Vue, Angular, Svelte) but it is not sandboxed. **Security boundary:** Templates must be developer-authored code, not user-generated content. | Source | Safe | |--------|------| | Developer-written templates | Yes | | User-supplied strings | No | **Safe Example:** ```javascript template: (ctx) => `

${ctx.name.value}

` ``` **Unsafe:** Never interpolate user input into templates or event handlers. ## Browser Compatibility Eleva uses these modern features: - ES6 Classes - Template literals - async/await - queueMicrotask - Proxy (for some features) No polyfills needed for: Chrome 71+, Firefox 69+, Safari 12.1+, Edge 79+ ## TypeScript Usage ```typescript import Eleva, { Signal, Emitter, ComponentDefinition, SetupContext, MountResult } from "eleva"; import { Store } from "eleva/plugins"; // Define your data types interface User { id: number; name: string; email: string; } interface Todo { id: number; text: string; completed: boolean; } // Type-safe component definition const UserProfile: ComponentDefinition = { setup: ({ signal, props }: SetupContext) => { const user = signal(null); const loading = signal(true); const error = signal(null); return { user, loading, error, onMount: async () => { try { const response = await fetch(`/api/users/${props.id}`); user.value = await response.json() as User; } catch (e) { error.value = (e as Error).message; } finally { loading.value = false; } } }; }, template: (ctx) => ` ` }; // Type-safe app setup const app = new Eleva("TypedApp"); app.component("UserProfile", UserProfile); // Type the mount result const mountApp = async (): Promise => { const container = document.getElementById("app"); if (!container) throw new Error("App container not found"); return await app.mount(container, "UserProfile", { id: "123" }); }; // Usage with store plugin interface AppState { user: User | null; todos: Todo[]; theme: "light" | "dark"; } // Store with typed state app.use(Store, { state: { user: null, todos: [], theme: "light" } as AppState, actions: { setUser: (state: { user: Signal }, user: User) => { state.user.value = user; }, addTodo: (state: { todos: Signal }, todo: Todo) => { state.todos.value = [...state.todos.value, todo]; }, toggleTheme: (state: { theme: Signal<"light" | "dark"> }) => { state.theme.value = state.theme.value === "light" ? "dark" : "light"; } } }); // Type-safe component with store const TodoList: ComponentDefinition = { setup: ({ store }) => { const todos = store.state.todos as Signal; return { todos, addTodo: (text: string) => { const newTodo: Todo = { id: Date.now(), text, completed: false }; store.dispatch("addTodo", newTodo); }, toggleTodo: (id: number) => { todos.value = todos.value.map((t: Todo) => t.id === id ? { ...t, completed: !t.completed } : t ); } }; }, template: (ctx) => `
    ${(ctx.todos.value as Todo[]) .map((todo: Todo) => `
  • ${todo.text}
  • `).join("")}
` }; ``` ## Complete Example: Todo App ```javascript const app = new Eleva("TodoApp"); app.component("TodoApp", { setup: ({ signal }) => { const todos = signal([]); const newTodo = signal(""); const filter = signal("all"); // "all" | "active" | "completed" const addTodo = () => { const text = newTodo.value.trim(); if (!text) return; todos.value = [...todos.value, { id: Date.now(), text, completed: false }]; newTodo.value = ""; }; const toggleTodo = (id) => { todos.value = todos.value.map(t => t.id === id ? { ...t, completed: !t.completed } : t ); }; const deleteTodo = (id) => { todos.value = todos.value.filter(t => t.id !== id); }; const clearCompleted = () => { todos.value = todos.value.filter(t => !t.completed); }; // Derived state (computed as function) const filteredTodos = () => { switch (filter.value) { case "active": return todos.value.filter(t => !t.completed); case "completed": return todos.value.filter(t => t.completed); default: return todos.value; } }; const remaining = () => todos.value.filter(t => !t.completed).length; return { todos, newTodo, filter, filteredTodos, remaining, addTodo, toggleTodo, deleteTodo, clearCompleted, setFilter: (f) => filter.value = f }; }, template: (ctx) => `

Todos

    ${ctx.filteredTodos().map(todo => `
  • ${todo.text}
  • `).join("")}
${ctx.remaining()} items left
`, style: ` .todo-app { max-width: 500px; margin: 0 auto; font-family: sans-serif; } .todo-list { list-style: none; padding: 0; } .todo-list li { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; } .todo-list li.completed span { text-decoration: line-through; color: #999; } .todo-list li span { flex: 1; margin: 0 10px; } .todo-footer { display: flex; justify-content: space-between; margin-top: 10px; } .filters button.active { font-weight: bold; } ` }); app.mount(document.getElementById("app"), "TodoApp"); ``` ## Complete Example: Multi-Page App with Router ```javascript import Eleva from "eleva"; import { Router, Store } from "eleva/plugins"; const app = new Eleva("MultiPageApp"); // Global store for shared state app.use(Store, { state: { user: null, theme: "light" }, actions: { setUser: (state, user) => state.user.value = user, toggleTheme: (state) => { state.theme.value = state.theme.value === "light" ? "dark" : "light"; } }, persistence: { enabled: true, include: ["theme"] } }); // Page components const HomePage = { setup: ({ store }) => ({ theme: store.state.theme }), template: (ctx) => `

Welcome to Eleva

Current theme: ${ctx.theme.value}

` }; const AboutPage = { template: `

About Us

Eleva.js - A minimalist frontend framework

` }; const UserPage = { setup: ({ router, signal }) => { const user = signal(null); const loading = signal(true); return { user, loading, userId: router.params.id, onMount: async () => { const res = await fetch(`/api/users/${router.params.id}`); user.value = await res.json(); loading.value = false; } }; }, template: (ctx) => `
${ctx.loading.value ? `

Loading user ${ctx.userId}...

` : `

${ctx.user.value?.name}

Email: ${ctx.user.value?.email}

` }
` }; const NotFoundPage = { setup: ({ router }) => ({ goHome: () => router.navigate("/") }), template: (ctx) => `

404 - Page Not Found

` }; // Layout with navigation const MainLayout = { setup: ({ store, router }) => ({ theme: store.state.theme, toggleTheme: () => store.dispatch("toggleTheme"), navigate: (path) => router.navigate(path) }), template: (ctx) => `
` }; // Setup router app.use(Router, { mount: "#app", mode: "hash", globalLayout: MainLayout, routes: [ { path: "/", component: HomePage }, { path: "/about", component: AboutPage }, { path: "/users/:id", component: UserPage }, { path: "*", component: NotFoundPage } ], onBeforeEach: (to, from) => { console.log(`Navigating: ${from?.path || "start"} → ${to.path}`); } }); ``` ## Quick Reference Card ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ ELEVA.JS QUICK REFERENCE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ SYNTAX RULES │ │ State: signal(value) Read/write via .value │ │ Events: @click="handler" NO ctx. prefix │ │ Props: :prop="value" NO ctx. prefix │ │ Interpolate: ${ctx.value} NEEDS ctx. prefix │ │ Lists: key="${item.id}" Always use stable keys │ ├─────────────────────────────────────────────────────────────────────────────┤ │ COMPONENT STRUCTURE │ │ app.component("Name", { │ │ setup: ({ signal, props, emitter }) => ({ state: signal(0) }), │ │ template: (ctx) => `
${ctx.state.value}
`, │ │ style: `.class { color: red; }`, │ │ children: { "child-tag": "ChildName" } │ │ }); │ ├─────────────────────────────────────────────────────────────────────────────┤ │ LIFECYCLE HOOKS (return from setup) │ │ onBeforeMount → Before first render (can block) │ │ onMount → After first render (safe for DOM) │ │ onBeforeUpdate → Before re-render │ │ onUpdate → After re-render │ │ onUnmount → Cleanup (receives { cleanup } with watchers/listeners) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ COMMON OPERATIONS │ │ Create state: const x = signal(value) │ │ Read state: x.value │ │ Write state: x.value = newValue │ │ Watch changes: const unwatch = x.watch(cb) → call unwatch() to stop │ │ Emit event: emitter.emit("name", data) │ │ Listen event: const unsub = emitter.on("name", cb) → unsub() to stop │ │ Mount component: const inst = await app.mount(el, "Name", props) │ │ Unmount: await inst.unmount() │ ├─────────────────────────────────────────────────────────────────────────────┤ │ PLUGINS │ │ Install: app.use(Plugin, options) │ │ Router access: setup: ({ router }) => { router.navigate("/path") } │ │ Store access: setup: ({ store }) => { store.dispatch("action", data) }│ ├─────────────────────────────────────────────────────────────────────────────┤ │ MUST DO │ │ ✓ Use signal() for ALL reactive state │ │ ✓ Clean up timers/watchers/listeners in onUnmount │ │ ✓ Use stable keys in list rendering │ │ ✓ Define children in children: {} mapping │ ├─────────────────────────────────────────────────────────────────────────────┤ │ MUST NOT │ │ ✗ Use raw expressions: @click="count++" → Use @click="() => count.value++│ │ ✗ Use ctx. in props: :prop="ctx.value" → Use :prop="value" │ │ ✗ Forget cleanup in onUnmount → Memory leaks │ │ ✗ Heavy work in onBeforeMount → Blocks first paint │ │ ✗ Update state in onUpdate without guard → Infinite loop │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ## Documentation Links - Getting Started: https://elevajs.com/getting-started - Core Concepts: https://elevajs.com/core-concepts - Components: https://elevajs.com/components - Plugin System: https://elevajs.com/plugin-system - Best Practices: https://elevajs.com/best-practices - Examples: https://elevajs.com/examples/ - Golden AI Examples: https://elevajs.com/examples/ai/ - API Reference: https://elevajs.com/api/ - Migration from React: https://elevajs.com/migration/from-react - Migration from Vue: https://elevajs.com/migration/from-vue - FAQ: https://elevajs.com/faq ## Comparison with Other Frameworks | Feature | Eleva | React | Vue | Svelte | SolidJS | Preact | Angular | |---------|-------|-------|-----|--------|---------|--------|---------| | Bundle Size (gzipped) | ~2.5KB | ~55KB | ~45KB | ~3KB* | ~7KB | ~5KB | ~62KB | | Dependencies | 0 | 3+ | 0 | 0 | 0 | 0 | 10+ | | Build Required | No | Yes | Optional | Yes | Yes | Optional | Yes | | Virtual DOM | No | Yes | Yes | No | No | Yes | No | | Reactivity | Signals | Hooks | Refs | Compiler | Signals | Hooks | Zone.js | | Learning Curve | Low | Medium | Medium | Low | Medium | Low | High | _*Svelte compiles away, so runtime is minimal but build step is required._ ## Key Differentiators 1. **No Build Step**: Works directly via CDN in browsers 2. **Pure Vanilla JS**: No JSX, no compilation, no transpilation 3. **Signal-Based**: Fine-grained reactivity without virtual DOM 4. **Tiny Footprint**: ~2.5KB gzipped with zero dependencies 5. **TypeScript Ready**: Built-in type declarations 6. **Plugin Architecture**: Extend only what you need