01. Obtaining vertex.js
Vertex is a 1kloc SPA framework containing everything you need from React, Ractive-Load and jQuery while still being jQuery-compatible.
vertex.js is a single, self-contained file with no build step and no dependencies. Download it from the Gist below and drop it wherever your project serves static assets.
Or fetch it from the command line:
# save to your project's static directory
curl -o static/vertex.js \
https://gist.githubusercontent.com/LukeB42/ef5b142325fc2bcd4915ba9b452f6230/raw/867cf609e8ec6bcb6700eda7f81c7c60e9ee01c9/vertex.js
It ships as a UMD module, so it works equally as a plain
<script> tag, a CommonJS require(),
or an AMD define().
02. Loading in the <head>
Add a single <script> tag. Place it before any
code that references Vertex or V$.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>My App</title> <script src="/static/vertex.js"></script> <!-- If you also use jQuery, load it BEFORE vertex.js. vertex.js detects $ and leaves it untouched. --> <!-- <script src="/static/jquery.min.js"></script> --> </head> <body> <div id="root"></div> </body> </html>
After loading, the following globals are available:
| Global | Description |
|---|---|
Vertex |
Full namespace — all features live here |
V$ |
Shorthand DOM wrapper — always available |
$ |
Also set to the DOM wrapper only when jQuery is absent |
03. Setting a templates directory
Set Vertex.template.load.baseUri once at startup and every
subsequent load() call that receives a relative path will
automatically prepend it. Absolute URLs (starting with
http://, https://, or /) are
always used as-is, so fully-qualified paths continue to work unchanged.
// main.js — set the base once, then use short names everywhere Vertex.template.load.baseUri = "/static/templates/"; // "user-card" resolves to /static/templates/user-card Vertex.template.load("user-card", { el: "#sidebar", data: { name: "Alice", role: "Engineer" } }).then(instance => { instance.on("change", e => console.log("changed:", e)); }); // Absolute paths bypass baseUri entirely Vertex.template.load("/other/path/special.html", { el: "#special" }); Vertex.template.load("https://cdn.example.com/tmpl.html", { el: "#remote" });
A template file at /static/templates/user-card.html
is just a regular HTML fragment wrapped in a
<template> tag:
<!-- /static/templates/user-card.html --> <template> <div class="card"> <h2>{{name}}</h2> <p>{{role}}</p> {{#if email}}<a href="mailto:{{email}}">{{email}}</a>{{/if}} </div> </template>
Note: If the file has no <template>
tag, Vertex.template.load() uses the entire response body as
the template string. Both forms work.
04. DOM layer — V$ / VQuery
V$(selector) returns a chainable wrapper around a set of
matched elements — identical in spirit to hn.js with a fuller jQuery
surface. Every method returns this for chaining.
Selecting & creating elements
// CSS selector V$(".card").addClass("active"); // HTML creation const el = V$('<li class="item">Hello</li>'); // Scoped query (2nd arg = context) V$("li", "#my-list").each(function() { console.log(this.textContent); }); // Document ready V$(function() { console.log("DOM ready"); });
Events — .on() / .off() / .trigger()
// Direct event binding V$("button").on("click", function(e) { V$(this).toggleClass("pressed"); }); // Multiple events at once V$("input").on("focus blur", function() { V$(this).toggleClass("active"); }); // Event delegation (bubbles up from ".row" to "#table") V$("#table").on("click", ".row", function(e) { console.log("row clicked:", this.dataset.id); }); // Remove handler const handler = e => doSomething(e); V$("#btn").on("click", handler); V$("#btn").off("click", handler); // Custom event dispatch V$("#root").trigger("app:ready", { version: "1.0" });
Attributes, styles & values
// .attr(name) → get // .attr(name, val) → set (chainable) // .css(prop) → get computed value // .css(prop, val) → set style property // .css({ prop: val }) → set multiple // .val() → get input value // .val(v) → set input value V$("img") .attr("alt", "A scenic photo") .css({ borderRadius: "4px", opacity: "0.9" }); const username = V$("#name-input").val(); V$("#name-input").val("").attr("placeholder", "Enter name…");
Content & traversal
// Content V$("#output").html("<strong>Done.</strong>"); V$("#label").text("Status: OK"); V$("ul").append("<li>New item</li>"); V$("ul").prepend("<li>First item</li>"); // Traversal V$(".panel").find("input").val(""); // clear all inputs inside .panel V$("li.active").parent().addClass("has-active"); V$("li").first().addClass("leader"); V$("li").eq(2).remove(); V$("li").filter(function(el, i) { return i % 2 === 0; }).addClass("even"); V$(".item").not(".disabled").on("click", handleClick);
05. AJAX
Vertex.ajax() wraps the Fetch API with a jQuery-shaped
surface: success/error callbacks, dataType, content-type handling,
and .done()/.fail() on the returned Promise.
// Full options form Vertex.ajax({ url: "/api/tracks", method: "GET", data: { genre: "bass", limit: 20 }, dataType: "json", success: tracks => renderTracks(tracks), error: err => console.error(err) }); // POST with JSON body Vertex.ajax({ url: "/api/session", method: "POST", contentType: "application/json", data: { token: myToken }, success: session => startSession(session) }); // Promise style Vertex.ajax({ url: "/api/ping" }) .done(res => console.log("ok", res)) .fail(err => console.warn("failed", err)); // Shorthand GET / POST Vertex.get("/api/user", data => console.log(data)); Vertex.post("/api/save", { title: "Mix A" }, res => console.log(res));
06. Fiber reconciler — createElement & render
Vertex's reconciler follows the same fiber architecture described at pomb.us. The public API is intentionally React-compatible.
createElement
const { createElement: h, render, Fragment } = Vertex; // Host element h("div", { className: "card" }, h("h2", null, "Hello"), h("p", null, "World") ) // Function component function Badge({ label, colour }) { return h("span", { style: { background: colour } }, label); } h(Badge, { label: "Bass", colour: "#c8ff00" }) // Fragment — renders children with no wrapper element h(Fragment, null, h("dt", null, "BPM"), h("dd", null, "174") )
render
// Mount your root component once — Vertex handles all subsequent updates function App() { return h("div", { className: "app" }, h("h1", null, "Vertex") ); } Vertex.render( h(App, null), document.getElementById("root") );
Lazy / async components
// Vertex.lazy() follows the React.lazy Suspense protocol. // The component is fetched once; Vertex re-renders automatically. const HeavyChart = Vertex.lazy(() => import("/static/js/chart.js")); function Dashboard() { return h(HeavyChart, { data: chartData }); }
07. Hooks
All hooks follow React's rules: call them only at the top level of a function component, never inside conditionals or loops.
useState
function Counter() { const [count, setCount] = Vertex.useState(0); return h("div", null, h("span", null, String(count)), h("button", { onClick: () => setCount(c => c + 1) }, "+"), h("button", { onClick: () => setCount(0) }, "reset") ); }
useReducer
function reducer(state, action) { switch (action.type) { case "add": return { ...state, items: [...state.items, action.item] }; case "clear": return { ...state, items: [] }; default: return state; } } function Playlist() { const [state, dispatch] = Vertex.useReducer(reducer, { items: [] }); return h("ul", null, ...state.items.map(t => h("li", { key: t.id }, t.title)) ); }
useEffect
function AudioPlayer({ src }) { const audioRef = Vertex.useRef(null); // Run once on mount — teardown on unmount Vertex.useEffect(function() { const ctx = new AudioContext(); audioRef.current = ctx; return () => ctx.close(); // cleanup }, []); // Re-run when src changes Vertex.useEffect(function() { if (audioRef.current) loadTrack(audioRef.current, src); }, [src]); return h("div", { className: "player" }, "Playing: " + src); }
useMemo & useCallback
function TrackList({ tracks, filter }) { // Only re-computed when tracks or filter changes const filtered = Vertex.useMemo( () => tracks.filter(t => t.genre === filter), [tracks, filter] ); // Stable function reference — safe to pass to child components const handleClick = Vertex.useCallback( t => console.log("selected:", t.title), [] ); return h("ul", null, ...filtered.map(t => h("li", { onClick: () => handleClick(t) }, t.title) ) ); }
useRef
function FocusInput() { const inputRef = Vertex.useRef(null); Vertex.useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []); return h("input", { ref: inputRef, // Vertex will write the DOM node here placeholder: "Type…" }); }
createContext & useContext
const ThemeCtx = Vertex.createContext("dark"); function App() { return h(ThemeCtx.Provider, { value: "dark" }, h(Toolbar, null) ); } function Toolbar() { const theme = Vertex.useContext(ThemeCtx); return h("nav", { className: "toolbar theme-" + theme }); }
08. Template engine — Vertex.template / Mustache
The Vertex.template constructor takes an element target, a
mustache template string, and a data object. It renders immediately
and re-renders on every .set() or .update().
Basic usage
const r = new Vertex.template({ el: "#app", template: ` <h1>{{title}}</h1> <ul> {{#each tracks}} <li>{{@index}}. {{name}} — {{bpm}} BPM</li> {{/each}} </ul> `, data: { title: "My Set", tracks: [ { name: "Vortex", bpm: 174 }, { name: "Subsonic", bpm: 140 }, ] } }); // Update a single key — triggers re-render r.set("title", "Night Set"); // Merge multiple keys at once r.update({ title: "Morning Set", tracks: [] }); // Listen for data changes r.on("change", ({ keypath, value }) => { console.log(keypath, "→", value); });
Mustache syntax reference
| Syntax | Behaviour |
|---|---|
{{key}} |
HTML-escaped interpolation |
{{{key}}} |
Raw / unescaped HTML |
{{user.name}} |
Nested dot-path resolution |
{{#each items}} … {{/each}} |
Loop — keys from each item available directly; @index for position |
{{#if flag}} … {{/if}} |
Conditional block |
{{#if flag}} … {{else}} … {{/if}} |
Conditional with fallback |
Two-way binding
Add data-bind="keypath" to any <input>
inside a template and Vertex will keep the input and the data
object in sync automatically:
new Vertex.template({ el: "#form", template: '<input data-bind="username" placeholder="Username">', data: { username: "" }, oncomplete() { console.log("mounted, username =", this.get("username")); } });
Vertex.template.load() — remote templates
// Set base once at startup Vertex.template.load.baseUri = "/static/templates/"; // Short name resolves to /static/templates/player.html Vertex.template.load("player", { el: "#player-container", data: { track: "Vortex.wav", playing: false } }).then(instance => { instance.on("change", e => syncBackend(e)); });
Want to understand how to safely mix Moustache templates and React components? Read the interop guide.
09. Hash router — Backbone style
Vertex.Router is a singleton. Routes are matched against
the URL fragment (#/…) using named parameters
(:name) and splats (*rest).
Singleton API
const { Router } = Vertex; Router .add("", params => showHome()) .add("projects", params => showProjects()) .add("projects/:id", params => showProject(params.id)) .add("files/*path", params => showFile(params.path)) .start(); // begins listening; dispatches current fragment // Navigate programmatically Router.navigate("projects/42"); // sets #/projects/42 Router.navigate("projects/42", { trigger: true }); // + fire handler // Remove a route Router.remove("files/*path"); // Stop / reset Router.stop(); Router.reset();
Class-based router (Backbone.Router syntax)
const AppRouter = Vertex.RouterClass.extend({ routes: { "": "home", "projects": "projects", "projects/:id": "project", "files/*path": "file" }, home() { console.log("home"); }, projects() { console.log("projects"); }, project({ id }) { console.log("project", id); }, file({ path }) { console.log("file", path); } }); const router = new AppRouter(); Vertex.Router.start();
10. useHash — router meets reconciler
useHash() is a hook that returns the current
location.hash fragment and automatically re-renders
the component on every hashchange event. It's the
idiomatic way to drive the fiber reconciler from URL state.
const { createElement: h, render, useHash } = Vertex; const ROUTES = { "": Home, "projects": Projects, "about": About }; function App() { const hash = useHash(); // e.g. "#/projects" const key = hash.replace(/^#\//, ""); const Page = ROUTES[key] || NotFound; return h("main", null, h("nav", null, h("a", { href: "#/" }, "Home"), h("a", { href: "#/projects" }, "Projects"), h("a", { href: "#/about" }, "About") ), h(Page, null) ); } render(h(App, null), document.getElementById("root"));
Combine with Vertex.Router when you also need pattern
matching on dynamic segments — useHash handles the
re-render cycle while the Router extracts params.
11. jQuery compatibility
Vertex is explicitly designed to coexist with jQuery on the same page. The rule is simple: load jQuery first, then vertex.js.
<!-- jQuery loaded first --> <script src="/static/jquery.min.js"></script> <script src="/static/vertex.js"></script>
vertex.js checks window.jQuery and window.$
before assigning anything. If they exist it leaves them completely
alone. Use V$ or Vertex.$v() for the
Vertex DOM wrapper in that scenario:
// jQuery and Vertex DOM layer side by side — no conflict $("#jq-widget").datepicker(); // ← jQuery V$("#vx-card").on("click", fn); // ← Vertex // Or explicitly via the namespace Vertex.$v("#vx-card").css("color", "#c8ff00");
jQuery static utilities are mirrored on Vertex.VQuery:
VQuery.extend(), VQuery.each(),
VQuery.isArray(), VQuery.isFunction(),
VQuery.trim(), VQuery.noop(),
VQuery.parseJSON(), and VQuery.now().
12. Quick API reference
Vertex namespace
| Symbol | Description |
|---|---|
Vertex.createElement(type, props, …children) | Create a virtual element descriptor |
Vertex.render(element, container) | Mount or update the component tree |
Vertex.Fragment | Wrapper-free grouping element |
Vertex.lazy(factory) | Async component loader |
Vertex.createContext(default) | Create a context object |
Vertex.useState(initial) | Hook: local state |
Vertex.useReducer(reducer, initial) | Hook: reducer-based state |
Vertex.useEffect(fn, deps) | Hook: side effects & cleanup |
Vertex.useMemo(fn, deps) | Hook: memoised value |
Vertex.useCallback(fn, deps) | Hook: memoised callback |
Vertex.useRef(initial) | Hook: mutable ref |
Vertex.useContext(ctx) | Hook: read context value |
Vertex.useHash() | Hook: reactive URL hash |
Vertex.template | Mustache template constructor |
Vertex.template.load(url, options) | Fetch and mount a remote template file |
Vertex.template.load.baseUri | Base path prepended to relative URLs (default "") |
Vertex.Router | Singleton hash router |
Vertex.RouterClass | Backbone-style base class |
Vertex.$v(selector) | VQuery DOM wrapper |
Vertex.ajax(options) | Fetch wrapper |
Vertex.get(url, …) | Shorthand GET |
Vertex.post(url, …) | Shorthand POST |
VQuery instance methods
| Method | Description |
|---|---|
.on(events, [sel], fn) | Bind event (delegation if sel given) |
.off(events, [fn]) | Remove event handler(s) |
.trigger(event, [detail]) | Dispatch CustomEvent |
.attr(name, [val]) | Get / set attribute |
.css(prop, [val]) | Get computed / set inline style |
.val([v]) | Get / set input value |
.html([content]) | Get / set innerHTML |
.text([content]) | Get / set textContent |
.addClass / .removeClass / .toggleClass / .hasClass | Class manipulation |
.append / .prepend / .after / .before | DOM insertion |
.find / .parent / .children / .closest / .siblings | Traversal |
.first / .last / .eq(i) / .get(i) | Subset selection |
.filter / .not / .is / .add | Filtering |
.remove / .empty / .clone | DOM mutation |
.each(fn) | Iterate matched elements |
.data(key, [val]) | Get / set data-* attribute |
.hide / .show / .toggle | Visibility shortcuts |
.width / .height / .offset | Dimension helpers |
.serialize() | Serialise form to query string |
.prop(name, [val]) | Get / set DOM property |