01. Obtaining vertex.js

Vertex is a 1kloc library combining a jQuery-compatible DOM layer, a full Mustache template engine (Ractive.load-style — fetch a template, bind data, mount it), and a Backbone-style hash router. It deliberately has no React-style component layer — no virtual DOM, no fiber reconciler, no hooks. Here's the template engine's two-way data binding, live — drag the slider, or pick a track:

Active track

That's genuine two-way binding — the data stays live via nothing but the data-bind attribute itself, with a couple of lines of plain JS painting the bar and percentage as you drag (§06 has the full breakdown) — while the buttons show the same data object taking an ordinary .set() call just as easily. vertex.js itself is a single, self-contained file with no build step and no dependencies. Grab it straight from this site 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://float64co.github.io/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. 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

SyntaxBehaviour
{{key}} HTML-escaped interpolation
{{{key}}} / {{&key}} Raw / unescaped HTML (two equivalent forms)
{{user.name}} Nested dot-path resolution
{{#each items}} … {{/each}} Explicit, array-only loop — a silent no-op on anything that isn't an array. Item keys and @index available directly inside
{{#name}} … {{/name}} Full Mustache section: an array loops (like #each); a truthy object or scalar renders once with its keys pushed into scope; falsy or an empty array renders nothing
{{^name}} … {{/name}} Inverted section — renders only when name is falsy or an empty array
{{#if flag}} … {{/if}} Conditional block
{{#if flag}} … {{else}} … {{/if}} Conditional with fallback
{{! comment }} Comment — produces no output
{{> name}} Partial — resolved against the partials map passed to the template (see below)

Sections nest to any depth. Closing tags are matched structurally (a stack), not by name — {{#foo}} … {{/bar}} closes the same way {{#foo}} … {{/foo}} would.

Partials

new Vertex.template({
  el:       "#app",
  template: '<ul>{{#each users}}{{> user-row}}{{/each}}</ul>',
  partials: {
    "user-row": '<li>{{name}} — {{role}}</li>'
  },
  data: { users: [{ name: "Alice", role: "Engineer" }] }
});

Computed properties

Pass a computed map of getter functions alongside data. Each key is defined on the data object with this bound to the template instance, so it can read other fields and is re-evaluated on every render:

const list = new Vertex.template({
  el:       "#results",
  template: '{{#each visible}}<li>{{label}}</li>{{/each}}',
  data:     { items: [...], filter: "" },
  computed: {
    visible: function () {
      return this._data.items.filter(function (i) {
        return i.label.includes(this._data.filter);
      }, this);
    }
  }
});

list.set("filter", "bass");   // re-renders "visible" automatically

Two-way binding

That live demo at the top of the page is this in its entirety: add data-bind="keypath" to any <input> inside a template and Vertex keeps the input and the data object in sync automatically — no manual event wiring on your end. The range input has to live inside the template's own markup for this to work; data-bind only wires up inputs the template itself rendered, not ones sitting elsewhere on the page.

const TRACKS = [
  { name: "Jungle Pressure", bpm: 170 },
  { name: "Dark Matter DnB", bpm: 174 },
  { name: "Halftime Drift",  bpm: 85  }
];

const demo = new Vertex.template({
  el: "#md-out",
  template: `
    <input type="range" min="0" max="100" data-bind="progress"> <span class="pct">{{progress}}%</span>
    {{#each tracks}}
      <div class="track-row{{#if active}} active-row{{/if}}">
        {{@index}}. {{name}} — {{bpm}} bpm
      </div>
    {{/each}}
    <span class="bar" style="width:{{progress}}%"></span>
  `,
  data: { progress: 0, tracks: TRACKS.map((t, i) => ({ ...t, active: i === 0 })) }
});

// data-bind="progress" keeps the data live and commits once the drag
// settles — but that commit is debounced (see below), so the bar and
// percentage wouldn't visibly move DURING the drag on their own. This
// paints them on every tick via a direct write to two plain elements
// that have no native interactive state — never the slider itself.
Vertex.$v("#md-out").on("input", "input[type=range]", function () {
  Vertex.$v("#md-out .pct").text(this.value + "%");
  Vertex.$v("#md-out .bar").css("width", this.value + "%");
});

// only the button-driven "active track" needs an explicit .set()
Vertex.$v("#md-tracks").on("click", "button", function () {
  const idx = parseInt(this.dataset.track, 10);
  demo.set("tracks", TRACKS.map((t, i) => ({ ...t, active: i === idx })));
});

Every change still replaces the template's entire innerHTML — there's no virtual DOM here — which raises an obvious question for a range input specifically: a slider's drag is native pointer-capture state tied to that exact DOM node, and per spec that capture releases the instant the element leaves the document, even if an identical node is reinserted the same tick. Re-rendering on every input tick would silently end the drag after the first pixel of movement. So for <input type="range"> specifically, input events update the data model directly — .get() is always live — without touching the DOM at all; the official re-render (the one the template engine itself drives, reconciling everything bound to that key) fires once, debounced ~120ms after the last input event goes quiet. That's deliberately not wired to the native change event — some browsers fire it more than once per drag instead of exactly once at release, which would mean occasionally re-rendering (and interrupting) mid-gesture anyway. Watching for a gap in input events needs no assumption about any browser's change semantics at all.

That debounce is why the demo's bar and percentage visibly move during the drag despite all that — they're not waiting on the template at all. A second, plain input listener (see the code above) writes their width/text directly on every tick, entirely outside the render pipeline. That's safe specifically because a <span> has no native interactive state to lose — unlike the slider, it was never risky to touch mid-drag, just pointless to route through a whole-subtree re-render for. The official, debounced re-render still happens once the drag settles, so anything else bound to progress — with no hand-rolled listener of its own — still ends up correct, just a beat later.

Every other input type still re-renders on every keystroke, with the currently-focused element spliced back in as the same live node rather than a freshly-parsed lookalike, so cursor position survives too. See the interop guide for what "everything else gets fully rebuilt on every change" means for foreign DOM you park inside a template yourself.

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

Mounting anything else — a third-party widget, a nested template — inside a template's own element? Its re-render will destroy it. Read the interop guide for the safe patterns.

07. 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();

08. 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().

09. Quick API reference

Vertex namespace

SymbolDescription
Vertex.templateMustache template constructor
Vertex.template.load(url, options)Fetch and mount a remote template file
Vertex.template.load.baseUriBase path prepended to relative URLs (default "")
Vertex.parseTemplate(tmpl, data, partials?)Render a template string to HTML without mounting it
Vertex.RouterSingleton hash router
Vertex.RouterClassBackbone-style base class
Vertex.VQueryThe DOM wrapper constructor
Vertex.$v(selector)VQuery DOM wrapper
Vertex.ajax(options)Fetch wrapper
Vertex.get(url, …)Shorthand GET
Vertex.post(url, …)Shorthand POST

Template instance methods

MethodDescription
.get(keypath)Read a value from the data object (dot-path supported)
.set(keypath, value)Write one value and re-render; emits "change"
.update(partialData)Merge multiple keys into the data object and re-render
.on(event, fn) / .off(event, [fn])Listen for/remove "change" handlers
.teardown()Clear the mounted element and drop all handlers

VQuery instance methods

MethodDescription
.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 / .hasClassClass manipulation
.append / .prepend / .after / .beforeDOM insertion
.find / .parent / .children / .closest / .siblingsTraversal
.first / .last / .eq(i) / .get(i)Subset selection
.filter / .not / .is / .addFiltering
.remove / .empty / .cloneDOM mutation
.each(fn)Iterate matched elements
.data(key, [val])Get / set data-* attribute
.hide / .show / .toggleVisibility shortcuts
.width / .height / .offsetDimension helpers
.serialize()Serialise form to query string
.prop(name, [val])Get / set DOM property