Repeaters
A list of the same few fields a person adds to and takes from: phone numbers, the lines of an invoice, the stops of a route. The page posts every row; the library adds and removes them.
Examples
The list carries data-repeat and holds a <template> of one row. A button with command="--add" naming the list in aria-controls adds a row made from it - every {i} in the row's attributes, its names, ids and for, turned into a running index that never repeats - and puts the focus in its first field. A row's command="--remove" button takes the row away; the focus goes to the next row's first field, else the previous row's, else to Add. data-min and data-max hold the count: a button that would pass them is aria-disabled. Like --toggle, the buttons take no commandfor.
<fieldset class="vstack gap-8">
<legend class="form-label">Phone numbers</legend>
<ul class="list-unstyled vstack gap-8" id="repeat-phones-list" data-repeat data-min="1" data-max="4">
<li class="hstack gap-8">
<label class="visually-hidden" for="repeat-phones-0">Phone</label>
<input class="form-control" id="repeat-phones-0" name="phones[0]" type="tel" value="+1 202 555 0143" autocomplete="off">
<button class="btn btn-icon btn-text" type="button" command="--remove" aria-label="Remove this phone"><i class="bi bi-trash" aria-hidden="true"></i></button>
</li>
<template>
<li class="hstack gap-8">
<label class="visually-hidden" for="repeat-phones-{i}">Another phone</label>
<input class="form-control" id="repeat-phones-{i}" name="phones[{i}]" type="tel" autocomplete="off">
<button class="btn btn-icon btn-text" type="button" command="--remove" aria-label="Remove this phone"><i class="bi bi-trash" aria-hidden="true"></i></button>
</li>
</template>
</ul>
<div><button class="btn btn-sm" type="button" command="--add" aria-controls="repeat-phones-list"><i class="bi bi-plus-lg" aria-hidden="true"></i>Add a phone</button></div>
</fieldset>The page's say
redz:add and redz:remove on the list come first, with the row in detail.row - the new one before it goes in, or the one about to go. A page that cancels one keeps the list as it is: it asks before a row with saved work goes, or fills the new row's fields first. Rows that carry a data-value also move with data-reorder.
document.getElementById('repeat-phones-list').addEventListener('redz:remove', (event) => {
if (event.detail.row.querySelector('input').value !== '' && !window.confirm('Remove this phone?')) {
event.preventDefault();
}
});
Keyboard and accessibility
- Add puts the focus in the new row's field and a removal puts it on the next row, so the person goes on typing without looking for it.
- Each row's field has its own label, hidden where the list's legend says enough; each remove button is named after what it takes away.
- A button that cannot act stays in the Tab order as
aria-disabled, its clicks swallowed, so a screen reader still finds it and hears why.