// JavaScript Templating Framework

Build with Forge

A lightweight, zero-dependency JavaScript framework with a powerful template engine, reactive state, composable components, client-side routing, and a built-in event bus. Under 6 KB minified.

Template Engine

Mustache-inspired syntax with conditionals, loops, helpers, and raw HTML output. Compiles to a fast pure-JS render function.

Reactive State

Micro-store with subscription, computed values, and batched async re-renders. No proxy magic — predictable and debuggable.

Components

Self-contained units with local data, lifecycle hooks, declarative event binding, and nested child components.

Router

Hash or History mode routing with named params, redirects, per-route titles, and a plug-and-play outlet element.

Event Bus

Global publish/subscribe for cross-component communication. on, off, emit, and once.

Zero Dependencies

Pure ES5-compatible JavaScript. Works as a UMD module (Node, AMD, browser global). No build step required.

Installation

Drop a single script tag into your page — no npm, no bundler required. Or import it as a CommonJS/AMD module.

html
<script src="forge.js"></script>
<!-- Forge is now available as window.Forge -->
js — CommonJS
const Forge = require('./forge.js');

Quick Start

Mount a component in three lines. The template re-renders automatically whenever you call setData().

javascript
const app = Forge.createComponent('#app', {
  data: { count: 0 },
  template: `
    <h2>Count: {{ count }}</h2>
    <button data-on="click:increment">+ Increment</button>
  `,
  methods: {
    increment() {
      this.setData(d => ({ count: d.count + 1 }));
    }
  }
});

Template Engine

Forge templates compile to plain JavaScript render functions. Use them standalone via Forge.render() or Forge.compile() — no DOM required.

template syntax
{# comment — stripped at compile time #}

<!-- Escaped (XSS-safe) -->
<p>Hello, {{ name }}</p>

<!-- Raw HTML -->
<div>{{{ htmlContent }}}</div>

<!-- Conditionals -->
{% if isAdmin %}
  <button>Delete</button>
{% else if isMod %}
  <button>Moderate</button>
{% else %}
  <p>Read-only</p>
{% endif %}

<!-- Loops (item.index always available) -->
<ul>
{% each users as user %}
  <li>{{ user.index + 1 }}. {{ user.name }}</li>
{% endeach %}
</ul>

<!-- Local variable -->
{% set greeting = "Hello, " + name %}
<p>{{ greeting }}</p>
js — standalone
// Render once
const html = Forge.render(
  `<ul>{% each items as i %}<li>{{ i.name }}</li>{% endeach %}</ul>`,
  { items: [{ name: 'Alpha' }, { name: 'Beta' }] }
);

// Compile once, call many times
const render = Forge.compile('<h1>{{ title }}</h1>');
document.body.innerHTML = render({ title: 'Forge' });

Components

Components encapsulate a template, local reactive data, methods, and lifecycle hooks. Bind DOM events declaratively with data-on attributes.

javascript
const UserCard = Forge.defineComponent({
  data: { likes: 0, liked: false },
  template: `
    <div class="card">
      <h3>{{ name }}</h3>
      <button data-on="click:toggleLike">
        {% if liked %}♥ Liked{% else %}♡ Like{% endif %}
        ({{ likes }})
      </button>
    </div>
  `,
  methods: {
    toggleLike() {
      this.setData(d => ({
        liked: !d.liked,
        likes: d.liked ? d.likes - 1 : d.likes + 1
      }));
    }
  },
  lifecycle: {
    mounted()   { console.log('mounted'); },
    updated()   { console.log('updated', this.data); },
    destroyed() { console.log('destroyed'); }
  }
});

Forge.createComponent('#user-card', {
  ...UserCard,
  data: { ...UserCard.data, name: 'Ada Lovelace' }
});

Event Binding

Use data-on="event:methodName" for single events, or comma-separate for multiple:

html — in template
<!-- Single event -->
<button data-on="click:save">Save</button>

<!-- Multiple events -->
<input data-on="input:handleInput,blur:validate" />

<!-- Attribute form -->
<button data-onclick="handleClick">Click</button>

Reactive Store

A simple global store for shared state. Subscribe to changes, define computed values, and inject specific keys into any component via storeKeys.

javascript
const store = Forge.createStore({ user: null, theme: 'dark' });

// Computed value
store.computed('isLoggedIn', s => s.user !== null);

// Update state
store.setState({ user: { name: 'Ada', role: 'admin' } });

// Subscribe
const unsub = store.subscribe((next, prev) => {
  console.log('Changed', prev, '→', next);
});

console.log(store.getComputed('isLoggedIn')); // true

// Connect to a component
Forge.createComponent('#nav', {
  store,
  storeKeys: ['user', 'theme'],
  template: `<p>Welcome, {{ user.name }}!</p>`
});

unsub(); // clean up

Router

Client-side routing in hash or history mode. Define routes that render HTML strings or mount full components into a #forge-outlet element.

javascript
const router = Forge.createRouter([
  {
    path: '/',
    title: 'Home',
    render: () => '<h1>Home</h1>'
  },
  {
    path: '/users/:id',
    title: 'User Profile',
    render: ({ params }) =>
      Forge.render('<h1>User: {{ id }}</h1>', params)
  },
  {
    path: '/dashboard',
    component: DashboardComponent
  },
  {
    path: '/old',
    redirect: '/new'
  }
], {
  mode: 'hash',     // 'hash' | 'history'
  outlet: '#app'
});

router.start();
router.push('/users/42');
console.log(router.params); // { id: '42' }

router.on('navigate', ({ path, params }) => {
  console.log('→', path, params);
});

Event Bus

Create a standalone event bus for cross-component messaging. All Forge components and the Router extend EventBus internally.

javascript
const bus = Forge.createBus();

const off = bus.on('user:login', (user) => {
  console.log('Logged in:', user.name);
});

bus.once('app:ready', () => console.log('Ready!'));
bus.emit('user:login', { name: 'Ada' });
bus.emit('app:ready');
off(); // unsubscribe

Counter

A reactive counter built with a Forge component — running live on this page.

Counter Component

javascript
Forge.createComponent('#demo-counter', {
  data: { count: 0 },
  template: `
    <div class="demo-counter">
      <button class="demo-btn" data-on="click:dec">−</button>
      <span class="demo-count-val">{{ count }}</span>
      <button class="demo-btn" data-on="click:inc">+</button>
    </div>
  `,
  methods: {
    inc() { this.setData(d => ({ count: d.count + 1 })); },
    dec() { this.setData(d => ({ count: d.count - 1 })); }
  }
});

Todo List

A fully reactive todo list with add, complete, and remove — running live below.

Todo Component

javascript
Forge.createComponent('#demo-todo', {
  data: {
    input: '',
    items: [{ text: 'Try Forge.js', done: false }]
  },
  template: `
    <div class="todo-input-row">
      <input class="todo-input" data-on="input:onInput"
             placeholder="Add a task…" />
      <button class="todo-add-btn" data-on="click:addItem">Add</button>
    </div>
    <ul class="todo-list" role="list">
    {% each items as item %}
      <li class="todo-item">
        <input type="checkbox" {% if item.done %}checked{% endif %}
               data-on="click:toggle" data-idx="{{ item.index }}" />
        <span class="{{ item.done ? 'done' : '' }}">
          {{ item.text }}
        </span>
        <button class="todo-remove" data-on="click:remove"
                data-idx="{{ item.index }}" aria-label="Remove">✕</button>
      </li>
    {% endeach %}
    </ul>
  `,
  methods: { /* ... */ }
});

Full API Reference

Forge (top-level)

MethodReturnsDescription
Forge.createStore(state)StoreCreate a reactive global store.
Forge.createComponent(target, opts)ComponentDefine and immediately mount a component.
Forge.defineComponent(opts)objectDefine a reusable component blueprint (not mounted).
Forge.createRouter(routes, opts)RouterCreate a client-side router.
Forge.render(template, data)stringRender a template string with data.
Forge.compile(template)FunctionCompile a template to a reusable render function.
Forge.createBus()EventBusCreate a standalone event bus.

Store

Method / PropertyDescription
store.stateRead a deep-cloned snapshot of current state.
store.setState(patch|fn)Merge patch object or result of updater function into state.
store.replaceState(state)Replace entire state.
store.subscribe(fn)Subscribe to state changes. Returns unsubscribe function.
store.computed(name, fn)Define a computed value derived from state.
store.getComputed(name)Evaluate and return a computed value.

Component

Option / MethodDescription
dataInitial local state object.
templateForge template string.
methodsObject of methods. this = component instance.
componentsChild component definitions keyed by name.
store / storeKeysConnect global store; inject listed keys into template.
lifecycle.createdCalled before first render.
lifecycle.mountedCalled after first render.
lifecycle.updatedCalled after every re-render.
lifecycle.destroyedCalled when component is destroyed.
comp.setData(patch|fn)Update local data and schedule a re-render.
comp.dataDeep-cloned snapshot of local data.
comp.elThe mounted DOM element.
comp.destroy()Unmount, clean up subscriptions, destroy children.

Router

Option / MethodDescription
mode'hash' (default) or 'history'.
outletCSS selector for the router outlet. Default: #forge-outlet.
router.start()Begin listening to navigation events and render current route.
router.push(path)Navigate to path (adds history entry).
router.replace(path)Navigate to path (replaces history entry).
router.paramsNamed parameters of the active route.
router.on('navigate', fn)Listen for route changes.
router.on('not-found', fn)Listen for unmatched paths.