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.
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.
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:
Initialization is usually done once after the DOM is ready:
document.addEventListener('DOMContentLoaded', () => Rowio.init());
Rowio is configured entirely through data-* attributes. No JavaScript configuration objects are required. This keeps Rowio declarative, predictable, and framework-agnostic.
All configuration is intentionally explicit. Rowio never infers behavior from markup structure alone.
Rowio emits DOM events on the .rowio wrapper element. Each event is a CustomEvent and carries its payload in event.detail.
Most events expose these keys inside event.detail:
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})`);
});
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.
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.).