Version: 1.0.0 This guide covers component registration, mounting, children, props, styles, and communication patterns.
A component in Eleva is a plain JavaScript object with these properties:
app.component("MyComponent", {
// 1. Setup - Initialize state (optional)
setup({ signal, emitter, props }) {
const state = signal(initialValue);
return { state, /* ...other exports */ };
},
// 2. Template - Define HTML structure (required)
template: (ctx) => `
<div>${ctx.state.value}</div>
`,
// 3. Style - Component-scoped CSS (optional)
style: `
div { color: blue; }
`,
// 4. Children - Child component mappings (optional)
children: {
".child-container": "ChildComponent"
}
});
Why this order?
setup initializes the data that template and style might referencetemplate defines the structure that style will stylestyle applies to the template’s elementschildren maps to elements created in the templateRegister components globally, then mount by name:
const app = new Eleva("MyApp");
app.component("HelloWorld", {
setup({ signal }) {
const count = signal(0);
return { count };
},
template: (ctx) => `
<div>
<h1>Hello, Eleva!</h1>
<p>Count: ${ctx.count.value}</p>
<button @click="() => count.value++">Increment</button>
</div>
`,
});
app.mount(document.getElementById("app"), "HelloWorld").then((instance) => {
console.log("Component mounted:", instance);
});
Mount a component without registering it first:
const DirectComponent = {
template: () => `<div>No setup needed!</div>`,
};
const app = new Eleva("MyApp");
app
.mount(document.getElementById("app"), DirectComponent)
.then((instance) => console.log("Mounted Direct:", instance));
Pass initial data when mounting:
app.component("UserProfile", {
setup({ props }) {
return {
name: props.name || "Guest",
role: props.role || "User"
};
},
template: (ctx) => `
<div>
<h2>${ctx.name}</h2>
<p>Role: ${ctx.role}</p>
</div>
`
});
// Mount with props
app.mount(document.getElementById("app"), "UserProfile", {
name: "Alice",
role: "Admin"
});
const instance = await app.mount(document.getElementById("app"), "MyComponent");
// Later...
await instance.unmount();
The container element receives a _eleva_instance property that references the mounted instance.
Eleva provides multiple ways to mount child components.
Components are explicitly defined in the parent’s children configuration:
// Child Component
app.component("TodoItem", {
setup: (context) => {
const { title, completed, onToggle } = context.props;
return { title, completed, onToggle };
},
template: (ctx) => `
<div class="todo-item ${ctx.completed ? 'completed' : ''}">
<input type="checkbox"
${ctx.completed ? 'checked' : ''}
@click="onToggle" />
<span>${ctx.title}</span>
</div>
`,
});
// Parent Component
app.component("TodoList", {
setup: ({ signal }) => {
const todos = signal([
{ id: 1, title: "Learn Eleva", completed: false },
{ id: 2, title: "Build an app", completed: false },
]);
const toggleTodo = (id) => {
todos.value = todos.value.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
};
return { todos, toggleTodo };
},
template: (ctx) => `
<div class="todo-list">
<h2>My Todo List</h2>
${ctx.todos.value.map((todo) => `
<div key="${todo.id}" class="todo-item"
:title="${todo.title}"
:completed="${todo.completed}"
@click="() => toggleTodo(todo.id)">
</div>
`).join("")}
</div>
`,
children: {
".todo-item": "TodoItem",
},
});
| Type | Syntax | Use Case |
|---|---|---|
| Direct | "UserCard": "UserCard" |
Simple component composition |
| Container-Based | "#container": "UserCard" |
Layout control needed |
| Dynamic | ".container": { setup, template, children } |
Dynamic component behavior |
| Variable-Based | ".container": ComponentVar |
Component from variable |
children: {
"UserCard": "UserCard" // Direct mounting without container
}
children: {
"#container": "UserCard" // Mounting in a container element
}
children: {
".dynamic-container": {
setup: ({ signal }) => ({
userData: signal({ name: "John", role: "admin" })
}),
template: (ctx) => `<UserCard :user="userData.value" :editable="true" />`,
children: { "UserCard": "UserCard" }
}
}
const UserCard = {
setup: (ctx) => ({ /* setup logic */ }),
template: (ctx) => `<div>User Card</div>`,
};
app.component("UserList", {
template: (ctx) => `
<div class="user-list">
<div class="user-card-container"></div>
</div>
`,
children: {
".user-card-container": UserCard, // Mount from variable
},
});
| Selector Type | Example | Use Case |
|---|---|---|
| Class | ".item" |
Multiple elements, list items |
| ID | "#sidebar" |
Single unique element |
| Data attribute | "[data-component]" |
Explicit component markers |
| Nested | ".container .item" |
Scoped selection |
// Class selector - for lists/multiple instances
children: {
".user-card": "UserCard",
".comment": "Comment"
}
// ID selector - for unique elements
children: {
"#header": "Header",
"#footer": "Footer"
}
// Data attribute - explicit and clear
template: () => `
<div data-component="sidebar"></div>
<div data-component="content"></div>
`,
children: {
"[data-component='sidebar']": "Sidebar",
"[data-component='content']": "Content"
}
Recommendation: Use classes for lists, IDs for unique elements, and data attributes when you want explicit component markers.
Props flow from parent template to child via :prop attributes:
app.component("ProductList", {
setup: ({ signal }) => {
const products = signal([
{ id: 1, name: "Widget", price: 29.99 },
{ id: 2, name: "Gadget", price: 49.99 }
]);
function handleSelect(product) {
console.log("Selected:", product);
}
return { products, handleSelect };
},
template: (ctx) => `
<div class="products">
${ctx.products.value.map(product => `
<div key="${product.id}" class="product-card"
:product="product"
:onSelect="() => handleSelect(product)">
</div>
`).join("")}
</div>
`,
children: {
".product-card": "ProductCard"
}
});
// Child receives props
app.component("ProductCard", {
setup: ({ props }) => {
const { product, onSelect } = props;
return { product, onSelect };
},
template: (ctx) => `
<div class="card" @click="onSelect">
<h3>${ctx.product.name}</h3>
<p>$${ctx.product.price}</p>
</div>
`
});
Props are evaluated expressions, so you can pass any value:
| Type | Example |
|---|---|
| Primitives | :count="42", :name="'John'", :active="true" |
| Objects | :user="user.value", :config="{ theme: 'dark' }" |
| Arrays | :items="items.value", :options="[1, 2, 3]" |
| Functions | :onClick="handleClick", :onSubmit="(data) => save(data)" |
| Signals | :userSignal="user" (pass the Signal itself) |
What the child receives depends on what you pass:
:user="user.value" → child receives the evaluated value:user="user" → child receives the Signal itselfEleva supports both static and dynamic styles:
app.component("Button", {
template: () => `<button class="btn">Click me</button>`,
style: `
.btn {
padding: 8px 16px;
background: blue;
color: white;
border: none;
border-radius: 4px;
}
.btn:hover {
background: darkblue;
}
`
});
app.component("ThemableButton", {
setup: ({ signal }) => ({
theme: signal("primary")
}),
template: (ctx) => `
<button class="btn btn-${ctx.theme.value}">Click me</button>
`,
style: (ctx) => `
.btn-primary {
background: blue;
color: white;
}
.btn-secondary {
background: gray;
color: white;
}
`
});
Eleva provides multiple ways to share data between components.
Pass any JavaScript value from parent to child:
// Parent - pass complex data directly (no JSON.stringify!)
template: (ctx) => `
<div class="child"
:user="user.value"
:items="items"
:onSelect="handleSelect">
</div>
`
// Child - receives actual values
setup({ props }) {
// props.user is already an object
// props.items is already an array
// props.onSelect is a callable function
return { user: props.user, items: props.items, onSelect: props.onSelect };
}
// For reactive props in child, pass the signal itself (not .value)
// Parent: :counter="counter"
// Child: props.counter.watch(() => { /* react to changes */ })
Use when: Passing data from parent to child.
Child-to-parent communication, sibling communication, decoupled messaging:
// Child component - emits events
setup({ emitter }) {
function handleClick(item) {
emitter.emit("item:selected", item);
emitter.emit("cart:add", { id: item.id, qty: 1 });
}
return { handleClick };
}
// Parent or any component - listens for events
setup({ emitter }) {
emitter.on("item:selected", (item) => {
console.log("Selected:", item);
});
emitter.on("cart:add", ({ id, qty }) => {
// Update cart state
});
return {};
}
Use when:
Shared state accessible by any component:
import { Store } from "eleva/plugins";
// Initialize store
app.use(Store, {
state: {
user: null,
theme: "light"
},
actions: {
setUser: (state, user) => { state.user.value = user; },
setTheme: (state, theme) => { state.theme.value = theme; }
}
});
// Any component can access store
app.component("UserProfile", {
setup({ store }) {
const user = store.state.user;
const theme = store.state.theme;
function logout() {
store.dispatch("logout");
}
return { user, theme, logout };
},
template: (ctx) => `
<div class="profile">
${ctx.user.value
? `<p>Welcome, ${ctx.user.value.name}!</p>
<button @click="logout">Logout</button>`
: `<p>Please log in</p>`
}
</div>
`
});
Use when:
| Scenario | Solution | Why |
|---|---|---|
| Pass any value to child | Props | Direct value passing |
| Child notifies parent of action | Emitter | Events flow up |
| Siblings need to communicate | Emitter | Decoupled messaging |
| Many components need same data | Store | Central state management |
| Parent updates, child should react | Props (pass signal) | Pass signal reference |
// DON'T: Use JSON.stringify for props (not needed!)
:data='${JSON.stringify(object)}' // Old approach
:data="object" // Just pass directly
// DON'T: Use Store for parent-child only communication
store.dispatch("setParentData", data); // Overkill, use props
// DON'T: Mutate store state directly
store.state.user.value = newUser; // Use actions instead
store.dispatch("setUser", newUser); // Correct
| Depth | Recommendation |
|---|---|
| 1-2 levels | Ideal, easy to understand |
| 3 levels | Acceptable, consider flattening |
| 4+ levels | Too deep, refactor |
// Good: 2 levels deep
// App → UserList → UserCard
// Acceptable: 3 levels
// App → Dashboard → WidgetList → Widget
// Avoid: 4+ levels - hard to trace data flow
// App → Page → Section → List → Item → SubItem
// Consider: Flatten structure or use Store for shared state
Mount different components to different selectors:
app.component("Layout", {
template: () => `
<div class="layout">
<header id="header"></header>
<nav id="nav"></nav>
<main id="content"></main>
<aside id="sidebar"></aside>
<footer id="footer"></footer>
</div>
`,
children: {
"#header": "Header",
"#nav": "Navigation",
"#content": "MainContent",
"#sidebar": "Sidebar",
"#footer": "Footer"
}
});
Conditionally render different components:
app.component("TabPanel", {
setup: ({ signal }) => {
const activeTab = signal("home");
const setTab = (tab) => { activeTab.value = tab; };
return { activeTab, setTab };
},
template: (ctx) => `
<div class="tabs">
<button @click="() => setTab('home')">Home</button>
<button @click="() => setTab('profile')">Profile</button>
<button @click="() => setTab('settings')">Settings</button>
</div>
<div class="tab-content" data-tab="${ctx.activeTab.value}"></div>
`,
children: {
"[data-tab='home']": "HomeTab",
"[data-tab='profile']": "ProfileTab",
"[data-tab='settings']": "SettingsTab"
}
});
| Topic | Key Points |
|---|---|
| Registration | app.component(name, definition) or direct mount |
| Mounting | app.mount(container, compName, props) returns Promise |
| Children | Use children object with selector → component mappings |
| Props | Use :prop syntax; no JSON.stringify needed |
| Styles | Static string or dynamic function |
| Communication | Props (down), Emitter (up), Store (global) |
| ← Core Concepts | Back to Main Docs | Architecture → |