Data input
Repeater
Rows of fields that a reader adds and removes, within a minimum and a maximum, numbered in order for the form.
| Class name | Type | Description |
|---|---|---|
repeaterplf:repeater | Component | Holds the rows, their template and the add button |
repeater-listplf:repeater-list | Part | The list of rows |
repeater-rowplf:repeater-row | Part | A row: its fields share the width, then its buttons |
Default
Name a row’s fields with [] in the template, such as emails[] or links[][url]; the script numbers the rows in order.
<fieldset class="repeater" data-plf-repeater data-plf-max="5">
<legend class="field-legend">Email addresses</legend>
<p class="field-help">Add up to 5 addresses where we can reach you.</p>
<ol class="repeater-list">
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="emails[0]" type="email" dir="ltr" autocomplete="email" value="sam@example.test" aria-label="Email">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove email" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
</ol>
<template>
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="emails[]" type="email" dir="ltr" autocomplete="email" aria-label="Email">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove email" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
</template>
<button class="btn btn-outline btn-sm" data-plf-repeater-add hidden>Add email address</button>
</fieldset><fieldset class="plf:repeater" data-plf-repeater data-plf-max="5">
<legend class="plf:field-legend">Email addresses</legend>
<p class="plf:field-help">Add up to 5 addresses where we can reach you.</p>
<ol class="plf:repeater-list">
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="emails[0]" type="email" dir="ltr" autocomplete="email" value="sam@example.test" aria-label="Email">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove email" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
</ol>
<template>
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="emails[]" type="email" dir="ltr" autocomplete="email" aria-label="Email">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove email" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
</template>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-repeater-add hidden>Add email address</button>
</fieldset>'use client';
import { Icon, InputGroup, InputGroupAction, InputGroupControl, Repeater } from '@pelatform/react';
export function Example() {
return (
<Repeater
legend="Email addresses"
help="Add up to 5 addresses where we can reach you."
max={5}
defaultItems={['sam@example.test']}
createItem={() => ''}
addLabel="Add email address"
>
{(email, { index, fieldId, removeProps }) => (
<InputGroup>
<InputGroupControl id={fieldId} name={`emails[${index}]`} type="email" dir="ltr" autoComplete="email" defaultValue={email} aria-label="Email" />
<InputGroupAction {...removeProps} aria-label="Remove email">
<Icon name="x" />
</InputGroupAction>
</InputGroup>
)}
</Repeater>
);
}'use client';
import { Icon, InputGroup, InputGroupAction, InputGroupControl, Repeater } from '@pelatform/react/prefixed';
export function Example() {
return (
<Repeater
legend="Email addresses"
help="Add up to 5 addresses where we can reach you."
max={5}
defaultItems={['sam@example.test']}
createItem={() => ''}
addLabel="Add email address"
>
{(email, { index, fieldId, removeProps }) => (
<InputGroup>
<InputGroupControl id={fieldId} name={`emails[${index}]`} type="email" dir="ltr" autoComplete="email" defaultValue={email} aria-label="Email" />
<InputGroupAction {...removeProps} aria-label="Remove email">
<Icon name="x" />
</InputGroupAction>
</InputGroup>
)}
</Repeater>
);
}Several fields
Write the template’s ids as for one field; each new row takes its own, and its labels follow.
<fieldset class="repeater" data-plf-repeater>
<legend class="field-legend">Links</legend>
<ol class="repeater-list">
<li class="repeater-row">
<div class="field">
<label class="field-label" for="repeater-link-title">Title</label>
<input class="input" id="repeater-link-title" name="links[0][title]" value="Documentation">
</div>
<div class="field">
<label class="field-label" for="repeater-link-url">URL</label>
<input class="input" id="repeater-link-url" name="links[0][url]" type="url" dir="ltr" value="https://example.test/docs">
</div>
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove link" hidden>
<span class="icon icon-x"></span>
</button>
</li>
</ol>
<template>
<li class="repeater-row">
<div class="field">
<label class="field-label" for="repeater-new-title">Title</label>
<input class="input" id="repeater-new-title" name="links[][title]">
</div>
<div class="field">
<label class="field-label" for="repeater-new-url">URL</label>
<input class="input" id="repeater-new-url" name="links[][url]" type="url" dir="ltr">
</div>
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove link" hidden>
<span class="icon icon-x"></span>
</button>
</li>
</template>
<button class="btn btn-outline btn-sm" data-plf-repeater-add hidden>Add link</button>
</fieldset><fieldset class="plf:repeater" data-plf-repeater>
<legend class="plf:field-legend">Links</legend>
<ol class="plf:repeater-list">
<li class="plf:repeater-row">
<div class="plf:field">
<label class="plf:field-label" for="repeater-link-title">Title</label>
<input class="plf:input" id="repeater-link-title" name="links[0][title]" value="Documentation">
</div>
<div class="plf:field">
<label class="plf:field-label" for="repeater-link-url">URL</label>
<input class="plf:input" id="repeater-link-url" name="links[0][url]" type="url" dir="ltr" value="https://example.test/docs">
</div>
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove link" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
</ol>
<template>
<li class="plf:repeater-row">
<div class="plf:field">
<label class="plf:field-label" for="repeater-new-title">Title</label>
<input class="plf:input" id="repeater-new-title" name="links[][title]">
</div>
<div class="plf:field">
<label class="plf:field-label" for="repeater-new-url">URL</label>
<input class="plf:input" id="repeater-new-url" name="links[][url]" type="url" dir="ltr">
</div>
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove link" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
</template>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-repeater-add hidden>Add link</button>
</fieldset>'use client';
import { Button, Field, Icon, Input, Repeater } from '@pelatform/react';
export function Example() {
return (
<Repeater
legend="Links"
defaultItems={[{ title: 'Documentation', url: 'https://example.test/docs' }]}
createItem={() => ({ title: '', url: '' })}
addLabel="Add link"
>
{(link, { index, fieldId, removeProps }) => (
<>
<Field label="Title" id={fieldId}>
<Input name={`links[${index}][title]`} defaultValue={link.title} />
</Field>
<Field label="URL">
<Input name={`links[${index}][url]`} type="url" dir="ltr" defaultValue={link.url} />
</Field>
<Button variant="ghost" shape="square" {...removeProps} aria-label="Remove link">
<Icon name="x" />
</Button>
</>
)}
</Repeater>
);
}'use client';
import { Button, Field, Icon, Input, Repeater } from '@pelatform/react/prefixed';
export function Example() {
return (
<Repeater
legend="Links"
defaultItems={[{ title: 'Documentation', url: 'https://example.test/docs' }]}
createItem={() => ({ title: '', url: '' })}
addLabel="Add link"
>
{(link, { index, fieldId, removeProps }) => (
<>
<Field label="Title" id={fieldId}>
<Input name={`links[${index}][title]`} defaultValue={link.title} />
</Field>
<Field label="URL">
<Input name={`links[${index}][url]`} type="url" dir="ltr" defaultValue={link.url} />
</Field>
<Button variant="ghost" shape="square" {...removeProps} aria-label="Remove link">
<Icon name="x" />
</Button>
</>
)}
</Repeater>
);
}Limits
The remove buttons are disabled at the minimum and the add button at the maximum.
<fieldset class="repeater" data-plf-repeater data-plf-min="1" data-plf-max="3">
<legend class="field-legend">Allowed domains</legend>
<p class="field-help">At least one and up to three domains.</p>
<ol class="repeater-list">
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="domains[0]" dir="ltr" value="example.test" aria-label="Domain">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove domain" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
</ol>
<template>
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="domains[]" dir="ltr" aria-label="Domain">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove domain" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
</template>
<button class="btn btn-outline btn-sm" data-plf-repeater-add hidden>Add domain</button>
</fieldset><fieldset class="plf:repeater" data-plf-repeater data-plf-min="1" data-plf-max="3">
<legend class="plf:field-legend">Allowed domains</legend>
<p class="plf:field-help">At least one and up to three domains.</p>
<ol class="plf:repeater-list">
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="domains[0]" dir="ltr" value="example.test" aria-label="Domain">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove domain" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
</ol>
<template>
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="domains[]" dir="ltr" aria-label="Domain">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove domain" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
</template>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-repeater-add hidden>Add domain</button>
</fieldset>'use client';
import { Icon, InputGroup, InputGroupAction, InputGroupControl, Repeater } from '@pelatform/react';
export function Example() {
return (
<Repeater
legend="Allowed domains"
help="At least one and up to three domains."
min={1}
max={3}
defaultItems={['example.test']}
createItem={() => ''}
addLabel="Add domain"
>
{(domain, { index, fieldId, removeProps }) => (
<InputGroup>
<InputGroupControl id={fieldId} name={`domains[${index}]`} dir="ltr" defaultValue={domain} aria-label="Domain" />
<InputGroupAction {...removeProps} aria-label="Remove domain">
<Icon name="x" />
</InputGroupAction>
</InputGroup>
)}
</Repeater>
);
}'use client';
import { Icon, InputGroup, InputGroupAction, InputGroupControl, Repeater } from '@pelatform/react/prefixed';
export function Example() {
return (
<Repeater
legend="Allowed domains"
help="At least one and up to three domains."
min={1}
max={3}
defaultItems={['example.test']}
createItem={() => ''}
addLabel="Add domain"
>
{(domain, { index, fieldId, removeProps }) => (
<InputGroup>
<InputGroupControl id={fieldId} name={`domains[${index}]`} dir="ltr" defaultValue={domain} aria-label="Domain" />
<InputGroupAction {...removeProps} aria-label="Remove domain">
<Icon name="x" />
</InputGroupAction>
</InputGroup>
)}
</Repeater>
);
}<fieldset class="repeater" data-plf-repeater>
<legend class="field-legend">Onboarding steps</legend>
<ol class="sortable" data-plf-sortable aria-label="Onboarding steps">
<li class="sortable-item">
<button class="sortable-handle" data-plf-sortable-handle aria-label="Reorder step"></button>
<label class="sr-only" for="repeater-step-one" data-plf-sortable-name>Step</label>
<input class="input flex-1" id="repeater-step-one" name="steps[0]" value="Create a workspace">
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove step" hidden>
<span class="icon icon-x"></span>
</button>
</li>
<li class="sortable-item">
<button class="sortable-handle" data-plf-sortable-handle aria-label="Reorder step"></button>
<label class="sr-only" for="repeater-step-two" data-plf-sortable-name>Step</label>
<input class="input flex-1" id="repeater-step-two" name="steps[1]" value="Invite your team">
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove step" hidden>
<span class="icon icon-x"></span>
</button>
</li>
</ol>
<template>
<li class="sortable-item">
<button class="sortable-handle" data-plf-sortable-handle aria-label="Reorder step"></button>
<label class="sr-only" for="repeater-step-new" data-plf-sortable-name>Step</label>
<input class="input flex-1" id="repeater-step-new" name="steps[]">
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove step" hidden>
<span class="icon icon-x"></span>
</button>
</li>
</template>
<button class="btn btn-outline btn-sm" data-plf-repeater-add hidden>Add step</button>
</fieldset><fieldset class="plf:repeater" data-plf-repeater>
<legend class="plf:field-legend">Onboarding steps</legend>
<ol class="plf:sortable" data-plf-sortable aria-label="Onboarding steps">
<li class="plf:sortable-item">
<button class="plf:sortable-handle" data-plf-sortable-handle aria-label="Reorder step"></button>
<label class="plf:sr-only" for="repeater-step-one" data-plf-sortable-name>Step</label>
<input class="plf:input plf:flex-1" id="repeater-step-one" name="steps[0]" value="Create a workspace">
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove step" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
<li class="plf:sortable-item">
<button class="plf:sortable-handle" data-plf-sortable-handle aria-label="Reorder step"></button>
<label class="plf:sr-only" for="repeater-step-two" data-plf-sortable-name>Step</label>
<input class="plf:input plf:flex-1" id="repeater-step-two" name="steps[1]" value="Invite your team">
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove step" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
</ol>
<template>
<li class="plf:sortable-item">
<button class="plf:sortable-handle" data-plf-sortable-handle aria-label="Reorder step"></button>
<label class="plf:sr-only" for="repeater-step-new" data-plf-sortable-name>Step</label>
<input class="plf:input plf:flex-1" id="repeater-step-new" name="steps[]">
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove step" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
</template>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-repeater-add hidden>Add step</button>
</fieldset>Conditional field in a row
A condition in the template names its row’s field with [] too, and follows the row’s number.
<fieldset class="repeater" data-plf-repeater>
<legend class="field-legend">Notification channels</legend>
<ol class="repeater-list">
<li class="repeater-row">
<div class="field">
<label class="field-label" for="repeater-channel-type">Channel</label>
<select class="select" id="repeater-channel-type" name="channels[0][type]">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div class="field" data-plf-show-if="channels[0][type]=email">
<label class="field-label" for="repeater-channel-email">Email</label>
<input class="input" id="repeater-channel-email" name="channels[0][email]" type="email" dir="ltr" value="ops@example.test">
</div>
<div class="field" data-plf-show-if="channels[0][type]=webhook">
<label class="field-label" for="repeater-channel-url">URL</label>
<input class="input" id="repeater-channel-url" name="channels[0][url]" type="url" dir="ltr">
</div>
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove channel" hidden>
<span class="icon icon-x"></span>
</button>
</li>
</ol>
<template>
<li class="repeater-row">
<div class="field">
<label class="field-label" for="repeater-new-type">Channel</label>
<select class="select" id="repeater-new-type" name="channels[][type]">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div class="field" data-plf-show-if="channels[][type]=email">
<label class="field-label" for="repeater-new-email">Email</label>
<input class="input" id="repeater-new-email" name="channels[][email]" type="email" dir="ltr">
</div>
<div class="field" data-plf-show-if="channels[][type]=webhook">
<label class="field-label" for="repeater-new-url">URL</label>
<input class="input" id="repeater-new-url" name="channels[][url]" type="url" dir="ltr">
</div>
<button class="btn btn-ghost btn-square" data-plf-repeater-remove aria-label="Remove channel" hidden>
<span class="icon icon-x"></span>
</button>
</li>
</template>
<button class="btn btn-outline btn-sm" data-plf-repeater-add hidden>Add channel</button>
</fieldset><fieldset class="plf:repeater" data-plf-repeater>
<legend class="plf:field-legend">Notification channels</legend>
<ol class="plf:repeater-list">
<li class="plf:repeater-row">
<div class="plf:field">
<label class="plf:field-label" for="repeater-channel-type">Channel</label>
<select class="plf:select" id="repeater-channel-type" name="channels[0][type]">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div class="plf:field" data-plf-show-if="channels[0][type]=email">
<label class="plf:field-label" for="repeater-channel-email">Email</label>
<input class="plf:input" id="repeater-channel-email" name="channels[0][email]" type="email" dir="ltr" value="ops@example.test">
</div>
<div class="plf:field" data-plf-show-if="channels[0][type]=webhook">
<label class="plf:field-label" for="repeater-channel-url">URL</label>
<input class="plf:input" id="repeater-channel-url" name="channels[0][url]" type="url" dir="ltr">
</div>
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove channel" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
</ol>
<template>
<li class="plf:repeater-row">
<div class="plf:field">
<label class="plf:field-label" for="repeater-new-type">Channel</label>
<select class="plf:select" id="repeater-new-type" name="channels[][type]">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div class="plf:field" data-plf-show-if="channels[][type]=email">
<label class="plf:field-label" for="repeater-new-email">Email</label>
<input class="plf:input" id="repeater-new-email" name="channels[][email]" type="email" dir="ltr">
</div>
<div class="plf:field" data-plf-show-if="channels[][type]=webhook">
<label class="plf:field-label" for="repeater-new-url">URL</label>
<input class="plf:input" id="repeater-new-url" name="channels[][url]" type="url" dir="ltr">
</div>
<button class="plf:btn plf:btn-ghost plf:btn-square" data-plf-repeater-remove aria-label="Remove channel" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</li>
</template>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-repeater-add hidden>Add channel</button>
</fieldset>'use client';
import { useState } from 'react';
import { Button, Field, Icon, Input, Repeater, Select } from '@pelatform/react';
export function Example() {
const [channels, setChannels] = useState([{ type: 'email', email: 'ops@example.test' }]);
const setType = (index: number, type: string) => setChannels(channels.map((channel, at) => (at === index ? { ...channel, type } : channel)));
return (
<Repeater
legend="Notification channels"
items={channels}
onItemsChange={setChannels}
createItem={() => ({ type: 'email', email: '' })}
addLabel="Add channel"
>
{(channel, { index, fieldId, removeProps }) => (
<>
<Field label="Channel" id={fieldId}>
<Select name={`channels[${index}][type]`} value={channel.type} onChange={event => setType(index, event.target.value)}>
<option value="email">Email</option>
<option value="webhook">Webhook</option>
</Select>
</Field>
{channel.type === 'email' ? (
<Field label="Email">
<Input name={`channels[${index}][email]`} type="email" dir="ltr" defaultValue={channel.email} />
</Field>
) : (
<Field label="URL">
<Input name={`channels[${index}][url]`} type="url" dir="ltr" />
</Field>
)}
<Button variant="ghost" shape="square" {...removeProps} aria-label="Remove channel">
<Icon name="x" />
</Button>
</>
)}
</Repeater>
);
}'use client';
import { useState } from 'react';
import { Button, Field, Icon, Input, Repeater, Select } from '@pelatform/react/prefixed';
export function Example() {
const [channels, setChannels] = useState([{ type: 'email', email: 'ops@example.test' }]);
const setType = (index: number, type: string) => setChannels(channels.map((channel, at) => (at === index ? { ...channel, type } : channel)));
return (
<Repeater
legend="Notification channels"
items={channels}
onItemsChange={setChannels}
createItem={() => ({ type: 'email', email: '' })}
addLabel="Add channel"
>
{(channel, { index, fieldId, removeProps }) => (
<>
<Field label="Channel" id={fieldId}>
<Select name={`channels[${index}][type]`} value={channel.type} onChange={event => setType(index, event.target.value)}>
<option value="email">Email</option>
<option value="webhook">Webhook</option>
</Select>
</Field>
{channel.type === 'email' ? (
<Field label="Email">
<Input name={`channels[${index}][email]`} type="email" dir="ltr" defaultValue={channel.email} />
</Field>
) : (
<Field label="URL">
<Input name={`channels[${index}][url]`} type="url" dir="ltr" />
</Field>
)}
<Button variant="ghost" shape="square" {...removeProps} aria-label="Remove channel">
<Icon name="x" />
</Button>
</>
)}
</Repeater>
);
}Confirm removal
Your script cancels the removal, asks with PLF.Dialog.fire() and removes the row with instance.remove(row), whose reason is api.
<fieldset class="repeater" id="repeater-members" data-plf-repeater>
<legend class="field-legend">Team members</legend>
<ol class="repeater-list">
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="members[0]" type="email" dir="ltr" value="ana@example.test" aria-label="Member email">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove member" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="members[1]" type="email" dir="ltr" value="jo@example.test" aria-label="Member email">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove member" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
</ol>
<template>
<li class="repeater-row">
<div class="input-group">
<input class="input-group-control" name="members[]" type="email" dir="ltr" aria-label="Member email">
<button class="input-group-action" data-plf-repeater-remove aria-label="Remove member" hidden>
<span class="icon icon-x"></span>
</button>
</div>
</li>
</template>
<button class="btn btn-outline btn-sm" data-plf-repeater-add hidden>Add member</button>
</fieldset>
<script>
document.getElementById('repeater-members').addEventListener('plf:repeater:before-remove', async event => {
// The confirmed removal below comes from instance.remove(), so its reason is api and it goes ahead.
if (event.detail.reason === 'api') return;
event.preventDefault();
const { instance, row } = event.detail;
const result = await PLF.Dialog.fire({
title: 'Remove this member?',
text: 'They lose access to the workspace when you save.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Remove',
confirmButtonColor: 'error',
});
if (result.isConfirmed) instance.remove(row);
});
</script><fieldset class="plf:repeater" id="repeater-members" data-plf-repeater>
<legend class="plf:field-legend">Team members</legend>
<ol class="plf:repeater-list">
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="members[0]" type="email" dir="ltr" value="ana@example.test" aria-label="Member email">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove member" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="members[1]" type="email" dir="ltr" value="jo@example.test" aria-label="Member email">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove member" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
</ol>
<template>
<li class="plf:repeater-row">
<div class="plf:input-group">
<input class="plf:input-group-control" name="members[]" type="email" dir="ltr" aria-label="Member email">
<button class="plf:input-group-action" data-plf-repeater-remove aria-label="Remove member" hidden>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
</li>
</template>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-repeater-add hidden>Add member</button>
</fieldset>
<script>
document.getElementById('repeater-members').addEventListener('plf:repeater:before-remove', async event => {
// The confirmed removal below comes from instance.remove(), so its reason is api and it goes ahead.
if (event.detail.reason === 'api') return;
event.preventDefault();
const { instance, row } = event.detail;
const result = await PLF.Dialog.fire({
title: 'Remove this member?',
text: 'They lose access to the workspace when you save.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Remove',
confirmButtonColor: 'error',
});
if (result.isConfirmed) instance.remove(row);
});
</script>'use client';
import { Icon, InputGroup, InputGroupAction, InputGroupControl, Repeater, useDialog } from '@pelatform/react';
export function Example() {
const [fire, dialog] = useDialog();
return (
<>
<Repeater legend="Team members" defaultItems={['ana@example.test', 'jo@example.test']} createItem={() => ''} addLabel="Add member">
{(member, { index, fieldId, removeProps }) => (
<InputGroup>
<InputGroupControl id={fieldId} name={`members[${index}]`} type="email" dir="ltr" defaultValue={member} aria-label="Member email" />
<InputGroupAction
{...removeProps}
aria-label="Remove member"
onClick={async () => {
const result = await fire({
title: 'Remove this member?',
text: 'They lose access to the workspace when you save.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Remove',
confirmButtonColor: 'error',
});
if (result.isConfirmed) removeProps.onClick();
}}
>
<Icon name="x" />
</InputGroupAction>
</InputGroup>
)}
</Repeater>
{dialog}
</>
);
}'use client';
import { Icon, InputGroup, InputGroupAction, InputGroupControl, Repeater, useDialog } from '@pelatform/react/prefixed';
export function Example() {
const [fire, dialog] = useDialog();
return (
<>
<Repeater legend="Team members" defaultItems={['ana@example.test', 'jo@example.test']} createItem={() => ''} addLabel="Add member">
{(member, { index, fieldId, removeProps }) => (
<InputGroup>
<InputGroupControl id={fieldId} name={`members[${index}]`} type="email" dir="ltr" defaultValue={member} aria-label="Member email" />
<InputGroupAction
{...removeProps}
aria-label="Remove member"
onClick={async () => {
const result = await fire({
title: 'Remove this member?',
text: 'They lose access to the workspace when you save.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Remove',
confirmButtonColor: 'error',
});
if (result.isConfirmed) removeProps.onClick();
}}
>
<Icon name="x" />
</InputGroupAction>
</InputGroup>
)}
</Repeater>
{dialog}
</>
);
}JavaScript
The add and remove buttons need JavaScript, so author them hidden; without it the rows the page loaded still submit. The Full script (pelatform-ui.min.js) and the extension scripts after it start every component on page load, with no code of your own; with Core and single files, the last line starts them. For markup you add later, see Dynamic content.
<link rel="stylesheet" href="pelatform-ui.min.css">
<link rel="stylesheet" href="extensions/sortable.min.css">
<link rel="stylesheet" href="icons/icons.min.css">
<script src="pelatform-ui.min.js" defer></script>
<script src="extensions/sortable.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.min.css">
<link rel="stylesheet" href="components/button.min.css">
<link rel="stylesheet" href="components/form.min.css">
<link rel="stylesheet" href="components/repeater.min.css">
<link rel="stylesheet" href="extensions/sortable.min.css">
<link rel="stylesheet" href="icons/icons.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/field.min.js" defer></script>
<script src="components/repeater.min.js" defer></script>
<script src="extensions/sortable.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script><link rel="stylesheet" href="pelatform-ui.prefixed.min.css">
<link rel="stylesheet" href="extensions/sortable.prefixed.min.css">
<link rel="stylesheet" href="icons/icons.prefixed.min.css">
<script src="pelatform-ui.min.js" defer></script>
<script src="extensions/sortable.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.prefixed.min.css">
<link rel="stylesheet" href="components/button.prefixed.min.css">
<link rel="stylesheet" href="components/form.prefixed.min.css">
<link rel="stylesheet" href="components/repeater.prefixed.min.css">
<link rel="stylesheet" href="extensions/sortable.prefixed.min.css">
<link rel="stylesheet" href="icons/icons.prefixed.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/field.min.js" defer></script>
<script src="components/repeater.min.js" defer></script>
<script src="extensions/sortable.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script>| Attribute | Purpose |
|---|---|
data-plf-repeater |
The container: one <ol> or <ul> of <li> rows, one <template> holding one <li> row, and its add buttons. The template names its fields with [] after one base, such as links[][url]. After each addition, removal and move, the script numbers every row’s names in order (links[0][url]), and gives a new row’s ids their own values, which its for and aria-* references follow |
data-plf-min, data-plf-max |
On the container: the fewest and the most rows, whole numbers; 0 and no limit by default. The remove buttons are disabled at the minimum and the add buttons at the maximum; the script adds no rows by itself |
data-plf-repeater-add |
On a button outside the rows: adds a row from the template at the end and focuses its first field |
data-plf-repeater-remove |
On a button in a row: removes the row and focuses the row now in its place, the one before it or an add button. The script describes it by its row’s first field, so a screen reader hears that field’s value after the button’s name. In a form, write type="button" on every add and remove button, the template’s included |
data-plf-sortable |
On the <ol> or <ul>, with the Sortable extension: rows move by their handle once the list holds two; the script gives each row a data-plf-value it lacks |
data-plf-show-if |
In a row, a condition that names its row’s field with [] in the template, such as links[][type]=custom |
| Method | Behavior |
|---|---|
PLF. |
Returns the instance, creating it for valid markup |
PLF. |
Returns the instance or null |
instance. |
Adds a row at the end and returns it, without moving focus; null at the maximum or when canceled |
instance. |
Removes one of its rows; false at the minimum, for another element or when canceled |
instance. |
Returns the rows, in order |
instance. |
Removes the listeners and hides the buttons again; the rows and their values stay |
With ESM, import Repeater from @pelatform/ui/components/repeater.
Events
| Event | When |
|---|---|
plf: |
Before a row is added; cancelable. Its row is not in the page yet |
plf: |
After the row is in the page, numbered, and its components have started |
plf: |
Before a row is removed; cancelable, such as to ask for confirmation |
plf: |
After the row has left the page and the others are numbered again |
Each event’s detail:
| Property | Value |
|---|---|
instance |
The Repeater instance |
reason |
trigger, or api from a method |
trigger |
The add or remove button, or null from a method |
row |
The row added or removed |
index |
The row’s zero-based position |
Events bubble from the container. A form reset brings back the rows the page loaded, in their order and with their values, and sends no events.
Keyboard
| Key | Action |
|---|---|
| Tab, Shift+Tab | Move through the rows’ fields and buttons, then the add button |
| Enter, Space | On an add or remove button, adds or removes a row |
| Space, Enter, then the arrow keys | On a sortable row’s handle, move the row, as in Sortable |
React
Repeater from @pelatform/react renders the fieldset, its rows and the add button, and its function child renders each row’s content; it keeps each row’s React key, disables the buttons at min and max, and moves focus as the script does. As its child and createItem are functions, the component that renders it is a client component ('use client').
| Prop | Values | Default |
|---|---|---|
legend |
The legend (field-legend), which names the rows |
None |
help |
Help text after the legend (field-help) |
None |
items |
The rows’ items, which the application changes in onItemsChange |
Its own state |
defaultItems |
The items it starts with when the application does not set items |
No rows |
onItemsChange |
(items), called with the new items when a button adds or removes a row |
None |
createItem |
(), which returns the item of a row the add button adds |
Required |
min |
The fewest rows, at which the remove buttons are disabled | 0 |
max |
The most rows, at which the add button is disabled | No limit |
addLabel |
The add button’s text | Required |
children |
(item, row), which renders a row’s content |
Required |
The child’s row:
| Property | Value |
|---|---|
index |
The row’s position from 0, which numbers its fields’ names, as emails[${index}] |
fieldId |
The id for the row’s first field, on its control or its Field; it describes the remove button, so a screen reader hears that field’s value after the button’s name |
removeProps |
The remove button’s props, spread on a Button or an InputGroupAction: type="button", aria-describedby, disabled at min and the click that removes the row |
The other props are a fieldset’s, className and ref included. Without items, a form reset brings back the rows it started with.
CSS variables
Override on a root or on a single repeater.
| Variable | Default |
|---|---|
--plf-repeater-gap |
--plf-space-4, 1rem (16px), between the rows, the list and the add button |
--plf-repeater-field-width |
12rem (192px), the narrowest a row’s field gets before the row wraps onto another line, so two fields stack on a phone |