DOM Hydration

DOM Hydration lets WakaPAC initialize a component's reactive state directly from server-rendered HTML, without manually declaring properties in the abstraction. Fields marked with data-pac-field are scanned at startup and automatically become reactive properties.

explanation

Understanding DOM Hydration

Normally, properties must be declared in the abstraction before WakaPAC can bind to them:

wakaPAC('#app', {
    title: '',
    slug: '',
    status: 'draft'
});

With hydration enabled, WakaPAC reads the initial values from the DOM instead. The server renders the HTML with the correct values, and WakaPAC picks them up automatically:

<div data-pac-id="post-form">
    <input name="title" data-pac-field value="My First Post">
    <input name="slug" data-pac-field value="my-first-post">
    <select name="status" data-pac-field>
        <option value="draft" selected>Draft</option>
        <option value="published">Published</option>
    </select>
</div>

<script>
    wakaPAC('post-form', {
        save() {
            console.log(this.title, this.slug, this.status);
        }
    }, { hydrate: true });
</script>

The properties title, slug, and status are created automatically and are fully reactive — bindings, computed properties, and watchers all work as normal.

Note: Hydration populates the abstraction before the reactive proxy is created. This means hydrated properties are indistinguishable from manually declared ones — they participate in dependency tracking, computed properties, and two-way bindings from the start.

Enabling Hydration

Pass hydrate: true in the options object as the third argument to wakaPAC():

wakaPAC('my-component', {
    // abstraction methods
}, { hydrate: true });

Marking Fields

data-pac-field

Add data-pac-field to any form element that should be hydrated. The property name is taken from the element's name attribute. WakaPAC reads each field type appropriately:

  • text, textarea, select, hidden — hydrated as a string via el.value
  • checkbox — hydrated as a boolean from el.checked, not el.value
  • radio groups — the checked button's value is used; defaults to an empty string if none are selected
  • number, range — hydrated as a number; empty fields default to an empty string rather than NaN
  • file — excluded from hydration; file inputs cannot be meaningfully hydrated

Elements without a name attribute are silently ignored during the scan.

<div data-pac-id="my-form">
    <input name="title" data-pac-field value="Hello World">
    <input type="checkbox" name="active" data-pac-field checked>
    <input type="number" name="priority" data-pac-field value="3">

    <input type="radio" name="theme" data-pac-field value="light" checked>
    <input type="radio" name="theme" data-pac-field value="dark">
</div>

<script>
    wakaPAC('my-form', {
        init() {
            console.log(this.title);    // "Hello World" (string)
            console.log(this.active);   // true (boolean)
            console.log(this.priority); // 3 (number)
            console.log(this.theme);    // "light" (string)
        }
    }, { hydrate: true });
</script>

Bracket Notation

Field names using bracket notation are automatically mapped to nested reactive properties:

<input name="configuration[theme]" data-pac-field value="dark">
<input name="configuration[language]" data-pac-field value="en">
wakaPAC('my-form', {
    init() {
        console.log(this.configuration.theme);    // "dark"
        console.log(this.configuration.language); // "en"
    }
}, { hydrate: true });

Deeper nesting is supported as well:

<input name="configuration[section][title]" data-pac-field value="Hello">

Nested Components

Hydration only scans fields that directly belong to the current component. Fields inside a nested WakaPAC component (identified by their own data-pac-id) are skipped — they belong to that child component, not the parent:

<div data-pac-id="post-form">
    <input name="title" data-pac-field value="My Post">        <!-- hydrated -->

    <div data-pac-id="rich-editor">
        <input name="internal" data-pac-field value="...">     <!-- skipped -->
    </div>
</div>

Using Hydrated Properties

Two-Way Binding

Hydrated properties work with data-pac-bind just like manually declared ones:

<div data-pac-id="post-form">
    <input name="title" data-pac-field value="My First Post"
           data-pac-bind="value: title">

    <p>Preview: {{title}}</p>

    <button data-pac-bind="click: save">Save</button>
</div>

<script>
    wakaPAC('post-form', {
        save() {
            console.log(this.title, this.slug);
        }
    }, { hydrate: true });
</script>

Computed Properties

Computed properties can depend on hydrated fields:

<div data-pac-id="post-form">
    <input name="title" data-pac-field value="My First Post"
           data-pac-bind="value: title">
    <input name="slug" data-pac-field value=""
           data-pac-bind="value: slug">

    <p>Slug preview: {{autoSlug}}</p>
</div>

<script>
    wakaPAC('post-form', {
        computed: {
            autoSlug() {
                return this.title.toLowerCase().replace(/\s+/g, '-');
            }
        }
    }, { hydrate: true });
</script>

Server-Side State

Beyond field values, a server-rendered component often needs to provide additional state — such as dropdown options, lookup tables, or configuration — that is not tied to a specific form field. This can be passed via the data-pac-state attribute as a JSON object.

When hydrate: true is set, WakaPAC automatically reads data-pac-state and merges it into the component's initial state before the reactive proxy is created:

<div data-pac-id="my-form"
     data-pac-state='{"roles": [{"value": "admin", "label": "Admin"}, {"value": "editor", "label": "Editor"}]}'>
    <input name="username" data-pac-field value="alice">
    <select name="role" data-pac-field data-pac-bind="foreach: roles, value: role">
        <option data-pac-bind="value: item.value">{{item.label}}</option>
    </select>
</div>

<script>
    wakaPAC('my-form', {}, { hydrate: true });
</script>

The merged state follows this order of precedence:

  1. data-pac-state is applied first
  2. data-pac-field values are applied second — scalar field values always override state attribute values
  3. Properties declared in the abstraction object take final precedence
Note: data-pac-state is only read when hydrate: true is set. Without hydration, the attribute is ignored.

Complex Field Types

Some field types — rich text editors, image uploaders, relation pickers — are too complex to be represented as a plain HTML input and cannot be hydrated automatically. How you handle these is up to you. One approach is a nested WakaPAC component that communicates its value to the parent via sendMessage:

const MSG_FIELD_CHANGE = wakaPAC.MSG_USER + 1;

// Child component sends its value up when it changes
wakaPAC('body-editor', {
    onChange(value) {
        wakaPAC.sendMessageToParent('body-editor', MSG_FIELD_CHANGE, 0, 0, { name: 'body', value });
    }
});

// Parent listens and stores the value
wakaPAC('post-form', {
    msgProc(event) {
        if (event.message === MSG_FIELD_CHANGE) {
            this[event.detail.name] = event.detail.value;
        }
    }
}, { hydrate: true });