Rowio is a lightweight, framework-agnostic JavaScript library for building repeatable form rows using the native <template> element.
It solves one specific problem:
Safely adding, removing, reindexing, and harvesting repeated form rows without magic.
Rowio Free focuses purely on frontend behavior. Backend helpers, validation, and calculations are provided by Rowio Pro.
- Vanilla JavaScript (no dependencies)
- Multiple Rowio instances per page
<template>-based row source- Hard remove + full reindex (names, ids, labels)
- Optional JSON prefill
- Copy-down controls (field-based)
- Explicit DOM events for integrations
- Works with any backend or framework
Download or include the script:
<script src="rowio.js"></script>No build step required.
A working Rowio instance consists of:
- A wrapper with class
.rowio - A
<template class="rowio-template">containing one.rowio-row - An optional
.rowio-rowscontainer (auto-created if missing) - Controls with
.rowio-addand.rowio-remove
<div class="rowio" data-rowio-prefix="items">
<template class="rowio-template">
<div class="rowio-row row gx-2">
<div class="col">
<input type="text" name="name" class="form-control" placeholder="Item name">
</div>
<div class="col">
<input type="number" name="qty" class="form-control" placeholder="Qty">
</div>
<div class="col-auto">
<button type="button" class="btn btn-danger rowio-remove">×</button>
</div>
</div>
</template>
<button type="button" class="btn btn-primary rowio-add">Add row</button>
</div>- Fields inside the template must not be indexed
- Rowio will automatically convert them to:
items__name__0
items__qty__0
items__name__1
items__qty__1
...
Initialization is manual by design.
<script>
document.addEventListener('DOMContentLoaded', () => {
Rowio.init();
});
</script>You can also target a specific element or selector:
Rowio.init('.rowio');
Rowio.init(document.querySelector('.rowio'));
Rowio.init(document.querySelectorAll('.rowio'));Configured on the .rowio wrapper:
data-rowio-prefix– required, input name prefixdata-rowio-shown– initial number of rows (default: 1)data-rowio-max– maximum allowed rows (0 = unlimited)data-rowio-copy-down– comma-separated field namesdata-rowio-copy-down-class– CSS classes for copy-down buttons
Optional on <template>:
data-rowio-key– override instance key
Optional on inputs / editors (<input>, <select>, <textarea> or contenteditable="true"):
data-rowio-default- default value for new rowsdata-rowio-html- useinnerHTMLinstead oftextContent(forcontenteditable="true")
Rows can be prefilled by embedding JSON inside the template:
<template class="rowio-template">
<script type="application/json" class="rowio-data">
[
{ "name": "Apple", "qty": 2 },
{ "name": "Banana", "qty": 5 }
]
</script>
<!-- row markup -->
</template>
⚠️ Rowio does not sanitize prefill data.
Rowio emits CustomEvents on the wrapper element:
rowio:ready- after initializationrowio:row-add- after a row is added and rows reindexedrowio:row-remove- after a row is removed and rows reindexedrowio:copy-down- after a copy-down actionrowio:change- after any change (add, remove, copy-down)rowio:max-rows-reached- when trying to exceed max rows
Example:
wrapper.addEventListener('rowio:change', e => {
const { row, index, fields } = e.detail;
// re-init plugins, recalc totals, validate, etc.
});Rowio Free only guarantees correct naming and indexing.
Backend parsing, validation, normalization, and calculations are handled by Rowio Pro.
- Modern evergreen browsers
- Requires
<template>support
MIT License
- Rowio Pro – extended by validation, calculations and backend helpers
Rowio is intentionally boring.
It does not guess, auto-submit, validate, or sanitize. It only guarantees one thing:
Repeatable rows that stay structurally correct.
Everything else is your choice.