Data Binding

WakaPAC uses the data-pac-bind attribute to connect your data to DOM elements. Bindings automatically synchronize between your data and the page - when data changes, the DOM updates, and when users interact with form elements, your data updates.

explanation

Understanding Data Binding

Binding Syntax

The data-pac-bind attribute contains comma-separated binding declarations:

<element data-pac-bind="bindingType: expression, anotherType: expression">

Each binding has two parts:

  • Binding type: What aspect of the element to control (value, visible, click, etc.)
  • Expression: An expression evaluated against your data using WakaPAC's expression language

Expression Language

Expressions inside data-pac-bind and {{ }} interpolations aren't run through JavaScript's eval or Function. WakaPAC ships its own small hand-written parser and tokenizer that reads the expression string, builds a syntax tree from it, and evaluates that tree directly against your abstraction. This keeps expressions safe to embed in HTML attributes, but it also means the language is a deliberately limited subset of JavaScript rather than all of it.

Supported:

  • Property access via dot and bracket notation, including computed keys: user.name, items[0], regions[country]
  • Literals: strings, numbers, true, false, null, undefined, array literals ([1, 2, 3]) and object literals ({color: 'red'})
  • Arithmetic (+ - * / %), comparison (=== !== == != >= <= > <), and logical (&& ||) operators
  • Unary operators: !, -, +
  • Ternary expressions: isActive ? 'on' : 'off'
  • Method calls on your abstraction: {{ formatDate(createdAt) }}
  • One level of method call on a data property that's itself a plain object: user.getFullName()
  • A whitelisted set of array methods — includes(), indexOf(), join() — usable on an array at any nesting depth, e.g. user.tags.includes('admin')

Not supported:

  • Array methods that take a callback, such as filter and map — that logic belongs in a computed property, where it's cached and only recalculates when dependencies change
  • Method calls chained two or more levels deep on a non-array object, e.g. user.address.getCity() — only a bare top-level method or one property hop can carry a call; move deeper logic into a computed property or a top-level method instead
  • Assignment (=, +=, etc.), new, arrow functions, and template literals
  • Optional chaining (?.) and nullish coalescing (??)
  • Multiple statements or semicolons — an expression always evaluates to a single value

One-Way vs Two-Way Binding

One-way bindings flow data from your abstraction to the DOM. When your data changes, the DOM updates automatically:

  • visible, if, enable - Control element state
  • click, submit, change - Attach event handlers
  • class, style - Control appearance
  • Standard attributes (src, href, title, etc.)

Two-way bindings flow data in both directions. DOM changes update your data, and data changes update the DOM:

  • value - For text inputs, textareas, select dropdowns, and multi-select (bound to an array)
  • checked - For checkboxes

Special Scope Variables

Event-handler bindings (click, submit, change) expose a few extra variables that aren't part of your data model. Reference them like any other identifier once you use parentheses to pass explicit arguments:

  • $event - The original DOM event. Available on click, submit, and change.
  • $item - The current array element. Available only on click inside a foreach loop.
  • $index - The current array position (0-based). Available only on click inside a foreach loop.
<li data-pac-bind="click: removeTask($item.id, $index, $event)">{{item.name}}</li>

Without parentheses, a bare method reference receives these as plain function parameters automatically instead — see Event Handlers in foreach for that behavior.

When Bindings Update

Bindings re-evaluate automatically whenever the data they reference changes. For example, if a binding uses {{ user.name }}, it updates when user.name is assigned a new value. WakaPAC tracks dependencies and only updates affected bindings.

Form Input Bindings

value - Text Inputs and Textareas

Two-way binding for text fields. Changes in the input update the data property, and changing the data property updates the input:

<div id="app">
    <input data-pac-bind="value: username" type="text">
    <textarea data-pac-bind="value: description"></textarea>

    <p>Username: {{username}}</p>
    <p>Description: {{description}}</p>
</div>

<script>
    wakaPAC('#app', {
        username: 'alice',
        description: 'Software developer'
    });
</script>

value - Select Dropdowns

Track the selected option's value:

<div id="app">
    <select data-pac-bind="value: selectedOption">
        <option value="A">Option A</option>
        <option value="B">Option B</option>
        <option value="C">Option C</option>
    </select>

    <p>You selected: {{selectedOption}}</p>
</div>

<script>
    wakaPAC('#app', {
        selectedOption: 'B'  // Pre-selects "Option B"
    });
</script>

value - Select Dropdowns (Multiple)

For <select multiple>, bind to an array. The array contains the values of all currently selected options, and setting it pre-selects the matching options:

<div id="app">
    <select multiple data-pac-bind="value: selectedColors">
        <option value="red">Red</option>
        <option value="green">Green</option>
        <option value="blue">Blue</option>
        <option value="yellow">Yellow</option>
    </select>

    <p>Selected: {{selectedColors.join(', ')}}</p>
</div>

<script>
    wakaPAC('#app', {
        selectedColors: ['red', 'blue']  // Pre-selects Red and Blue
    });
</script>

checked - Checkboxes

Two-way binding for checkbox state (true when checked, false when unchecked):

<div id="app">
    <label>
        <input type="checkbox" data-pac-bind="checked: isActive">
        Active
    </label>

    <label>
        <input type="checkbox" data-pac-bind="checked: newsletter">
        Subscribe to newsletter
    </label>

    <p>Active: {{isActive}}, Newsletter: {{newsletter}}</p>
</div>

<script>
    wakaPAC('#app', {
        isActive: true,
        newsletter: false
    });
</script>

value - Radio Buttons

Radio buttons use value binding (not checked) because all radio buttons in a group bind to the same property. The bound property stores which radio button's value is currently selected:

<div id="app">
    <label>
        <input type="radio" name="theme" value="light" data-pac-bind="value: selectedTheme">
        Light
    </label>
    <label>
        <input type="radio" name="theme" value="dark" data-pac-bind="value: selectedTheme">
        Dark
    </label>
    <label>
        <input type="radio" name="theme" value="auto" data-pac-bind="value: selectedTheme">
        Auto
    </label>

    <p>Selected theme: {{selectedTheme}}</p>
</div>

<script>
    wakaPAC('#app', {
        selectedTheme: 'light'
    });
</script>

Display Control Bindings

visible - Toggle Visibility

Controls element visibility using the CSS display property. The element remains in the DOM but is hidden when the expression evaluates to false:

<div id="app">
    <button data-pac-bind="click: toggle">Toggle</button>

    <div data-pac-bind="visible: isVisible">
        <p>This content is only visible when isVisible is true</p>
    </div>
</div>

<script>
    wakaPAC('#app', {
        isVisible: true,
        toggle() {
            this.isVisible = !this.isVisible;
        }
    });
</script>

Use visible when the element is toggled frequently, or when you want to preserve its state (scroll position, input values) while hidden. Use if or wp-if when the content is conditionally relevant — it won't be rendered at all when the condition is false.

if - Conditional Rendering

Clears and restores the element's innerHTML based on the condition. The outer element remains in the DOM but becomes empty when the condition is false:

<div id="app">
    <div data-pac-bind="if: !isLoading">
        <p>Content loaded successfully</p>
    </div>
</div>

<script>
    wakaPAC('#app', {
        isLoading: false,
    });
</script>

wp-if - Comment-Based Conditionals

Use HTML comments to conditionally render content without wrapper elements. This is ideal for tables, grids, or when you need to control multiple sibling elements together. A block opens with <!-- wp-if: expression -->, optionally continues with one or more <!-- wp-else-if: expression --> branches, optionally ends with a <!-- wp-else --> fallback, and always closes with <!-- /wp-if -->. Branches are evaluated in order and only the first truthy one is rendered:

<div id="app">
    <!-- wp-if: status === 'loading' -->
    <p>Loading...</p>
    <!-- wp-else-if: status === 'error' -->
    <p>Something went wrong.</p>
    <!-- wp-else-if: status === 'empty' -->
    <p>No results found.</p>
    <!-- wp-else -->
    <p>Ready.</p>
    <!-- /wp-if -->
</div>

<script>
    wakaPAC('#app', {
        status: 'ready'
    });
</script>

enable - Enable/Disable Controls

Controls the disabled attribute on form elements. When the expression is true, the element is enabled; when false, it's disabled:

<div id="app">
    <input data-pac-bind="value: email" placeholder="Enter email">
    <button data-pac-bind="enable: isFormValid, click: submit">Submit</button>

    <p data-pac-bind="visible: !isFormValid">Please enter a valid email address</p>
</div>

<script>
    wakaPAC('#app', {
        email: '',

        computed: {
            isFormValid() {
                return this.email.includes('@') && this.email.length > 5;
            }
        },

        submit() {
            console.log('Form submitted:', this.email);
        }
    });
</script>

Event Handler Bindings

click - Event Handlers

Binds a method on your abstraction to the element's click event. Use a bare method reference (no parentheses) to let WakaPAC pass the event object automatically:

<div id="app">
    <button data-pac-bind="click: handleClick">Click me</button>
    <p>Clicked {{count}} times</p>
</div>

<script>
    wakaPAC('#app', {
        count: 0,
        handleClick(event) {
            this.count++;
        }
    });
</script>

Use parentheses when you want to pass specific arguments instead. WakaPAC then evaluates the call as an expression and no event object is injected automatically — though it's still available as $event; see Special Scope Variables:

<button data-pac-bind="click: remove('item-1')">Remove</button>

Inside a foreach, bare method references receive (item, index, event) instead of just (event). See Event Handlers in foreach for details.

submit - Form Submission

Binds a method on your abstraction to a <form>'s submit event. WakaPAC always calls event.preventDefault() for you, so the browser never performs its default navigation/submission — your method decides what happens next:

<div id="app">
    <form data-pac-bind="submit: handleSubmit">
        <input data-pac-bind="value: email" type="email" required>
        <button type="submit">Sign up</button>
    </form>
</div>

<script>
    wakaPAC('#app', {
        email: '',
        handleSubmit(event) {
            console.log('Submitting:', this.email);
        }
    });
</script>

Same bare-reference vs. parentheses rule as click (see above) — parentheses let you pass explicit arguments instead of the auto-injected event:

<form data-pac-bind="submit: handleSubmit($event)">...</form>

change - Change Events

Binds a method on your abstraction to the element's change event — fired when a checkbox, radio button, or <select> is toggled/selected, or when a text input commits its value (typically on blur). It's commonly combined with value or checked on the same element to run extra logic whenever the bound value settles:

<div id="app">
    <select data-pac-bind="value: country, change: onCountryChange">
        <option value="us">United States</option>
        <option value="nl">Netherlands</option>
    </select>
</div>

<script>
    wakaPAC('#app', {
        country: 'us',
        onCountryChange(event) {
            console.log('Country changed to:', this.country);
        }
    });
</script>

Same bare-reference vs. parentheses rule as click (see above):

<input data-pac-bind="value: email, change: validateField('email', $event)">

Event Handlers in foreach

Bare method references inside a foreach loop automatically receive the current item and index as parameters, in addition to the event object:

removeTask(item, index, event) {
    // item: the current array element
    // index: the array position (0-based)
    // event: the original DOM event
    this.tasks.splice(index, 1);
}

This automatic injection only applies to bare references (click: removeTask). Calls with parentheses (click: removeTask()) are evaluated as expressions instead and receive no parameters automatically — but the same values are available as $item, $index, and $event; see Special Scope Variables.

See foreach - Repeat Elements below for the full foreach binding syntax.

Style and Appearance Bindings

class - CSS Classes (String)

Apply CSS classes dynamically based on data. When using a string expression, the result becomes the element's class:

<div id="app">
    <div data-pac-bind="class: currentTheme">
        <p>This div has dynamic classes</p>
    </div>

    <button data-pac-bind="click: changeTheme">Change Theme</button>
</div>

<script>
    wakaPAC('#app', {
        currentTheme: 'dark',
        changeTheme() {
            this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
        }
    });
</script>

class - CSS Classes (Object)

Toggle multiple classes independently using an object. Each key is a class name, each value is a boolean expression:

<div id="app">
    <div data-pac-bind="class: {active: isActive, error: hasError, 'user-premium': isPremium}">
        <p>This div has conditional classes</p>
    </div>

    <button data-pac-bind="click: toggleActive">Toggle Active</button>
    <button data-pac-bind="click: toggleError">Toggle Error</button>
</div>

<script>
    wakaPAC('#app', {
        isActive: true,
        hasError: false,
        isPremium: true,
        toggleActive() {
            this.isActive = !this.isActive;
        },
        toggleError() {
            this.hasError = !this.hasError;
        }
    });
</script>

style - Inline Styles (Object)

Apply inline CSS styles dynamically. Each key is a CSS property, each value is a string or expression:

<div id="app">
    <div data-pac-bind="style: {color: textColor, fontSize: fontSize + 'px', backgroundColor: bgColor}">
        <p>This text has dynamic styling</p>
    </div>

    <button data-pac-bind="click: increaseFontSize">Increase Font</button>
</div>

<script>
    wakaPAC('#app', {
        textColor: '#333',
        fontSize: 16,
        bgColor: '#f0f0f0',
        increaseFontSize() {
            this.fontSize += 2;
        }
    });
</script>

Attribute Bindings

Bind any standard HTML attribute by using the attribute name as the binding type:

<div id="app">
    <img data-pac-bind="src: imageUrl, alt: imageDescription">
    <a data-pac-bind="href: linkUrl, title: linkTitle">Click here</a>
    <input data-pac-bind="placeholder: placeholderText, maxlength: maxChars">
</div>

<script>
    wakaPAC('#app', {
        imageUrl: 'photo.jpg',
        imageDescription: 'A beautiful photo',
        linkUrl: 'https://example.com',
        linkTitle: 'Visit our website',
        placeholderText: 'Enter your name',
        maxChars: 50
    });
</script>

Data Model Utilities

Property Aliasing

Unlike the sections above, this isn't a data-pac-bind binding type — it's a JavaScript API for your data model, used when defining your abstraction.

Sometimes two properties need to refer to the same underlying value — reading or writing either one should always affect the same reactive signal. wakaPAC.sameAs() creates a getter/setter alias that redirects all reads and writes to a target property path. The alias is fully two-way and participates in bindings exactly like any other property.

wakaPAC('#signup', {
    email:         '',
    confirm_email: wakaPAC.sameAs('email')
});

Both properties can be used in data-pac-bind expressions and behave identically — binding to either one produces the same result:

<input data-pac-bind="value: email">
<input data-pac-bind="value: confirm_email">

<p>Email:    {{email}}</p>
<p>Confirm:  {{confirm_email}}</p>

Writing to the alias writes through to the target, firing the normal reactive change event. Writing to the target is reflected when the alias is read:

wakaPAC('#signup', {
    email:         'a@example.com',
    confirm_email: wakaPAC.sameAs('email'),

    init() {
        this.confirm_email = 'b@example.com';
        console.log(this.email);         // "b@example.com"

        this.email = 'c@example.com';
        console.log(this.confirm_email); // "c@example.com"
    }
});

The target path supports dot notation for nested properties:

wakaPAC('#form', {
    form: {
        email: { value: '' }
    },
    email: wakaPAC.sameAs('form.email.value')
});

Aliases are resolved once at initialisation, before the reactive proxy is created. Avoid alias chains — always point directly to the real property:

// correct
a: '',
b: wakaPAC.sameAs('a'),
c: wakaPAC.sameAs('a')

// avoid
a: '',
b: wakaPAC.sameAs('a'),
c: wakaPAC.sameAs('b') // chain — b is itself an alias

List Rendering

foreach - Repeat Elements

The foreach binding repeats an element for each item in an array, automatically updating when the array changes. By default the current item is available as item and its position as $index (0-based):

<div id="app">
    <ul data-pac-bind="foreach: items">
        <li>{{$index + 1}}. {{item}}</li>
    </ul>
</div>

<script>
    wakaPAC('#app', {
        items: ['Apple', 'Banana', 'Cherry']
    });
</script>

Use data-pac-item to give the iteration variable a meaningful name, and data-pac-index to rename the index variable. Both are optional:

<div data-pac-bind="foreach: users" data-pac-item="user" data-pac-index="i">
    <p>{{i + 1}}. {{user.name}} — {{user.email}}</p>
</div>

The $ prefix on the default $index name prevents conflicts with your own data properties. Renamed index variables carry no prefix.

Nested foreach Loops

You can nest foreach loops for multi-dimensional data. Each loop has its own item and index context:

<div id="app">
    <div data-pac-bind="foreach: categories" data-pac-item="category">
        <h3>{{category.name}}</h3>
        <ul data-pac-bind="foreach: category.products" data-pac-item="product">
            <li>{{product.name}} - ${{product.price}}</li>
        </ul>
    </div>
</div>

<script>
    wakaPAC('#app', {
        categories: [
            {
                name: 'Electronics',
                products: [
                    { name: 'Laptop', price: 999 },
                    { name: 'Mouse', price: 25 }
                ]
            },
            {
                name: 'Books',
                products: [
                    { name: 'JavaScript Guide', price: 39 },
                    { name: 'Design Patterns', price: 45 }
                ]
            }
        ]
    });
</script>

placeholder - Default Option for a foreach select

A <select> with foreach can declare a placeholder binding to add a real, empty first option ("Choose one", "None", etc.) alongside the array-driven options — no extra wrapper element needed:

<select data-pac-bind="value: selectedId, foreach: users, placeholder: 'Choose a user'" data-pac-item="user">
    <option value="{{user.id}}">{{user.name}}</option>
</select>

The placeholder is a normal expression, evaluated the same way any other binding expression is, so it can depend on other data and stays correct as that data changes — not just when the array itself changes:

<div id="app">
    <select data-pac-bind="value: selectedId, foreach: users, placeholder: loading ? 'Loading…' : 'Choose a user'" data-pac-item="user">
        <option value="{{user.id}}">{{user.name}}</option>
    </select>
    <button data-pac-bind="click: finishLoading">Simulate load finishing</button>
</div>

<script>
    wakaPAC('#app', {
        loading: true,
        selectedId: '',
        users: [
            { id: 1, name: 'Alice' },
            { id: 2, name: 'Bob' }
        ],
        finishLoading() {
            this.loading = false;
        }
    });
</script>

The placeholder option always renders with an empty value="", so selectedId === '' is how you check whether a real option has been picked yet.