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.name || 'Guest'}} // fallback when empty
    {{html this.body}}       // trusted HTML (use carefully)
    {{myApp.data.field}}     // read another app
    Template vs bind {{this.x}} in an app template renders once at boot. bind fields stay live when you set().

    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
      readyAfter first renderFocus, logging, DOM plugins

      Ajax hooks (loaded), route teardown (destroy), and depends are covered 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"{ id, text }id: 1 = checked
      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
      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

      <label><input type="checkbox" bind="agree" /> I agree</label>
      <select bind="colors"></select>
      <select multiple bind="tags"></select>
      <div bind="size" bind-type="radio"></div>
      
      this.set('colors', false, { id: 'green' });  // set selected option

      input type="radio" alone is not a bind target — use a wrapper with bind-type="radio".

      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: '…' } }. Map the response in loaded before the template renders. Use error on a load entry for failed requests.

      Code
      pax.app('home', {
        load: {
          question: {
            url: 'https://yesno.wtf/api',
            error: function(err) {
              this.data.answer = 'unavailable';
            }
          }
        },
        loaded: function() {
          this.data.answer = this.load.question.data.answer;
          this.data.image = this.load.question.data.image;
        },
        template: '<h1>{{this.answer}}</h1><img src="{{this.image}}" />'
      });
      Live
      loaded vs ready Map ajax data in loaded (before render). Use ready for DOM work after the template is on screen.

      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.

      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
      data, valuesApp state (merged on re-register)
      template, templatesHTML templates with {{…}}
      loadAjax entries — each needs url; optional error
      dependsArray of app names to wait for
      changePer-field change callbacks
      sharedtrue (default) or false for instances
      urlRoute path; auto-set from <template app> if omitted
      init, loaded, ready, destroyLifecycle hooks
      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)
      ElementDataMethods
      Text elementsstringset()
      input, textareastringtwo-way + set()
      input type="checkbox"{id, text}two-way + set()
      select[{id, text}]set() / values
      select multiple[{id, text}]values (array)
      bind-type="radio"[{id, text}]set() / values
      ul, ol, tbodyarraypush(), pop(), render()
      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.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.

      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 →