eleva

eleva.js - Pure JavaScript, Pure Performance

License GitHub package.json version Version 100% Javascript Zero Dependencies codecov Minified Size Gzipped Size


eleva.js Full Logo

eleva.js - A minimalist, pure vanilla javascript frontend framework. | Product Hunt




Welcome to the official documentation for eleva.js, a minimalist, lightweight, pure vanilla JavaScript frontend runtime framework. Whether you’re new to JavaScript or an experienced developer, this guide will help you understand Eleva’s core concepts, architecture, and how to integrate and extend it in your projects.

Beta Release Notice: This documentation is for eleva.js v1.2.14-beta. The core functionality is stable and suitable for production use. While we’re still gathering feedback before the final v1.0.0 release, the framework has reached a significant milestone in its development. Please be aware of the known limitations and help us improve Eleva by sharing your feedback and experiences.


Table of Contents


1. Introduction

Eleva is designed to offer a simple yet powerful way to build frontend applications using pure vanilla JavaScript. Its goal is to empower developers who value simplicity, performance, and full control over their application to build modular and high-performance apps without the overhead of larger frameworks.


2. Design Philosophy

Eleva is unopinionated. Unlike many frameworks that enforce a specific project structure or coding paradigm, Eleva provides only the minimal core with a flexible plugin system, leaving architectural decisions in your hands. This means:


3. Core Principles

At the heart of Eleva are a few fundamental principles that guide its design and usage:


4. Performance Benchmarks

Preliminary benchmarks illustrate Eleva’s efficiency compared to popular frameworks:

Framework Bundle Size (KB) Initial Load Time (ms) DOM Update Speed (s) Peak Memory Usage (KB) Overall Performance Score (lower is better)
Eleva (Direct DOM) 2 0.05 0.002 0.25 0.58 (Best)
React (Virtual DOM) 4.1 5.34 0.020 0.25 9.71
Vue (Reactive State) 45 4.72 0.021 3.10 13.21
Angular (Two-way Binding) 62 5.26 0.021 0.25 16.88 (Slowest)

Detailed Benchmark Metrics Report

⚠️ Disclaimer: Benchmarks are based on internal tests and may vary by project and environment.


5. Getting Started

Installation

Install via npm:

npm install eleva

Or include via CDN:

<!-- jsDelivr (Recommended) -->
<script src="https://cdn.jsdelivr.net/npm/eleva"></script>

or

<!-- unpkg -->
<script src="https://unpkg.com/eleva"></script>

Quick Start Tutorial

Below is a step-by-step tutorial to help you get started. This example demonstrates component registration, state creation, and mounting using a DOM element (not a selector), with asynchronous handling.

import Eleva from "eleva";

const app = new Eleva("MyApp");

// Define a simple component
app.component("HelloWorld", {
  // Optional setup: if omitted, Eleva defaults to an empty state
  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>
  `,
});

// Mount the component by providing a DOM element and handling the returned Promise
app
  .mount(document.getElementById("app"), "HelloWorld")
  .then((instance) => console.log("Component mounted:", instance));

For interactive demos, check out the CodePen Example.


6. Core Concepts

TemplateEngine

The TemplateEngine is responsible for parsing templates and evaluating embedded expressions.

Example:

const template = "Hello, !";
const data = { name: "World" };
const output = TemplateEngine.parse(template, data);
console.log(output); // "Hello, World!"

Key Features:

Template Interpolation

Eleva supports two methods for dynamic content:

  1. Native Template Literals (${...}):
    Evaluated once, providing static content.

Example:

const greeting = `Hello, ${name}!`; // Evaluates to "Hello, World!" if name is "World"
  1. Handlebars-like Syntax (``):
    Enables dynamic, reactive updates.
<p>Hello, !</p>

When to Use Each:

Setup Context vs. Event Context

Understanding how data flows during component initialization and event handling is key:

Setup Context

Example:

const MyComponent = {
  setup: ({ signal }) => {
    const counter = signal(0);
    return { counter };
  },
  template: (ctx) => `
    <div>
      <p>Counter: ${ctx.counter.value}</p>
    </div>
  `,
};

Event Context

Example:

const MyComponent = {
  setup: ({ signal }) => {
    const counter = signal(0);
    function increment(event) {
      console.log("Event type:", event.type);
      counter.value++;
    }
    return { counter, increment };
  },
  template: (ctx) => `
    <div>
      <p>Counter: ${ctx.counter.value}</p>
      <button @click="increment">Increment</button>
    </div>
  `,
};

Signal (Reactivity)

The Signal provides fine-grained reactivity by updating only the affected DOM parts.

Example:

const count = new Signal(0);

count.watch((newVal) => console.log("Count updated:", newVal));

count.value = 1; // Logs: "Count updated: 1"

Key Features:

Emitter (Event Handling)

The Emitter enables inter-component communication through events and using a publish–subscribe pattern.

Example:

const emitter = new Emitter();

emitter.on("greet", (name) => console.log(`Hello, ${name}!`)); // Logs: "Hello, Alice!"

emitter.emit("greet", "Alice");

Key Features:

Renderer (DOM Diffing)

The Renderer efficiently updates the DOM through direct manipulation, avoiding the overhead of virtual DOM implementations. It uses a performant diffing algorithm to update only the necessary parts of the DOM tree.

Example:

const renderer = new Renderer();

const container = document.getElementById('app');
const newHtml = '<div>Updated content</div>';

renderer.patchDOM(container, newHtml); // Update a container with new HTML

Key Features:

Eleva (Core)

The Eleva class orchestrates component registration, mounting, plugin integration, lifecycle management, and events.

Lifecycle Hooks

Eleva provides a set of lifecycle hooks that allow you to execute code at specific stages of a component’s lifecycle. These hooks are available through the setup method’s return object.

Available Hooks:

Example:

app.component("MyComponent", {
  setup() {
    // Define your lifecycle hooks
    const hooks = {
      beforeMount: () => {
        console.log("Component will mount");
      },
      mounted: () => {
        console.log("Component mounted");
      },
      beforeUpdate: () => {
        console.log("Component will update");
      },
      updated: () => {
        console.log("Component updated");
      },
      beforeUnmount: () => {
        console.log("Component will unmount");
      }
    };

    // Return both your component state and lifecycle hooks
    return {
      // Your component state
      count: 0,
      // Lifecycle hooks
      onBeforeMount: hooks.beforeMount,
      onMount: hooks.mounted,
      onBeforeUpdate: hooks.beforeUpdate,
      onUpdate: hooks.updated,
      onUnmount: hooks.beforeUnmount
    };
  },
  template(ctx) {
    return `<div>Count: ${ctx.count}</div>`;
  }
});

Important Notes:

  1. Lifecycle hooks must be returned from the setup method to be effective
  2. The hooks are available in the setup context but are empty functions by default
  3. You must override these default functions by returning your own implementations
  4. Hooks are called automatically by the framework at the appropriate lifecycle stages

Example (with Reactive State):

app.component("Counter", {
  setup({ signal }) {
    const count = signal(0);
    
    return {
      count,
      onMount: () => {
        console.log("Counter mounted with initial value:", count.value);
      },
      onUpdate: () => {
        console.log("Counter updated to:", count.value);
      }
    };
  },
  template(ctx) {
    return `
      <div>
        <p>Count: ${ctx.count.value}</p>
        <button @click="() => count.value++">Increment</button>
      </div>
    `;
  }
});

Component Registration & Mounting

Register components globally or directly, then mount using a DOM element.

Example (Global Registration):

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

Example (Direct Component Definition):

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

Children Components & Passing Props

Eleva provides two powerful ways to mount child components in your application:

  1. Explicit Component Mounting
    • Components are explicitly defined in the parent component’s children configuration
    • Provides clear parent-child relationships
    • Allows for dynamic prop passing via attributes prefixed with :.

    Example:

    // 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 using explicit mounting
    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 class="todo-item" 
                 :title="${todo.title}"
                 :completed="${todo.completed}"
                 :onToggle="() => toggleTodo(${todo.id})">
            </div>
          `).join('')}
        </div>
      `,
      children: {
        ".todo-item": "TodoItem"  // Explicitly define child component
      },
    });
    
  2. Component Mounting
    • Components are mounted explicitly using their registered names
    • Provides clear and controlled component relationships
    • Supports dynamic prop passing and automatic cleanup

    Example:

    // Child Component
    app.component("UserCard", {
      setup: (context) => {
        const { user, onSelect } = context.props;
        return { user, onSelect };
      },
      template: (ctx) => `
        <div class="user-card" @click="onSelect">
          <img src="${ctx.user.avatar}" alt="${ctx.user.name}" />
          <h3>${ctx.user.name}</h3>
          <p>${ctx.user.role}</p>
        </div>
      `
    });
    
    // Parent Component using explicit mounting
    app.component("UserList", {
      setup: ({ signal }) => {
        const users = signal([
          { id: 1, name: "John Doe", role: "Developer", avatar: "john.jpg" },
          { id: 2, name: "Jane Smith", role: "Designer", avatar: "jane.jpg" }
        ]);
    
        const selectUser = (user) => {
          console.log("Selected user:", user);
        };
    
        return { users, selectUser };
      },
      template: (ctx) => `
        <div class="user-list">
          <h2>Team Members</h2>
          ${ctx.users.value.map(user => `
            <div id="user-card-container"></div>
          `).join('')}
        </div>
      `,
      children: {
        '#user-card-container': {
          setup: (context) => {
            const user = context.props.user;
            return { user };
          },
          template: (ctx) => `
            <UserCard 
              :user='${JSON.stringify(ctx.user)}'
              :onSelect="() => selectUser(${JSON.stringify(ctx.user)})"
            ></UserCard>
          `,
          children: {
           "UserCard": "UserCard"
          }
        }
      }
    });
    

Types of Children Component Mounting

Eleva supports four main approaches to mounting child components, each with its own use cases and benefits:

  1. Direct Component Mounting
    children: {
      "UserCard": "UserCard"  // Direct mounting without container
    }
    
    • Use when: You want to mount a component directly in the parent’s template
    • Benefits:
      • Simplest and most performant approach
      • No additional DOM elements
      • Direct prop passing
    • Example use case: Simple component composition
  2. Container-Based Mounting
    children: {
      "#container": "UserCard"  // Mounting in a container element
    }
    
    • Use when: You need a container element for styling or layout
    • Benefits:
      • Better control over component positioning
      • Ability to add wrapper elements
      • Easier styling and layout management
    • Example use case: Complex layouts or when container styling is needed
  3. Dynamic Component Mounting
    children: {
      ".dynamic-container": {
        setup: (ctx) => ({ /* dynamic setup */ }),
        template: (ctx) => `<UserCard :props="${ctx.props}" />`,
        children: { "UserCard": "UserCard" }
      }
    }
    
    • Use when: You need dynamic component behavior or setup
    • Benefits:
      • Full control over component lifecycle
      • Ability to add custom logic
      • Dynamic prop computation
    • Example use case: Complex component interactions or dynamic data handling
  4. Variable-Based Component Mounting
    // Define component
    const UserCard = {
      setup: (ctx) => ({ /* setup logic */ }),
      template: (ctx) => `<div>User Card</div>`
    };
    
    // Parent component using variable-based mounting
    app.component("UserList", {
      template: (ctx) => `
        <div class="user-list">
          <div class="user-card-container"></div>
        </div>
      `,
      children: {
        ".user-card-container": UserCard  // Mount component directly from variable
      }
    });
    
    • Use when:
      • You have component definitions stored in variables
      • Components are created dynamically
      • You want to reuse component definitions
    • Benefits:
      • No need to register components globally
      • More flexible component composition
      • Better code organization
    • Example use case:
      • Dynamic component creation
      • Component libraries
      • Reusable component patterns

Best Practices for Component Mounting:

  1. Choose the Right Approach:
    • Use direct mounting for simple component relationships
    • Use container-based mounting when layout control is needed
    • Use dynamic mounting for complex component interactions
  2. Performance Considerations:
    • Direct mounting is most performant
    • Container-based mounting adds minimal overhead
    • Dynamic mounting has the most flexibility but requires careful optimization
  3. Maintainability:
    • Keep component hierarchies shallow when possible
    • Use meaningful container names
    • Document complex mounting patterns

Supported Children Selector Types

Eleva supports various selector types for defining child components in the children configuration:

  1. Component Name Selectors
    children: {
      "UserCard": "UserCard"  // Mounts UserCard component directly
    }
    
    • Best for: Direct component mounting without additional container elements
    • Use when: You want to mount a component directly without a wrapper element
  2. ID Selectors
    children: {
      "#user-card-container": "UserCard"  // Mounts in element with id="user-card-container"
    }
    
    • Best for: Unique, specific mounting points
    • Use when: You need to target a specific element in the template
  3. Class Selectors
    children: {
      ".todo-item": "TodoItem"  // Mounts in elements with class="todo-item"
    }
    
    • Best for: Multiple instances of the same component
    • Use when: You have a list or grid of similar components
  4. Attribute Selectors
    children: {
      "[data-component='user-card']": "UserCard"  // Mounts in elements with data-component="user-card"
    }
    
    • Best for: Semantic component identification
    • Use when: You want to use custom attributes for component mounting

Best Practices for Selector Types:

  1. Prefer Component Name Selectors when:
    • Mounting components directly without containers
    • Working with simple, direct component relationships
    • Performance is a priority (fewer DOM queries)
  2. Use ID Selectors when:
    • You need to target specific, unique mounting points
    • Working with complex layouts
    • Components need to be mounted in specific locations
  3. Choose Class Selectors when:
    • Working with lists or repeated components
    • Components share the same mounting pattern
    • You need to style or target multiple instances
  4. Consider Attribute Selectors when:
    • You need semantic component identification
    • Working with custom component attributes
    • Building complex component hierarchies

Performance Considerations:

Key Benefits of Component Mounting:

Style Injection & Scoped CSS

Eleva supports component-scoped styling through an optional style function defined in a component. The styles are injected into the component’s container to avoid global leakage.

Example:

const MyComponent = {
  style: (ctx) => `
    .my-component {
      color: blue;
      padding: rem;
    }
  `,
  template: (ctx) => `<div class="my-component">Styled Component</div>`,
};

Inter-Component Communication

Inter-component communication is facilitated by the built-in Emitter. Components can publish and subscribe to events, enabling decoupled interactions.

Example:

// Component A emits an event
app.component("ComponentA", {
  setup: ({ emitter }) => {
    function sendMessage() {
      emitter.emit("customEvent", "Hello from A");
    }
    return { sendMessage };
  },
  template: () => `<button @click="sendMessage">Send Message</button>`,
});

// Component B listens for the event
app.component("ComponentB", {
  setup: ({ emitter }) => {
    emitter.on("customEvent", (msg) => console.log(msg));
    return {};
  },
  template: () => `<div>Component B</div>`,
});

app.mount(document.getElementById("app"), "ComponentA");
app.mount(document.getElementById("app"), "ComponentB");

7. Architecture & Data Flow

Eleva’s design emphasizes clarity, modularity, and performance. This section explains how data flows through the framework and how its key components interact, providing more clarity on the underlying mechanics.

Key Components

  1. Component Definition:
    Components are plain JavaScript objects that describe a UI segment. They typically include:

    • A template function that returns HTML with interpolation placeholders.
    • An optional setup() function for initializing state (using reactive signals).
    • An optional style function for scoped CSS.
    • An optional children object for nested components.
  2. Signals (Reactivity): Signals are reactive data holders that notify watchers when their values change, triggering re-renders of the affected UI.

  3. TemplateEngine (Rendering): This module processes template strings by replacing placeholders (e.g., 8) with live data, enabling dynamic rendering.

  4. Renderer (DOM Diffing and Patching): The Renderer compares the new virtual DOM with the current DOM and patches only the parts that have changed, ensuring high performance and efficient updates.

  5. Emitter (Event Handling): The Emitter implements a publish–subscribe pattern to allow components to communicate by emitting and listening to custom events.

Data Flow Process

  1. Initialization:

    • Registration: Components are registered via app.component().
    • Mounting: app.mount() creates a context (including props, lifecycle hooks, and an emitter property) and executes setup() (if present) to create a reactive state.
  2. Rendering:

    • The template function is called with the combined context and reactive state.
    • The TemplateEngine parses the template, replacing expressions like 8 with the current values.
    • The Renderer takes the resulting HTML and patches it into the DOM, ensuring only changes are applied.
  3. Reactivity:

    • When a signal’s value changes (e.g., through a user event), its watcher triggers a re-run of the template.
    • The Renderer diffs the new HTML against the current DOM and applies only the necessary changes.
  4. Events:

    • Eleva binds event listeners (e.g., @click) during rendering.
    • When an event occurs, the handler is executed with the current state and event details.
    • Components can also emit custom events via the Emitter for cross-component communication.

Visual Overview

[Component Registration]
         │
         ▼
[Mounting & Context Creation]
         │
         ▼
[setup() Execution]
         │
         ▼
[Template Function Produces HTML]
         │
         ▼
[TemplateEngine Processes HTML]
         │
         ▼
[Renderer Patches the DOM] ◂────────┐
         │                          │
         ▼                          │
[User Interaction / Signal Change]  │
         │                          │
         ▼                          │ ↺
[Signal Watchers Trigger Re-render] │
         │                          │
         ▼                          │
[Renderer Diffs the DOM]   ─────────┘

Benefits


8. Plugin System

The Plugin System in Eleva provides a powerful way to extend the framework’s functionality. Plugins can add new features, modify existing behavior, or integrate with external libraries.

Plugin Structure

A plugin in Eleva is an object that must have two required properties:

const MyPlugin = {
  name: 'myPlugin', // Unique identifier for the plugin
  install(eleva, options) {
    // Plugin installation logic
  }
};

Installing Plugins

Plugins are installed using the use method on an Eleva instance:

const app = new Eleva('myApp');
app.use(MyPlugin, { /* optional configuration */ });

The use method:

Plugin Capabilities

Plugins can:

  1. Extend the Eleva Instance
    install(eleva) {
      eleva.newMethod = () => { /* ... */ };
    }
    
  2. Add Component Features
    install(eleva) {
      eleva.component('enhanced-component', {
        template: (ctx) => `...`,
        setup: (ctx) => ({ /* ... */ })
      });
    }
    
  3. Modify Component Behavior
    install(eleva) {
      const originalMount = eleva.mount;
      eleva.mount = function(container, compName, props) {
        // Add pre-mount logic
        const result = originalMount.call(this, container, compName, props);
        // Add post-mount logic
        return result;
      };
    }
    
  4. Add Global State or Services
    install(eleva) {
      eleva.services = {
        api: new ApiService(),
        storage: new StorageService()
      };
    }
    

Best Practices

  1. Naming Conventions
    • Use unique, descriptive names for plugins
    • Follow the pattern: eleva-{plugin-name} for published plugins
  2. Error Handling
    • Implement proper error handling in plugin methods
    • Provide meaningful error messages for debugging
  3. Documentation
    • Document plugin options and methods
    • Include usage examples
    • Specify any dependencies or requirements
  4. Performance
    • Keep plugin initialization lightweight
    • Use lazy loading for heavy features
    • Clean up resources when components unmount

Example Plugin

Here’s a complete example of a custom plugin:

const LoggerPlugin = {
  name: 'logger',
  install(eleva, options = {}) {
    const { level = 'info' } = options;
    
    // Add logging methods to Eleva instance
    eleva.log = {
      info: (msg) => console.log(`[INFO] ${msg}`),
      warn: (msg) => console.warn(`[WARN] ${msg}`),
      error: (msg) => console.error(`[ERROR] ${msg}`)
    };

    // Enhance component mounting with logging
    const originalMount = eleva.mount;
    eleva.mount = async function(container, compName, props) {
      eleva.log.info(`Mounting component: ${compName}`);
      const result = await originalMount.call(this, container, compName, props);
      eleva.log.info(`Component mounted: ${compName}`);
      return result;
    };
  }
};

// Usage
const app = new Eleva('myApp');
app.use(LoggerPlugin, { level: 'debug' });

Plugin Lifecycle

  1. Installation
    • Plugin is registered with the Eleva instance
    • install function is called with the instance and options
    • Plugin is stored in the internal registry
  2. Runtime
    • Plugin methods are available throughout the application lifecycle
    • Can interact with components and the Eleva instance
    • Can respond to component lifecycle events
  3. Cleanup
    • Plugins should clean up any resources they’ve created
    • Remove event listeners and subscriptions
    • Reset any modified behavior

TypeScript Support

Eleva provides TypeScript declarations for plugin development:

interface ElevaPlugin {
  name: string;
  install(eleva: Eleva, options?: Record<string, any>): void;
}

This ensures type safety when developing plugins in TypeScript.


9. Debugging & Developer Tools


10. Best Practices & Use Cases

Best Practices

Use Cases


11. Examples and Tutorials

Explore these guides for real-world examples:

Interactive demos are also available on Eleva’s CodePen Collection for you to experiment live.


12. FAQ

Q: Is Eleva production-ready? A: Eleva is currently in beta (v1.2.14-beta). While it’s stable and suitable for production use, we’re still gathering feedback before the final v1.0.0 release.

Q: How do I report issues or request features? A: Please use the GitHub Issues page.

Q: Can I use Eleva with TypeScript? A: Absolutely! Eleva includes built-in TypeScript declarations to help keep your codebase strongly typed.


13. Troubleshooting & Migration Guidelines

Troubleshooting

Migration Guidelines


14. API Reference

Detailed API documentation with parameter descriptions, return values, and usage examples can be found in the docs folder.


15. Contributing

Contributions are welcome! Whether you’re fixing bugs, adding features, or improving documentation, your input is invaluable. Please checkout the CONTRIBUTING file for detailed guidelines on how to get started.


16. Community & Support

Join our community for support, discussions, and collaboration:


17. Changelog

For a detailed log of all changes and updates, please refer to the Changelog.


18. License

Eleva is open-source and available under the MIT License.


Thank you for exploring Eleva! I hope this documentation helps you build amazing, high-performance frontend applications using pure vanilla JavaScript. For further information, interactive demos, and community support, please visit the GitHub Discussions page.