Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

462 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

LadrillosJS

LadrillosJS

A lightweight, zero-dependency web component framework.

npm version license bundle size GitHub stars

Build modular web apps with simple HTML components. No virtual DOM, no complex tooling required.


πŸ“‘ Table of Contents


πŸš€ Quick Start

1. Create a Component

Save this as counter.html:

<div class="counter">
  <div class="count-display">{count}</div>
  <div class="buttons">
    <button onclick="count--">βˆ’</button>
    <button onclick="count = 0">Reset</button>
    <button onclick="count++">+</button>
  </div>
  <p>Double: {count * 2} | Squared: {count * count}</p>
</div>

<script>
  let count = 0;
</script>

<style>
  .counter {
    text-align: center;
    padding: 2rem;
  }
  .count-display {
    font-size: 4rem;
    font-weight: bold;
  }
  button {
    padding: 0.5rem 1rem;
    margin: 0.25rem;
    cursor: pointer;
  }
</style>

2. Register and Use It

In your index.html, import LadrillosJS from the CDN, register the component, and drop the tag anywhere on the page:

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import { registerComponent } from "https://cdn.jsdelivr.net/npm/ladrillosjs@2/dist/index.js";
      registerComponent("my-counter", "./counter.html");
    </script>
  </head>
  <body>
    <my-counter></my-counter>
  </body>
</html>

3. Serve It

Components are fetched over HTTP, so serve the folder with any static server (opening the file directly via file:// won't work β€” browsers block fetch from local files):

npx serve          # or: python -m http.server 8080

That's it! Your reactive component is ready. πŸŽ‰

🧱 Starting a real project? Copy a template instead: templates/cdn-starter (no build step) or templates/vite-starter (Vite). Both come with a page shell, CSP, error reporting, and two example components.

πŸ“š Want more? The full documentation covers every feature step by step, from your first component to building a design system.


πŸ“¦ Installation

CDN (No Build Step)

LadrillosJS v2 is distributed as native ES modules. Import directly from a CDN:

<!-- ES Module (recommended) -->
<script type="module">
  import {
    registerComponent,
    registerComponents,
  } from "https://cdn.jsdelivr.net/npm/ladrillosjs@2/dist/index.js";

  registerComponent("my-component", "./component.html");
</script>

<!-- Also available on unpkg -->
<script type="module">
  import { registerComponent } from "https://unpkg.com/ladrillosjs@2/dist/index.js";
</script>

Note: LadrillosJS v2 is ESM-only. Legacy UMD/IIFE global builds are not published to npm.

NPM (With Build Tools)

npm install ladrillosjs
import { registerComponent, registerComponents } from "ladrillosjs";

// Single component
registerComponent("my-counter", "./components/counter.html");

// Multiple components
await registerComponents([
  { name: "app-header", path: "./components/header.html" },
  { name: "app-footer", path: "./components/footer.html" },
]);

Granular Imports (Tree-Shaking)

// Full API
import { registerComponent, $emit, $listen } from "ladrillosjs";

// Core only (no lazy loading, no event bus)
import { registerComponent } from "ladrillosjs/core";

// Lazy loading strategies only
import { lazyOnVisible, lazyOnIdle } from "ladrillosjs/lazy";

// Event bus only
import { $emit, $listen } from "ladrillosjs/events";

πŸ“– Core Concepts

Template Bindings

Use {expression} to display reactive data. Any JavaScript expression works:

<h1>{title}</h1>
<p>Hello, {user.name}!</p>
<span>Total: {items.length} items</span>
<p>Is adult: {age >= 18 ? 'Yes' : 'No'}</p>

Reactive State

Just declare variables with let β€” changes automatically update the DOM:

<script>
  let count = 0;
  let user = { name: "Alice", role: "Developer" };
  let items = ["Apple", "Banana", "Cherry"];
</script>

Event Handlers

Attach events directly in HTML with inline expressions or function calls:

<button onclick="count++">Increment</button>
<button onclick="handleClick()">Click me</button>
<input onkeyup="search(event.target.value)" />
<form onsubmit="handleSubmit(event)">...</form>

Or use the $on: directive with modifiers β€” dot-separated flags for common patterns like preventDefault, key filtering, and modifier keys:

<form $on:submit.prevent="handleSubmit()">...</form>
<input $on:keyup.enter="search()" />
<textarea $on:keydown.ctrl.s.prevent="save()"></textarea>
<div $on:click.self="closeModal()">...</div>
<button $on:click.once="trackFirstClick()">Buy</button>

See the Event Modifiers reference for every modifier and combination.

Two-Way Binding

Use $bind to sync form inputs with state:

<input type="text" $bind="username" placeholder="Enter name" />
<p>Hello, {username}!</p>

<textarea $bind="bio"></textarea>

<select $bind="country">
  <option value="us">United States</option>
  <option value="uk">United Kingdom</option>
</select>

<script>
  let username = "";
  let bio = "";
  let country = "us";
</script>

🧩 Built-in Elements & Directives

Conditional Rendering

Use <if>, <else-if>, and <else> to conditionally render elements:

<if condition="status === 'loading'">Loading...</if>
<else-if condition="status === 'error'">Something went wrong!</else-if>
<else>Content loaded successfully!</else>

<script>
  let status = "loading";
</script>

Show/Hide (CSS Toggle)

Use <show> to toggle visibility without removing from DOM (uses display: none):

<show condition="isVisible">I can be shown or hidden</show>

<button onclick="isVisible = !isVisible">Toggle</button>

<script>
  let isVisible = true;
</script>

<show> vs <if>: <show> toggles CSS display (children stay in DOM), <if> adds/removes children entirely.

List Rendering

Use <for> to render lists with optional index and key:

<!-- Simple list -->
<ul>
  <for each="fruit in fruits">
    <li>🍎 {fruit}</li>
  </for>
</ul>

<!-- With index -->
<for each="(item, index) in items">
  <div>#{index + 1}: {item}</div>
</for>

<!-- Object array with key -->
<for each="user in users" key="user.id">
  <div>
    <span>{user.avatar}</span>
    <span>{user.name}</span>
    <span>{user.role}</span>
  </div>
</for>

<script>
  let fruits = ["Apple", "Banana", "Cherry"];
  let items = ["First", "Second", "Third"];
  let users = [
    { id: 1, name: "Alice", role: "Developer", avatar: "πŸ‘©β€πŸ’»" },
    { id: 2, name: "Bob", role: "Designer", avatar: "πŸ‘¨β€πŸŽ¨" },
  ];
</script>

Lazy Loading

Use <lazy> to defer rendering until a trigger fires (viewport, idle, delay, interaction, media):

<lazy margin="100px">
  <heavy-chart></heavy-chart>
</lazy>

<lazy interaction="click,focus">
  <support-chat></support-chat>
</lazy>

Cheat Sheet

Element / Directive Purpose Example
<if> Conditional render <if condition="isLoggedIn">Welcome!</if>
<else-if> Chained condition <else-if condition="isGuest">Hello Guest</else-if>
<else> Fallback <else>Please log in</else>
<show> CSS visibility toggle <show condition="isOpen">Menu</show>
<for> Loop rendering <for each="item in items"><li>{item}</li></for>
<for> (indexed) Loop with index <for each="(item, i) in items">…</for>
<for key="…"> List optimization <for each="u in users" key="u.id">…</for>
<lazy> Defer rendering <lazy idle><analytics-pixel /></lazy>
$bind Two-way binding <input $bind="email" />
$ref Element reference <input $ref="inputEl" />
$on: + modifiers Events with modifiers <form $on:submit.prevent="save()">
$no:bind Escape {} binding <code $no:bind>{literal}</code>

πŸ“‘ Event Bus

Communicate between components using $emit and $listen:

Sender Component

<button onclick="sendMessage()">Send Message</button>

<script>
  let message = "Hello from sender!";

  function sendMessage() {
    $emit("my-event", { text: message, time: new Date().toLocaleTimeString() });
  }
</script>

Receiver Component

<div>
  <p>Received: {receivedMessage}</p>
</div>

<script>
  let receivedMessage = "Waiting...";

  $listen("my-event", (data) => {
    receivedMessage = data.text;
  });
</script>

🏷️ Element References

Use $ref to get direct DOM access for advanced manipulation:

<input type="text" $ref="inputEl" placeholder="Click button to focus" />
<button onclick="focusInput()">Focus Input</button>

<canvas $ref="canvas" width="200" height="100"></canvas>
<button onclick="draw()">Draw on Canvas</button>

<script>
  function focusInput() {
    $refs.inputEl.focus();
    $refs.inputEl.select();
  }

  function draw() {
    const ctx = $refs.canvas.getContext("2d");
    ctx.fillStyle = "blue";
    ctx.fillRect(10, 10, 100, 50);
  }
</script>

⏳ Lazy Loading

Load components only when needed to improve initial page load:

Lazy Loading Strategies

import {
  registerComponents,
  lazyOnVisible,
  lazyOnIdle,
  lazyOnInteraction,
  lazyOnMedia,
  lazyOnDelay,
} from "ladrillosjs";

await registerComponents([
  // Load when visible in viewport
  {
    name: "lazy-footer",
    path: "./footer.html",
    lazy: lazyOnVisible({ rootMargin: "100px" }),
  },

  // Load when browser is idle
  {
    name: "analytics-widget",
    path: "./analytics.html",
    lazy: lazyOnIdle(5000), // timeout: 5s max wait
  },

  // Load on user interaction
  {
    name: "modal-dialog",
    path: "./modal.html",
    lazy: lazyOnInteraction(["click", "focusin"]),
  },

  // Load based on media query
  {
    name: "mobile-nav",
    path: "./mobile-nav.html",
    lazy: lazyOnMedia("(max-width: 768px)"),
  },

  // Load after delay
  {
    name: "chat-widget",
    path: "./chat.html",
    lazy: lazyOnDelay(3000), // 3 second delay
  },
]);
Strategy Use Case
lazyOnVisible Below-fold content, footers, image galleries
lazyOnIdle Non-critical features, analytics
lazyOnInteraction Modals, dropdowns, tooltips
lazyOnMedia Mobile/desktop specific components
lazyOnDelay Chat widgets, notifications

Eager Override

Force a lazy component to load immediately by adding the eager attribute:

<lazy-footer eager></lazy-footer>

πŸ“‹ API Reference

registerComponent

registerComponent(name, path, useShadowDOM?, lazy?)
Parameter Type Default Description
name string required Tag name (must include hyphen)
path string required Path to .html component file
useShadowDOM boolean true Enable Shadow DOM encapsulation
lazy boolean | LazyStrategy false Lazy loading configuration
// Basic usage
registerComponent("my-button", "./button.html");

// Without Shadow DOM (for global CSS)
registerComponent("my-nav", "./nav.html", false);

// With lazy loading
registerComponent("my-footer", "./footer.html", true, lazyOnVisible());

registerComponents

Register multiple components with parallel fetching:

const result = await registerComponents([
  { name: "app-header", path: "./header.html" },
  { name: "app-footer", path: "./footer.html", lazy: lazyOnVisible() },
  { name: "user-card", path: "./user-card.html", useShadowDOM: false },
]);

// Returns: { success: [...], failed: [...], skipped: [...] }

$use

Infer the component tag name from the file path:

await $use("./components/user-card.html"); // Registers as <user-card>

loadLazyComponent

Force a lazy component to load immediately from JavaScript:

import { loadLazyComponent } from "ladrillosjs";

await loadLazyComponent("my-lazy-footer");

configure

Configure framework-level options (optional):

import { configure } from "ladrillosjs";

configure({
  cacheSize: 50, // Component LRU cache size (default: 25)
  onError: (err, context) => telemetry.capture(err, { context }),
  delegateLoopEvents: true, // Opt-in loop event delegation (default: false)
});

Development builds include actionable LJSxxx diagnostics with component and file context, a suggested fix, and a link to the error reference. Vite and other bundlers that honor the development package condition select this build automatically. To select it explicitly, import from ladrillosjs/dev (or ladrillosjs/core/dev). Production builds retain coded errors but remove development-only warnings.

The onError callback is also suitable for telemetry. Coded framework errors are instances of LadrillosError and expose code, docsUrl, hint, componentContext, and the original cause.

delegateLoopEvents

By default every handler inside a <for> row gets its own listener. With delegateLoopEvents: true, eligible handlers share one listener per event type on the loop's container β€” thousands of rows, a handful of listeners. Templates and handler code don't change at all, and render performance is the same either way; enable it when listener count itself matters (very large lists, memory-constrained devices, many handlers per row).

Fine print:

  • Non-bubbling events (focus, blur, mouseenter, …) and handlers using the .self, .capture, .once, or .passive modifiers automatically keep per-element listeners. .stop, .prevent, and key/system modifiers work identically in both modes.
  • event.currentTarget inside a delegated handler is the list container, not the row element (event.target is unaffected).
  • If you manually attach your own listener on an element inside a row and call stopPropagation(), delegated handlers above it won't fire.
  • Call configure() before your components render; already-rendered loops keep the mode they were created with.

Event Bus

Function Description
$emit(event, data) Broadcast an event to all listeners
$listen(event, callback) Subscribe to an event

πŸ› οΈ Using with Vite

LadrillosJS works seamlessly with Vite for production builds:

npm install ladrillosjs vite
// vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
  // LadrillosJS components work out of the box!
});
// main.js
import { registerComponent } from "ladrillosjs";

registerComponent("my-counter", "./components/counter.html", false);

See the samples/ directory for complete examples:

  • samples/vite-sample/ β€” Basic Vite setup
  • samples/vite-basic-site/ β€” Multi-component site
  • samples/ladrillos-demo/ β€” Full feature showcase

πŸ“Š Benchmarks

A js-framework-benchmark–style suite lives in benchmarks/. It renders the same keyed 1,000-row list in LadrillosJS, React 18 (keyed, memoized rows β€” the idiomatic fast path), and hand-optimized vanilla JS, and measures each operation from invocation until the live DOM reflects the change. Medians of 10 runs, headless Chromium on an Apple M5 Pro, ladrillosjs@2.0.0-beta.11:

Operation LadrillosJS React 18.3 (keyed, memoized rows) Vanilla JS (hand-optimized)
create 1,000 rows 3.2 ms 4.2 ms 1.7 ms
replace all 1,000 rows 3.8 ms 6.3 ms 2 ms
partial update (every 10th of 1,000) 1 ms 1.2 ms 0.1 ms
select row 0.6 ms 0.3 ms 0 ms
swap 2 rows 0.9 ms 3 ms 0 ms
remove row 0.8 ms 0.9 ms 0.1 ms
append 1,000 to 1,000 rows 3.6 ms 3.9 ms 1.2 ms
clear 1,000 rows 1.1 ms 4 ms 0.8 ms
create 10,000 rows 29.1 ms 228.3 ms 11.3 ms
JS payload (min+gzip) 27.7 KB 47 KB ~1 KB
JS heap after 1,000 rows 2.4 MB 6.2 MB 1.3 MB

How to read this honestly:

  • Every operation lands well within a single 60 fps frame (16.7 ms): updates on a 1,000-row list take about a millisecond, and bulk creation of 1,000 rows is under 5 ms.
  • LadrillosJS beats React on bulk creation (and by nearly 8Γ— on 10,000 rows), full replaces, row swaps, and clearing, while shipping ~40% less JS and using ~60% less memory for the same UI.
  • React 18 remains faster on single-row selection, and partial updates are effectively a tie. We publish these numbers to track and improve them β€” not to claim LadrillosJS wins everything.
  • Vanilla JS is the baseline.

Reproduce it yourself (no benchmark numbers should be trusted otherwise):

cd benchmarks && npm install && npm run bench

Methodology, fairness notes, and raw samples: benchmarks/README.md and benchmarks/results.json.


πŸ“š Examples

Todo List

A complete CRUD example combining all directives:

<div class="todo-app">
  <form onsubmit="addTodo(event)">
    <input type="text" $bind="newTodo" placeholder="What needs to be done?" />
    <button type="submit">Add</button>
  </form>

  <ul>
    <for each="todo in todos" key="todo.id">
      <li>
        <input type="checkbox" onclick="toggleTodo(todo.id)" />
        <span class="{todo.completed ? 'done' : ''}">{todo.text}</span>
        <button onclick="removeTodo(todo.id)">πŸ—‘οΈ</button>
      </li>
    </for>
  </ul>

  <if condition="todos.length === 0"><p>No todos yet!</p></if>
</div>

<script>
  let todos = [
    { id: 1, text: "Learn LadrillosJS", completed: false },
    { id: 2, text: "Build something awesome", completed: false },
  ];
  let newTodo = "";
  let nextId = 3;

  function addTodo(event) {
    event.preventDefault();
    if (newTodo.trim()) {
      todos = [...todos, { id: nextId++, text: newTodo, completed: false }];
      newTodo = "";
    }
  }

  function toggleTodo(id) {
    todos = todos.map((t) =>
      t.id === id ? { ...t, completed: !t.completed } : t,
    );
  }

  function removeTodo(id) {
    todos = todos.filter((t) => t.id !== id);
  }
</script>

πŸ’‘ Check the samples/ folder for more examples including lazy loading, event bus communication, and real-world patterns.


πŸ“š Documentation

Full guides live in the docs/ folder:

Start here Reference Advanced
Quick Start Built-in Elements & Directives Event Bus
Installation Event Modifiers ($on:) Lazy Loading
Components Conditionals & Loops Shadow DOM
Reactivity Two-Way Binding Design System Guide

Upgrading from v1? See the Migration Guide. Using TypeScript? See the TypeScript guide.


πŸ”’ Security & Trust Model

Component HTML is trusted code. Treat a .html component the same way you'd treat a .js file you import.

LadrillosJS runs the <script> in each component, evaluates your {expression} bindings, and compiles your inline event handlers as JavaScript with full access to the page (window, document, fetch, localStorage, …). This is by design β€” it's what lets components feel like plain HTML + JS with no build step, exactly like the template in a Vue single-file component or a Svelte .svelte file. It also means the framework is not a sandbox: you should only register and run component files that you wrote or otherwise trust.

Practical guidance:

  • βœ… Do author your own components and load them from your own origin.
  • ❌ Don't fetch and register component HTML from untrusted third parties, or build component files by concatenating user input into a template β€” that's equivalent to running arbitrary code from that source.
  • βœ… Dynamic data is fine. Rendering untrusted data β€” API responses, user comments, list items β€” through bindings and <for> loops is safe: values are set via textContent/setAttribute and passed to event handlers as arguments, never executed as code. (Interpolating into innerHTML yourself, or into a <script>/handler you assemble by hand, is not β€” same rule as above.)

In short: untrusted data through LadrillosJS's bindings is safe; untrusted templates/scripts are not, because those are your application code.

Content Security Policy

By default components are compiled in the browser (that's what "no build step" costs), so a page using LadrillosJS needs 'unsafe-eval':

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-eval';
  style-src 'self';
  connect-src 'self';

Notably, 'unsafe-inline' for scripts is not needed β€” inline onclick attributes are removed and reattached via addEventListener() β€” and neither is blob:. 'unsafe-inline' for styles is not needed either: component CSS is adopted as a constructed stylesheet rather than injected as <style>. require-trusted-types-for 'script' is supported on the build below.

If you use Vite, you can drop 'unsafe-eval'. Add @ladrillosjs/vite-plugin and import from ladrillosjs/csp; components are compiled at build time instead, with no change to your components or your code:

// vite.config.js
import ladrillos from "@ladrillosjs/vite-plugin";

export default { plugins: [ladrillos()] };

See Content Security Policy for the full breakdown, both policies, and the verification results.


🀝 Contributing

Contributions are welcome! Here's how you can help:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Commit your changes: git commit -m 'Add amazing feature'
  4. Push to the branch: git push origin feature/amazing-feature
  5. Open a Pull Request

Development Setup

git clone https://github.com/drubiodev/LadrillosJS.git
cd LadrillosJS
npm install
npm run dev        # Watch mode for development
npm run build      # Build for production
npm test           # Unit tests (Vitest)

πŸ“„ License

MIT License β€” see LICENSE for details.


Built with ❀️ by Daniel Rubio

GitHub β€’ NPM β€’ Issues

About

A lightweight, zero-dependency web component framework for building modular web applications.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages