// 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.
Mustache-inspired syntax with conditionals, loops, helpers, and raw HTML output. Compiles to a fast pure-JS render function.
Micro-store with subscription, computed values, and batched async re-renders. No proxy magic — predictable and debuggable.
Self-contained units with local data, lifecycle hooks, declarative event binding, and nested child components.
Hash or History mode routing with named params, redirects, per-route titles, and a plug-and-play outlet element.
Global publish/subscribe for cross-component communication. on, off, emit, and once.
Pure ES5-compatible JavaScript. Works as a UMD module (Node, AMD, browser global). No build step required.
// 01
Installation
Drop a single script tag into your page — no npm, no bundler required. Or import it as a CommonJS/AMD module.
<script src="forge.js"></script> <!-- Forge is now available as window.Forge -->
const Forge = require('./forge.js');
// 02
Quick Start
Mount a component in three lines. The template re-renders automatically whenever you call setData().
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 })); } } });
// 03
Template Engine
Forge templates compile to plain JavaScript render functions. Use them standalone via
Forge.render() or Forge.compile() — no DOM required.
{# 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>
// 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' });
// 04
Components
Components encapsulate a template, local reactive data, methods, and lifecycle hooks.
Bind DOM events declaratively with data-on attributes.
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:
<!-- 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>
// 05
Reactive Store
A simple global store for shared state. Subscribe to changes, define computed values, and
inject specific keys into any component via storeKeys.
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
// 06
Router
Client-side routing in hash or history mode. Define routes that render HTML strings or
mount full components into a #forge-outlet element.
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); });
// 07
Event Bus
Create a standalone event bus for cross-component messaging. All Forge components and the Router extend EventBus internally.
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
// Live Demo
Counter
A reactive counter built with a Forge component — running live on this page.
Counter Component
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 })); } } });
// Live Demo
Todo List
A fully reactive todo list with add, complete, and remove — running live below.
Todo Component
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: { /* ... */ } });
// 08
Full API Reference
Forge (top-level)
| Method | Returns | Description |
|---|---|---|
Forge.createStore(state) | Store | Create a reactive global store. |
Forge.createComponent(target, opts) | Component | Define and immediately mount a component. |
Forge.defineComponent(opts) | object | Define a reusable component blueprint (not mounted). |
Forge.createRouter(routes, opts) | Router | Create a client-side router. |
Forge.render(template, data) | string | Render a template string with data. |
Forge.compile(template) | Function | Compile a template to a reusable render function. |
Forge.createBus() | EventBus | Create a standalone event bus. |
Store
| Method / Property | Description |
|---|---|
store.state | Read 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 / Method | Description |
|---|---|
data | Initial local state object. |
template | Forge template string. |
methods | Object of methods. this = component instance. |
components | Child component definitions keyed by name. |
store / storeKeys | Connect global store; inject listed keys into template. |
lifecycle.created | Called before first render. |
lifecycle.mounted | Called after first render. |
lifecycle.updated | Called after every re-render. |
lifecycle.destroyed | Called when component is destroyed. |
comp.setData(patch|fn) | Update local data and schedule a re-render. |
comp.data | Deep-cloned snapshot of local data. |
comp.el | The mounted DOM element. |
comp.destroy() | Unmount, clean up subscriptions, destroy children. |
Router
| Option / Method | Description |
|---|---|
mode | 'hash' (default) or 'history'. |
outlet | CSS 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.params | Named parameters of the active route. |
router.on('navigate', fn) | Listen for route changes. |
router.on('not-found', fn) | Listen for unmatched paths. |