eleva

Components Guide

Version: 1.0.0 This guide covers component registration, mounting, children, props, styles, and communication patterns.

Component Definition Structure

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?


Component Registration & Mounting

Global Registration

Register 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);
});

Direct Component Definition

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));

Mounting with Props

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"
});

Unmounting Components

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.


Children Components & Passing Props

Eleva provides multiple ways to mount child components.

Explicit Component Mounting

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",
  },
});

Types of Children Mounting

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

1. Direct Component Mounting

children: {
  "UserCard": "UserCard"  // Direct mounting without container
}

2. Container-Based Mounting

children: {
  "#container": "UserCard"  // Mounting in a container element
}

3. Dynamic Component Mounting

children: {
  ".dynamic-container": {
    setup: ({ signal }) => ({
      userData: signal({ name: "John", role: "admin" })
    }),
    template: (ctx) => `<UserCard :user="userData.value" :editable="true" />`,
    children: { "UserCard": "UserCard" }
  }
}

4. Variable-Based Mounting

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 Patterns

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.


Passing Props to Children

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 Support Any JavaScript Type

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:


Style Injection & Scoped CSS

Eleva supports both static and dynamic styles:

Static 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;
    }
  `
});

Dynamic Styles

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;
    }
  `
});

Inter-Component Communication

Eleva provides multiple ways to share data between components.

Props (Data Down)

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.

Emitter (Events Up)

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:

Store Plugin (Global State)

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:

Decision Guide

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

Anti-Patterns to Avoid

// 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

Nesting Depth Guidelines

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

Multiple Children Mounting

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"
  }
});

Dynamic Children Based on State

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"
  }
});

Summary

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)

Next Steps


← Core Concepts Back to Main Docs Architecture →