PAX JS Framework

A micro JS framework — add app and bind to your markup, register a small object, and go. One file, no build step.

This guide starts simple and builds up — basics first, then core concepts, then advanced binding, ajax, and routing.

Install

Load from unpkg (no install), via npm, or use dist/ from this repo.

<!-- CDN (latest) -->
<script src="https://unpkg.com/pax-js-framework/dist/pax.js"></script>
<!-- minified: https://unpkg.com/pax-js-framework/dist/pax.min.js -->

<!-- npm install pax-js-framework -->
<script src="node_modules/pax-js-framework/dist/pax.min.js"></script>

<!-- local clone / this site -->
<script src="dist/pax.js"></script>

Then mark a root element with app="name", register with pax.app('name', { … }), and PAX boots on page load.

Part 1 — The basics

1 Your first app

An app is a named chunk of HTML + data. Put template text inside the root element, or pass a template string. Use {{this.field}} to print escaped data.

Code
<div app="hello">{{this.message}}</div>

<script src="https://unpkg.com/pax-js-framework/dist/pax.js"></script>
<script>
  pax.app('hello', {
    data: { message: 'Hello World' }
  });
</script>
Live
{{this.message}}

2 bind fields

bind="field" connects an element to data.field. Text binds update when you call this.set('field', value).

Code
<div app="greeter">
  <h1 bind="title"></h1>
  <button onclick="pax.greeter.greet()">Greet</button>
</div>

pax.app('greeter', {
  data: { title: 'Welcome' },
  greet: function() {
    this.set('title', 'Hello, PAX!');
  }
});
Live

3 Lists & push

A <ul bind="todos"> holds an array of strings. Call this.push('todos', value) to add a row — PAX renders each item as an <li> for you.

Code
<div app="todo">
  <h1 bind="title"></h1>
  <ul bind="todos"></ul>
  <input bind="inp" type="text" />
  <button onclick="pax.todo.add()">Add</button>
</div>

pax.app('todo', {
  data: {
    title: 'My List',
    todos: ['Learn PAX'],
    inp: ''
  },
  add: function() {
    if (!this.data.inp) return;
    this.push('todos', this.data.inp);
    this.set('inp', '');
  }
});
Live

    4 Two-way input & change

    Inputs sync back to data automatically. Use a change handler to react — here we reverse the typed text into another bind.

    Code
    <div app="mirror">
      <input bind="inp" type="text" placeholder="Type here" />
      <h2 bind="message">Reversed text appears here</h2>
    </div>
    
    pax.app('mirror', {
      change: {
        inp: function(v) {
          this.set('message', v.split('').reverse().join(''));
        }
      }
    });
    Live

    Reversed text appears here

    5 Click handlers

    Write normal JS in onclick="…". PAX rewires handlers at render (CSP-friendly). Call app methods via pax.appname.method().

    Code
    <button onclick="pax.counter.up()">+1</button>
    <span bind="count">0</span>
    
    pax.app('counter', {
      data: { count: 0 },
      up: function() {
        this.set('count', this.data.count + 1);
      }
    });
    Live
    0

    Part 2 — Core concepts

    How binding works

    Each bind="field" connects to data.field. PAX picks the wiring from the HTML tag — you don't declare types in JS.

    Elementdata shapeHow it updates
    h1, p, span, div, …stringthis.set('field', val)
    input, textareastringTwo-way — typing updates data
    ul / olarray of stringspush(), pop()

    For a list, put an array in data and call push — PAX renders one <li> per item using the default template {{value}}.

    <ul bind="todos"></ul>
    data: { todos: ['Learn PAX', 'Build something'] }
    
    this.push('todos', 'New item');
    this.pop('todos');       // remove last
    this.pop('todos', 1);    // remove at index

    More element types (select, checkbox, tables, custom row templates) are in Lists, forms & tables.

    Templates

    Use {{…}} in app template strings or inside bind markup. Output is escaped by default.

    {{this.name}}            // text from data
    {{this.user.email}}      // nested path (reactive in templates)
    {{this.name || 'Guest'}} // fallback when empty
    {{html this.body}}       // trusted HTML (use carefully)
    {{myApp.data.field}}     // read another app
    Template vs bind Simple {{this.field}} and nested paths like {{this.a.b}} or {{this.a.b.c}} in an app template are compiled to bind at boot — they stay live when you set() the path or replace a parent object. In attributes (e.g. src="{{this.image}}"), PAX compiles to attr:src="this.image" instead. Fallbacks ({{this.x || 'Guest'}}) and {{html …}} stay one-shot; use an explicit bind when you need those to update.
    templates iteration If data for a bind is an array, PAX renders the template once per item. Objects and strings render a single pass — no separate type flag needed. Nested paths like {{this.meta.tag}} work in row templates; use set() or push/pop to refresh. See nested.html.

    Show, hide & attr

    Toggle visibility and attributes from data — no manual style.display or helper strings in templates.

    <p show="!items.length">Nothing here yet.</p>
    <p hide="items.length">List is empty.</p>
    <img attr:src="this.image" />

    Expressions use data paths (todos.length), ! for negation, or app methods (isAdmin()).

    Code
    <div app="showdemo">
      <ul bind="items"></ul>
      <p show="!items.length">No items yet.</p>
      <button onclick="pax.showdemo.add()">Add</button>
    </div>
    
    pax.app('showdemo', {
      data: { items: [] },
      add: function() {
        this.push('items', 'Item ' + (this.data.items.length + 1));
      }
    });
    Live

      No items yet.

      Lifecycle hooks

      Each app boots in order. Pick the hook that matches when you need to run code:

      HookWhenUse for
      initBefore ajax / renderEarly setup that does not need fetched data or DOM
      loadedAfter ajax finishes, before first renderMap or transform load data, read from dependency apps
      readyAfter first renderFocus, logging, third-party plugins, reading pax.url
      destroyWhen leaving a routed appClear timers, listeners, or other teardown

      Boot order with ajax: init → load requests → loaded → render → ready. destroy runs only for routed apps when navigating away. See also depends in Part 3.

      Part 3 — Advanced

      Lists, forms & tables

      Full bind reference — for when you need more than strings in a list.

      Elementdata shapeHow it updates
      ul / olarray (strings or objects)push(), pop(), render()
      input type="checkbox"Checked state in values as {checked, text}
      selectarray of {id, text}Options from data; selection in values
      select multiplearray of {id, text}Selected items in values
      wrapper bind-type="radio"array of {id, text}Selected option in values
      wrapper bind-type="checkboxes"array of {id, text}Selected options in values (array)
      contenteditablestring (HTML)Two-way — edits update data on blur
      tbody / tablearray of objectspush(), pop(), render()

      Custom list templates

      String arrays use the default row: <li data-index="{{index}}">{{value}}</li>. For objects or buttons per row, set templates:

      data: { todos: [{ text: 'Learn PAX', done: 0 }] },
      templates: {
        todos: `<li data-index="{{index}}">{{this.text}}
          <button onclick='pax.home.pop("todos",{{index}})'>x</button></li>`
      }

      In list rows use {{this.field}}, {{index}}, and {{value}} for the current item. Put {{…}} inside the <ul> in HTML to use that as the row template instead.

      See todoAdvanced.html for a full example. push and pop patch individual rows when possible (append/remove) so other rows keep focus; call render() yourself after sort or bulk edits.

      Checkbox, select & radio

      data: {
        colors: [{ id: 'red', text: 'Red' }, { id: 'green', text: 'Green' }],
        multiTags: [{ id: 'a', text: 'Alpha' }, { id: 'b', text: 'Beta' }],
        size: [{ id: 's', text: 'Small' }, { id: 'm', text: 'Medium' }],
        tagBoxes: [{ id: 'a', text: 'Alpha' }, { id: 'b', text: 'Beta' }]
      },
      values: {
        agree: { checked: false, text: 'I agree' }
      },
      
      <label><input type="checkbox" bind="agree" /> I agree</label>
      <select bind="colors"></select>
      <select multiple bind="multiTags"></select>
      <div bind="size" bind-type="radio"></div>
      <div bind="tagBoxes" bind-type="checkboxes"></div>
      
      this.set('colors', false, { id: 'green' });       // selected option → values
      this.set('tagBoxes', false, [{ id: 'a' }]);       // checked boxes → values

      Options live in data (arrays of {id, text}); selections live in values. Use a unique bind id per element — the docs reuse tags in two snippets only to show both APIs, not on one page. See checkboxes.html.

      input type="radio" alone is not a bind target — use a wrapper with bind-type="radio". For multiple checkboxes, use bind-type="checkboxes" the same way (options in data, selections in values).

      Contenteditable

      <div bind="body" contenteditable="true"></div>
      
      data: { body: '<p>Edit me</p>' }  // two-way HTML on blur

      DOM scrape vs data

      On init, if a bind key is missing from data, PAX reads the DOM once (existing <li> items, table rows, select options). If the key exists — even [] or '' — data wins.

      Use filters to transform values before render: filters: { price: function(v) { return '$' + v; } }. Override tag detection with bind-type when needed.

      Ajax load

      Declare load: { key: { url: '…' } }. The JSON response is copied to data.key by default (setData is true). Use loaded only when you need extra mapping, and error on a load entry for failed requests.

      Code
      pax.app('home', {
        load: {
          question: {
            url: 'https://yesno.wtf/api',
            error: function(err) {
              this.data.question = { answer: 'unavailable', image: '' };
            }
          }
        },
        template: '<h1>{{this.question.answer}}</h1><img src="{{this.question.image}}" alt="" />'
      });
      Live
      loaded vs ready Ajax responses land on data automatically. Use app loaded for extra mapping before render. Use ready for DOM work after the template is on screen. Set setData: false on a load entry to keep the response only on load.key.data.

      App dependencies

      depends: ['otherApp'] waits until the other app finishes load + render. Apps without an [app] element still boot when something depends on them.

      Code
      pax.app('session', {
        data: { user: 'Jane Doe', role: 'Admin' },
        template: '<small>Session: {{this.user}}</small>'
      });
      
      pax.app('dash', {
        depends: ['session'],
        loaded: function() {
          this.data.msg = 'Welcome, ' + pax.session.data.user;
        },
        template: '<strong>{{this.msg}}</strong>'
      });
      Live

      Routing

      Set pax.routeHash = 1, put route templates in #routes, and navigate with pax.link('#/page').

      Each routed app needs a url — set it on the app def, or let PAX derive it from <template app="…">:

      <div id="routes">
        <template app="home">…</template>
        <template app="page1">…</template>
        <template app="settings_profile">…</template>
      </div>
      
      // auto: home → /home, page1 → /page1, settings_profile → /settings/profile
      // app="index" → /
      // override anytime: pax.app('home', { url: '/' })

      Underscores in the app name become path segments; dashes become underscores in the app key (my-page → app my_page, url /my-page). An explicit url on pax.app() always wins.

      Route loader

      While a route app loads, PAX shows pax.loadTemplate in the route outlet. The core build keeps this empty to stay small — add a loader only if you want one.

      <script src="pax.min.js"></script>
      <script src="pax-loader.js"></script>  <!-- optional SVG spinner -->
      
      // or inline / custom:
      pax.loadTemplate = '<svg …>…</svg>';
      
      // or fetch from CDN (call before first navigation):
      pax.setLoadTemplate('https://unpkg.com/pax-js-framework@1.0.11/dist/pax-loader.svg');

      dist/pax-loader.svg is a compact animated spinner (~400 bytes). pax-loader.js sets loadTemplate for you — include it right after pax.min.js.

      URL segments — pax.url

      On every navigation, PAX splits the path into pax.url — an array of segments between slashes. Routing picks the longest registered match; anything after that stays in the array for your app to read.

      // #/page1/edit/42  →  pax.url = ['page1', 'edit', '42']
      // route /page1 matches → page1 app boots
      // pax.urlTail = ['edit', '42']  (segments after the route)
      
      pax.app('page1', {
        url: '/page1',
        ready: function() {
          this.data.action = pax.url[1];       // 'edit'
          this.data.id = pax.url[2];           // '42'
          // or use pax.urlTail / this.urlTail
        }
      });

      pax.path is the full path string (no #). Query strings are in pax.getVars (e.g. ?tab=settingspax.getVars.tab).

      There is no built-in :id param syntax yet — read pax.url or pax.urlTail in ready / loaded and validate length yourself. That keeps the router small; named params can come later if needed.

      Use destroy on routed apps to clear timers or listeners when the user navigates away.

      See routing (manual urls) and routingAutomated (auto urls) below.

      Shared & independent instances

      shared: true (default) — one data model, mirrored DOM. shared: false — each [app="name"] gets its own clone; clicks target the right instance. Multiple shared roots stay in sync via mirror.

      See parentChild and parentChildInstances below.

      Debug mode

      pax.config.debug = true;  // before pax.app() calls

      Logs warnings for typos, missing apps, handler errors, and bad depends.

      API reference

      App definition keys
      KeyDescription
      State
      dataReactive app state. Each key lines up with a bind="key" field. Merged (not replaced) when you call pax.app() again. Holds list rows, text, checkbox state, and ajax payloads unless setData is disabled.
      valuesCurrent form selections, separate from option lists. For select, select multiple, and radio/checkbox groups, data holds the options ([{id, text}]) while values holds what the user picked. pax.getVals() prefers values over data for a bind id.
      Templates
      templateHTML string for the whole app root — one block with {{this.field}} expressions. Simple {{this.field}} entries compile to binds and stay live on set(). If omitted, PAX uses the innerHTML of the [app] element or a route <template app> tag.
      templatesPer-bind HTML keyed by bind id — a child template for each bind instead of a parent template for the entire app. Controls how that bound element renders (lists, inputs, or any bind). When data for that bind is an array, PAX iterates the template once per item. Merged on re-register.
      Ajax & dependencies
      loadAjax entries — each needs url. Optional error, loaded (per entry), and setData (default true — copies the JSON response onto data[key]).
      dependsArray of app names this app waits for before booting. Useful when one app reads another's data or ajax results.
      Callbacks
      changePer-field callbacks — change: { field: function(val, id, key) { … } }. Fires after set(), user input, push(), or pop() updates that field.
      Routing & instances
      urlRoute path for SPA routing. Auto-set from <template app> if omitted (underscores become path segments).
      sharedtrue (default) — one global app instance. false — a separate instance per [app] element (see pax.get(name, ref)).
      Lifecycle
      initRuns once at boot, before ajax load requests. Use for early setup that does not need fetched data or rendered DOM.
      loadedRuns after all load requests finish, before the first render. Use to map or transform ajax data, or read from dependency apps.
      readyRuns after the first render. DOM is on screen — use for focus, logging, third-party plugins, or reading pax.url / pax.urlTail.
      destroyRuns when a routed app is left for another route. Use to clear timers, listeners, or other teardown. Not called for static page apps.
      Instance methods (on every app)
      MethodDescription
      this.set(id, val)Update data and re-render binds
      this.push(id, val)Append to array bind
      this.pop(id)Remove from array bind
      this.render()Re-run bind wiring
      Bind types (auto-detected)
      ElementDataHow it updates
      Text elementsstringset() writes data → DOM (one-way)
      input, textareastringTwo-way — typing updates data; set() updates the field from code
      input type="checkbox"Checked state in values as {checked, text}set('agree', false, {checked:true}) from code
      select[{id, text}]Options in data; selected option in values. set() refreshes options; user change updates values
      select multiple[{id, text}]Options in data; selected items in values (array)
      bind-type="radio"[{id, text}]Options in data; selected option in values
      bind-type="checkboxes"[{id, text}]Options in data; selected options in values (array)
      contenteditablestring (HTML)Two-way — editing updates data on blur; set() updates HTML from code
      ul, ol, tbodyarraypush(), pop(), render()

      Two-way means user input flows back into data (PAX listens for keyup / change). One-way text binds only update when you call set(). In both cases, set() is how you change a field from JavaScript and refresh the DOM.

      HTML directives
      AttributeDescription
      show="expr"Visible when expression is truthy
      hide="expr"Hidden when expression is truthy
      attr:name="expr"Bind attribute to data (attr:src, attr:checked, …)
      Framework (pax)
      APIDescription
      pax.app(name, def)Register app (register alias)
      pax.get(name, ref?)Get app or instance (ref = index for shared: false)
      pax.getVals(key, ids)Read bind values from an app
      pax.versionFramework version string
      pax.link(path)Navigate (hash or history)
      pax.loadTemplateHTML shown in the route outlet while a page loads (empty by default)
      pax.setLoadTemplate(src, done?)Set loader HTML inline or fetch an SVG/file; optional callback when fetch finishes
      pax.urlPath segments array — ['page1', 'edit', '42']
      pax.urlTailSegments after the matched route
      pax.pathFull path string (no hash)
      pax.getVarsQuery string key/value pairs
      pax.routeHash1 for #/path routing
      pax.config.debugEnable dev warnings
      pax.el.routesRoute outlet selector (default #routes)

      Full examples

      Each example shows the source code next to a live preview. Open the full page for more room.

      nested.html

      Nested {{this.a.b.c}} in app templates and list row templates.

      Code
      Loading…
      Live
      Open full page →

      checkboxes.html

      Checkbox, select, multi-select, radio, and checkbox groups (data + values).

      Code
      Loading…
      Live
      Open full page →

      change2.html

      push on a data-only array and update a bound heading.

      Code
      Loading…
      Live
      Open full page →

      load.html

      Ajax load — response on data by default.

      Code
      Loading…
      Live
      Open full page →

      depends.html

      Wait for another app before booting.

      Code
      Loading…
      Live
      Open full page →

      tables.html

      Table / tbody binding and DOM scrape.

      Code
      Loading…
      Live
      Open full page →

      parentChild.html

      Nested shared apps (shared: true).

      Code
      Loading…
      Live
      Open full page →

      parentChildInstances.html

      Independent instances (shared: false).

      Code
      Loading…
      Live
      Open full page →

      routingAutomated.html

      Auto route paths from <template app>.

      Code
      Loading…
      Live
      Open full page →