Repeater
Repeater widgets are Input Fields that allow the user to enter information as a list given a template. The information will be stored and returned as an array of objects.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('guests', { addLabel: 'Add Guest', removeLabel: 'Remove Guest', label: 'Guest List', uid: 'repeater_basic', template: [ gui.layouts.flex([ gui.inputs.textInput('guests.items.guest_name', { label: 'Full Name', }), ]), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "uid": "repeater_basic", "kind": "input", "type": "repeater", "path": "guests", "label": "Guest List", "props": { "addLabel": "Add Guest", "removeLabel": "Remove Guest", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "guests.items.guest_name", "label": "Full Name" } ] } } } ]}Use this props to customize your Repeater widget.
| prop | type | description |
|---|---|---|
template | object | MANDATORY A template Field with the elements rendered by the repeater |
addButtonIcon | string | CSS class string for the icon displayed in the add button |
addLabel | string | Label text for the add button |
limit | number | Maximum number of items that can be added |
removeButtonIcon | string | CSS class string for the icon displayed in the remove button |
removeLabel | string | Label text for the remove button |
title | string | Optional title displayed at the top of each card |
Template
Section titled “Template”Use the property template to render a tree of Field widgets for each item in the array of the Repeater widget. The path in the Input Fields widgets inside the template will be updated replacing items keyword by the index of the array. For example, users.items.firstName will be replaced by users[0].firstName for the first element and so on.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('guests', { addLabel: 'Add Guest', removeLabel: 'Remove Guest', label: 'Guest List', uid: 'repeater_basic', template: [ gui.layouts.flex([ gui.inputs.textInput('guests.items.guest_name', { label: 'Full Name', }), ]), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "uid": "repeater_basic", "kind": "input", "type": "repeater", "path": "guests", "label": "Guest List", "props": { "addLabel": "Add Guest", "removeLabel": "Remove Guest", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "guests.items.guest_name", "label": "Full Name" } ] } } } ]}Add and Remove Labels
Section titled “Add and Remove Labels”Use the property addLabel and removeLabel to set custom labels for the Add and Remove button.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('users', { addLabel: 'Add new developer', removeLabel: 'Remove developer', defaultValue: [ { firstName: 'John', lastName: 'Doe', }, { firstName: 'Jane', lastName: 'Doe', }, ], template: [ gui.layouts.flex([ gui.inputs.textInput('users.items.firstName'), gui.inputs.textInput('users.items.lastName'), ]), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "repeater", "path": "users", "defaultValue": [ { "firstName": "John", "lastName": "Doe" }, { "firstName": "Jane", "lastName": "Doe" } ], "props": { "addLabel": "Add new developer", "removeLabel": "Remove developer", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "users.items.firstName" }, { "kind": "input", "type": "textinput", "path": "users.items.lastName" } ] } } } ]}Use the property limit to set a limit to the number of elements that can be added to a repeater.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('contacts', { limit: 3, addLabel: 'Add Contact', removeLabel: 'Delete', label: 'Emergency Contacts', uid: 'repeater_limit', template: [ gui.layouts.flex([ gui.inputs.textInput('contacts.items.phone', { label: 'Phone Number', }), ]), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "uid": "repeater_limit", "kind": "input", "type": "repeater", "path": "contacts", "label": "Emergency Contacts", "props": { "limit": 3, "addLabel": "Add Contact", "removeLabel": "Delete", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "contacts.items.phone", "label": "Phone Number" } ] } } } ]}Card Title
Section titled “Card Title”Use the property title to display a static title at the top of each card.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('guests', { title: 'Guest', addLabel: 'Add Guest', removeLabel: 'Remove', label: 'Guest List', uid: 'repeater_title', template: [ gui.layouts.flex([ gui.inputs.textInput('guests.items.guest_name', { label: 'Full Name', }), ]), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "uid": "repeater_title", "kind": "input", "type": "repeater", "path": "guests", "label": "Guest List", "props": { "title": "Guest", "addLabel": "Add Guest", "removeLabel": "Remove", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "guests.items.guest_name", "label": "Full Name" } ] } } } ]}Button Icons
Section titled “Button Icons”Use the properties addButtonIcon and removeButtonIcon to display an icon inside the add and remove buttons. Icons are passed as a CSS class string, the same way as other icon props in the library.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('guests', { addLabel: 'Add Guest', removeLabel: 'Remove', addButtonIcon: 'add', removeButtonIcon: 'remove', label: 'Guest List', uid: 'repeater_icons', template: [ gui.layouts.flex([ gui.inputs.textInput('guests.items.guest_name', { label: 'Full Name', }), ]), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "uid": "repeater_icons", "kind": "input", "type": "repeater", "path": "guests", "label": "Guest List", "props": { "addLabel": "Add Guest", "removeLabel": "Remove", "addButtonIcon": "add", "removeButtonIcon": "remove", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "guests.items.guest_name", "label": "Full Name" } ] } } } ]}Nested Repeaters
Section titled “Nested Repeaters”Repeaters can be nested inside other repeaters to model hierarchical data structures. Each nesting level adds an additional .items. segment to the path. For example, in a Teams → Members structure:
- The outer repeater path is
teams - Fields inside the outer template use
teams.items.fieldName - The inner repeater path is
teams.items.members - Fields inside the inner template use
teams.items.members.items.fieldName
At runtime, each items token is replaced by the corresponding array index. For instance, teams.items.members.items.memberName becomes teams[0].members[1].memberName for the second member of the first team.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('teams', { addLabel: 'Add Team', removeLabel: 'Remove Team', label: 'Teams', uid: 'team_repeater', template: [ gui.layouts.flex([ gui.inputs.textInput('teams.items.teamName', { label: 'Team Name', }), gui.inputs.repeater('teams.items.members', { addLabel: 'Add Member', removeLabel: 'Remove Member', label: 'Members', uid: 'member_repeater', template: [ gui.layouts.flex([ gui.inputs.textInput('teams.items.members.items.memberName', { label: 'Member Name', }), ]), ], }), ], { direction: 'column', }), ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "uid": "team_repeater", "kind": "input", "type": "repeater", "path": "teams", "label": "Teams", "props": { "addLabel": "Add Team", "removeLabel": "Remove Team", "template": { "kind": "layout", "type": "flex", "props": { "direction": "column" }, "children": [ { "kind": "input", "type": "textinput", "path": "teams.items.teamName", "label": "Team Name" }, { "uid": "member_repeater", "kind": "input", "type": "repeater", "path": "teams.items.members", "label": "Members", "props": { "addLabel": "Add Member", "removeLabel": "Remove Member", "template": { "kind": "layout", "type": "flex", "children": [ { "kind": "input", "type": "textinput", "path": "teams.items.members.items.memberName", "label": "Member Name" } ] } } } ] } } } ]}For the $item/$index scope variables available inside nested templates, see $item and $index below.
$item and $index
Section titled “$item and $index”Inside a repeater’s template, every widget can read the current row through two scope variables: $item (the row’s array element, looked up live from form data) and $index (its zero-based position). Both are available in include/exclude/disabled/readonly when expressions, in {{ }} interpolation slots, and in i18n params - anywhere those mechanisms already work, they just gain two more variables to read from.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('lineItems', { addLabel: 'Add Line Item', removeLabel: 'Remove Line Item', label: 'Line Items', uid: 'line_items_repeater', template: [ gui.layouts.flex([ gui.inputs.numberInput('lineItems.items.quantity', { label: 'Quantity', }), gui.inputs.currency('lineItems.items.unitPrice', { label: 'Unit Price', }), gui.displays.markdownText({ md: 'Row {{ $index + 1 }} total: {{ (($item.quantity ?? 0) * ($item.unitPrice ?? 0)).toFixed(2) }}', include: { when: '$item.quantity !== undefined', }, }), ], { direction: 'column', }), ], }),];{ "form": [ { "uid": "line_items_repeater", "kind": "input", "type": "repeater", "path": "lineItems", "label": "Line Items", "props": { "addLabel": "Add Line Item", "removeLabel": "Remove Line Item", "template": { "kind": "layout", "type": "flex", "props": { "direction": "column" }, "children": [ { "kind": "input", "type": "number", "path": "lineItems.items.quantity", "label": "Quantity" }, { "kind": "input", "type": "currency", "path": "lineItems.items.unitPrice", "label": "Unit Price" }, { "kind": "display", "type": "markdownText", "props": { "md": "Row {{ $index + 1 }} total: {{ (($item.quantity ?? 0) * ($item.unitPrice ?? 0)).toFixed(2) }}" }, "include": { "when": "$item.quantity !== undefined" } } ] } } } ]}A freshly added row has no data yet, so $item leaf values can be undefined - guard reads with ?? or ?., as in $item.quantity ?? 0. $index is always a defined number and never needs a guard.
Where you can use $item/$index:
- Inline
when-include,exclude,disabled, andreadonly - String interpolation -
{{ }}slots in any string prop - i18n dynamic params - bare-expression
paramsvalues
Scope in nested repeaters
Section titled “Scope in nested repeaters”For nested repeaters, $item/$index always resolve to the innermost enclosing repeater’s item. A widget placed directly in an outer template - not inside the inner repeater’s template - sees the outer item instead. Scope follows where a widget sits in the template tree, not “the deepest repeater anywhere in the form.”
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.repeater('projects', { addLabel: 'Add Project', removeLabel: 'Remove Project', label: 'Projects', uid: 'projects_repeater', template: [ gui.layouts.flex([ gui.inputs.textInput('projects.items.projectName', { label: 'Project Name', }), gui.inputs.repeater('projects.items.tasks', { addLabel: 'Add Task', removeLabel: 'Remove Task', label: 'Tasks', uid: 'tasks_repeater', template: [ gui.layouts.flex([ gui.inputs.numberInput('projects.items.tasks.items.hours', { label: 'Hours', }), gui.inputs.currency('projects.items.tasks.items.rate', { label: 'Hourly Rate', }), gui.displays.markdownText({ md: 'Task {{ $index + 1 }}: {{ (($item.hours ?? 0) * ($item.rate ?? 0)).toFixed(2) }}', }), ], { direction: 'column', }), ], }), gui.displays.alert({ text: 'Project total: {{ ($item.tasks ?? []).reduce((sum, task) => sum + ((task.hours ?? 0) * (task.rate ?? 0)), 0).toFixed(2) }}', }), ], { direction: 'column', }), ], }),];{ "form": [ { "uid": "projects_repeater", "kind": "input", "type": "repeater", "path": "projects", "label": "Projects", "props": { "addLabel": "Add Project", "removeLabel": "Remove Project", "template": { "kind": "layout", "type": "flex", "props": { "direction": "column" }, "children": [ { "kind": "input", "type": "textinput", "path": "projects.items.projectName", "label": "Project Name" }, { "uid": "tasks_repeater", "kind": "input", "type": "repeater", "path": "projects.items.tasks", "label": "Tasks", "props": { "addLabel": "Add Task", "removeLabel": "Remove Task", "template": { "kind": "layout", "type": "flex", "props": { "direction": "column" }, "children": [ { "kind": "input", "type": "number", "path": "projects.items.tasks.items.hours", "label": "Hours" }, { "kind": "input", "type": "currency", "path": "projects.items.tasks.items.rate", "label": "Hourly Rate" }, { "kind": "display", "type": "markdownText", "props": { "md": "Task {{ $index + 1 }}: {{ (($item.hours ?? 0) * ($item.rate ?? 0)).toFixed(2) }}" } } ] } } }, { "kind": "display", "type": "alert", "props": { "text": "Project total: {{ ($item.tasks ?? []).reduce((sum, task) => sum + ((task.hours ?? 0) * (task.rate ?? 0)), 0).toFixed(2) }}" } } ] } } } ]}Styling
Section titled “Styling”Repeaters can be styled as explained in the Styling Guide.
CSS Variables
Section titled “CSS Variables”Following you will find a list with the CSS Variables and a quick description of what you will style.
| CSS Variable | Description |
|---|---|
--gui-space-gap | Space between each repeater element |
--gui-field-gap | Space between Add/Remove buttons |
--gui-padding | Repeater element padding |
--gui-color-border | Repeater element border color |
--gui-radius | Repeater element border radius |
--gui-color-bg | Repeater element background color |
--gui-color-fg | Repeater element text color |
Anatomy
Section titled “Anatomy”This is the anatomy of the Repeater Widget in case you want to use your CSS styles.
<div class="gui-repeater"> <div id="repeater_uid"> <h2>Guest List</h2>
<div class="gui-repeater__card"> <div class="gui-repeater__card-header"> <span class="gui-repeater__card-title">Guest</span>
<button type="button" class="gui-button gui-repeater__remove-btn"> <span class="gui-button-icon delete"></span> Remove Guest </button> </div>
<gui-repeater-widget> <div class="gui-label"> <label>Full Name</label> </div> <div class="gui-widget"> <input type="text" value="John Doe" /> </div> </gui-repeater-widget> </div>
<button type="button" class="gui-button gui-repeater__add-btn"> <span class="gui-button-icon person_add"></span> Add Guest </button> </div></div>