Rowio - a lightweight JavaScript library for building repeatable form rows Free

Repeatable form rows from a single <template> — no dependencies, multiple instances, hard reindex, copy-down, JSON prefill, and clean DOM events.

Rowio is a lightweight vanilla JavaScript library for building repeatable form rows.

Rowio is suitable for admin panels, SaaS back offices, and complex forms where users need to add/remove rows dynamically.

Initialize Rowio on any element with the .rowio CSS class and configure using data-* attributes on the wrapper element.

Initialization

Rowio.init();

Rowio is initialized manually by calling Rowio.init(). This gives you full control over when and where repeatable rows become active.

For Rowio to initialize successfully, the following HTML structure is required.

  • A wrapper element with the class .rowio
  • A <template> element with class .rowio-template
  • Inside the template, exactly one element with class .rowio-row
  • Inputs inside the template must use unindexed field names (Rowio will prefix and index them automatically)

Minimal example:

<div class="rowio" data-rowio-prefix="items">
  <template class="rowio-template">
    <div class="rowio-row">
      <input name="price">
    </div>
  </template>
</div>

When initialized, Rowio will:

  • Clone the template row
  • Insert rows into a .rowio-rows container
  • Automatically rewrite field names to prefix__field__index
  • Emit lifecycle events such as rowio:ready

Initialization is usually done once after the DOM is ready:
document.addEventListener('DOMContentLoaded', () => Rowio.init());

Configuration via Data Attributes

HTML-driven

Rowio is configured entirely through data-* attributes. No JavaScript configuration objects are required. This keeps Rowio declarative, predictable, and framework-agnostic.

Wrapper-level attributes (.rowio)
  • data-rowio-prefix
    Defines the row group prefix used in input names. Example output name: items__price__0
  • data-rowio-shown
    Number of rows rendered initially when no prefill data is present. Minimum is always 1.
  • data-rowio-max
    Maximum number of rows allowed. Use 0 or omit for unlimited rows.
  • data-rowio-copy-down
    Comma-separated list of field names that support copy-down behavior. Copy-down buttons appear on the first row only. Example: data-rowio-copy-down="price,tax"
  • data-rowio-copy-down-class
    Optional CSS classes applied to copy-down buttons. Useful for custom styling.
Template-level attributes (<template>)
  • data-rowio-key
    Overrides the instance key used for backend harvesting. When present, Rowio injects a hidden input mapping the prefix to this key. This allows backend grouping without renaming inputs.
Field-level attributes (inputs / editors)
  • data-rowio-default
    Default value applied when a row is created and no prefill data exists.
  • data-rowio-html
    Used on contenteditable="true" elements. When set to 1 or true, Rowio writes and reads raw HTML instead of text.

All configuration is intentionally explicit. Rowio never infers behavior from markup structure alone.

Events

CustomEvent

Rowio emits DOM events on the .rowio wrapper element. Each event is a CustomEvent and carries its payload in event.detail.

List of emitted events
  • rowio:ready
    Fired after the instance is initialized and initial rows are rendered. Payload contains instance and current rows snapshot.
  • rowio:row-add
    Fired after a new row is added and rows reindexed. Immediately followed by rowio:change.
  • rowio:row-remove
    Fired after a row is removed and remaining rows are reindexed. Immediately followed by rowio:change. Note: payload typically has row: null because the row is gone.
  • rowio:copy-down
    Fired after copy-down writes values into subsequent rows. Immediately followed by rowio:change.
  • rowio:change
    Fired after a user-visible mutation happens (add / remove / copy-down). Use this as the “recalculate / revalidate / re-init plugins” hook.
  • rowio:max-rows-reached
    Fired when a row add is attempted but rejected because data-rowio-max is reached. No mutation happens. No row is created.
Common payload shape

Most events expose these keys inside event.detail:

  • instance – the Rowio instance
  • row – the affected row element (or null)
  • index – row index (0-based) when applicable
  • fields – map of field name → element or array of elements (when applicable)
  • editors – list of WYSIWYG candidates in the row (when applicable)
Example usage

Listen on a wrapper and react to events:


    // wrapper = the .rowio element
    const wrapper = document.querySelector('.rowio');

    // Re-init plugins / validation after any structural change
    wrapper.addEventListener('rowio:change', (e) => {

        const { instance, row, index, fields, editors } = e.detail;

        // Example: init Select2 / Choices on the affected row only
        if (row) {
            // init_select2(row.querySelectorAll('.select2-select'));
            // init_choices(row.querySelectorAll('.choices-select'));
        }

        // Example: re-init WYSIWYG (Rowio does not do this automatically)
        // editors.forEach(el => tinymce_init(el));
    });

    // Show alert when maximum number of rows is reached
    wrapper.addEventListener('rowio:max-rows-reached', (e) => {
        const { max, rows_count } = e.detail;
        // alert(`Max rows reached (${rows_count}/${max})`);
    });
                    

1) Basic repeatable rows

add / remove / hard reindex

Template fields use bare names and optionally ids (e.g. opt_value, opt_label). Rowio rewrites them to prefix___field__0, prefix___field__1 etc.

In this case the prefix is enum, so the full field names become enum__opt_value__0, enum__opt_label__0, enum__opt_value__1, enum__opt_label__1 etc.

Try: Click the + and × buttons to add/remove rows.

2) Copy-down controls (field names)

data-rowio-copy-down
Copy-down buttons appear next to configured fields in the first row. Clicking copies value down to all subsequent rows.
Try: Set e-mail/VAT in first row by clicking the button. It should fill all next rows.

3) JSON prefill inside template + max rows

script.rowio-data + data-rowio-max
Rowio reads JSON from <script type="application/json" class="rowio-data"> inside the template and renders those rows. Max rows blocks adding beyond the limit.
max=6
Try: Copy-down VAT. Try adding rows until max is reached.

4) contenteditable="true" field (HTML mode)

contenteditable + data-rowio-html
This demonstrates how Rowio treats contenteditable="true" elements as value carriers.
With data-rowio-html="true", Rowio uses innerHTML (not textContent).
Security note:
Rowio does not sanitize HTML. If this content comes from users, sanitize on the server.

Event log (integration hooks)

Rowio emits events on the wrapper element.

This log shows the sequence you can hook into (e.g. init plugins, re-init editors, custom validation, etc.).

Rowio Free demo page • Bootstrap 5 • No dependencies