repeater-js

Create field repeaters easily, with no dependencies and in less than 30 KiB!

Download View in GitHub Buy Me a Coffee at ko-fi.com

Installation

First go to the releases page and download repeater.min.js and repeater.min.css.

Add them to your HTML file:

<!-- Add the stylesheet -->
<link rel="stylesheet" href="<path-to-css-files>/repeater.min.css">

<!-- And the script -->
<script type="text/javascript" src="<path-to-js-files>/repeater.min.js"></script>

Then in your DOMContentLoaded event create your repeater.

Usage

To create a repeater, you will need a schema, check the README for more details on the schema structure.

So for example, with a very simple schema:

const schema = {
    collapsed: 'name',
    fields: [{
        type: 'text',
        name: 'name',
        label: 'Name',
        placeholder: 'Enter a name',
        required: true
    }, {
        type: 'email',
        name: 'email',
        label: 'Email address',
        placeholder: 'Enter an email address',
        required: true
    }]
};
const repeater = Repeater.create(document.getElementById('repeater'), schema, new Repeater.BootstrapAdapter());

Saving

To get the contents of the repeater just call the save method. You may listen to the repeater.changed event on the container element to keep it in sync.

The repeater data is serialized as JSON, so that you can easily send it to your backend through a simple textarea.

This textarea will be updated with the contents of the above repeater each time something changes:

const schema = {
    collapsed: 'name',
    fields: [{
        type: 'text',
        name: 'name',
        label: 'Name',
        placeholder: 'Enter a name',
        required: true
    }, {
        type: 'email',
        name: 'email',
        label: 'Email address',
        placeholder: 'Enter an email address',
        required: true
    }]
};
const container = document.getElementById('repeater');
const textarea = document.getElementById('textarea');
const repeater = Repeater.create(container, schema, new Repeater.BootstrapAdapter());
container.addEventListener('repeater.changed', () => {
    textarea.value = repeater.save();
});

Loading

Populating the repeater with data from your backend (or localStorage, who knows!) is equally easy.

Just call the load method with the data you want to load:

This is the format of the data that gets loaded:

[
    {"_collapsed":true,"name":"John Doe","email":"john.doe@example.com"},
    {"_collapsed":false,"name":"Jane Doe","email":"jane.doe@example.com"}
]
const schema = {
    collapsed: 'name',
    fields: [{
        type: 'text',
        name: 'name',
        label: 'Name',
        placeholder: 'Enter a name',
        required: true
    }, {
        type: 'email',
        name: 'email',
        label: 'Email address',
        placeholder: 'Enter an email address',
        required: true
    }]
};
const button = document.getElementById('load-data');
const repeater = Repeater.create(document.getElementById('repeater'), schema, new Repeater.BootstrapAdapter());
button.addEventListener('click', (e) => {
    e.preventDefault();
    repeater.load('[{"_collapsed":false,"name":"John Doe","email":"john.doe@example.com"},{"_collapsed":false,"name":"Jane Doe","email":"jane.doe@example.com"}]');
});

Advanced examples

Kitchen-sink

This is the kitchen-sink example, with all the available fields, some nested repeaters and multi-column fields.

const schema = {
    collapsed: 'title',
    fields: [{
        type: 'text',
        name: 'title',
        label: 'Title',
        placeholder: 'Enter a title',
        required: true,
    }, {
        type: 'email',
        name: 'email',
        label: 'Contact email',
        placeholder: 'Enter an email',
        layout: {
            column: 6
        }
    }, {
        type: 'phone',
        name: 'mobile',
        label: 'Mobile phone',
        placeholder: 'Enter a mobile phone number',
        layout: {
            column: 6
        }
    }, {
        type: 'url',
        name: 'link',
        label: 'Link',
        placeholder: 'Enter a link',
        layout: {
            newRow: true,
            column: 6
        }
    }, {
        type: 'date',
        name: 'dob',
        label: 'Date of birth',
        layout: {
            newRow: true,
            column: 4
        }
    }, {
        type: 'time',
        name: 'sunrise',
        label: 'Sunrise time',
        layout: {
            column: 4
        }
    }, {
        type: 'datetime',
        name: 'appointment',
        label: 'Appointment date',
        layout: {
            column: 4
        }
    }, {
        type: 'color',
        name: 'bg_color',
        label: 'Background color',
    }, {
        type: 'textarea',
        name: 'description',
        label: 'Description',
        placeholder: 'Enter a description',
        maxlength: 150,
    }, {
        type: 'password',
        name: 'token',
        label: 'API token',
        placeholder: 'Enter your API token',
        layout: {
            column: 9
        }
    }, {
        type: 'number',
        name: 'quantity',
        label: 'Quantity',
        min: 1,
        max: 10,
        value: 1,
        layout: {
            column: 3
        }
    }, {
        type: 'range',
        name: 'columns',
        label: 'Columns',
        min: 1,
        max: 12,
        value: 6,
    }, {
        type: 'select',
        name: 'status',
        label: 'Status',
        default: 'draft',
        options: [
            {'draft': 'Draft'},
            {'published': 'Published'},
        ]
    }, {
        type: 'radio',
        name: 'category',
        label: 'Category',
        options: [
            {'news': 'News'},
            {'events': 'Events'},
        ]
    }, {
        type: 'checkbox',
        name: 'tags',
        label: 'Tags',
        options: [
            {'cars': 'Cars'},
            {'tech': 'Tech'},
            {'medicine': 'Medicine'},
            {'gadgets': 'Gadgets'},
        ]
    }, {
        type: 'repeater',
        name: 'faqs',
        label: 'FAQs',
        schema: {
            item: 'Question',
            button: 'Add question',
            collapsed: 'question',
            fields: [{
                type: 'text',
                name: 'question',
                label: 'Question',
                required: true,
            }, {
                type: 'textarea',
                name: 'answer',
                label: 'Answer',
                required: true,
            }, {
                type: 'repeater',
                name: 'buttons',
                label: 'Buttons',
                schema: {
                    item: 'Button',
                    button: 'Add button',
                    collapsed: 'text',
                    fields: [{
                        type: 'text',
                        name: 'text',
                        label: 'Text',
                        required: true,
                        layout: {
                            column: 7
                        }
                    }, {
                        type: 'select',
                        name: 'type',
                        label: 'Type',
                        options: [
                            { 'btn-primary': 'Primary' },
                            { 'btn-secondary': 'Secondary' },
                            { 'btn-dark': 'Dark' },
                            { 'btn-light': 'Light' },
                            { 'btn-info': 'Info' },
                            { 'btn-success': 'Success' },
                            { 'btn-warning': 'Warning' },
                            { 'btn-danger': 'Danger' },
                        ],
                        required: true,
                        layout: {
                            column: 5
                        }
                    }, {
                        type: 'toggle',
                        name: 'outline',
                        label: 'Outline',
                        details: 'Use outlined button style'
                    }, {
                        type: 'url',
                        name: 'link',
                        label: 'Link',
                        required: true,
                    }]
                }
            }]
        }
    }]
};
const repeater = Repeater.create(document.getElementById('repeater'), schema, new Repeater.BootstrapAdapter());

Conditionals

This is the conditionals example, where fields may have extra options; try changing the field types or adding new fields and behold the flexibility of repeater-js!

const schema = {
    item: 'Field',
    button: 'Add field',
    collapsed: 'name',
    fields: [{
        type: 'text',
        name: 'name',
        label: 'Field name',
        required: true,
        layout: {
            column: 7
        }
    }, {
        type: 'select',
        name: 'type',
        label: 'Field type',
        options: [
            { 'text': 'Single-line text' },
            { 'textarea': 'Multi-line text' },
            { 'select': 'Selection box' },
            { 'checkbox': 'Checkbox group' },
            { 'radio': 'Radio-button group' },
        ],
        required: true,
        layout: {
            column: 5
        }
    }, {
        type: 'number',
        name: 'maxlength',
        label: 'Maximum length',
        min: 0,
        max: 260,
        conditional: {
            'field': 'type',
            'type': 'equal',
            'value': 'textarea',
        }
    }, {
        type: 'repeater',
        name: 'options',
        label: 'Options',
        schema: {
            item: 'Option',
            button: 'Add option',
            collapsed: 'label',
            fields: [{
                type: 'text',
                name: 'label',
                label: 'Label',
                required: true,
                transform: {
                    type: 'slug',
                    target: 'value'
                },
                layout: {
                    column: 7
                }
            },{
                type: 'text',
                name: 'value',
                label: 'Value',
                required: true,
                layout: {
                    column: 5
                }
            }]
        },
        conditional: {
            'field': 'type',
            'type': 'matches',
            'value': 'select|checkbox|radio',
        }
    }, {
        type: 'number',
        name: 'minimum',
        label: 'Minimum selection',
        min: 0,
        max: 4,
        conditional: {
            field: 'type',
            type: 'equal',
            value: 'checkbox'
        },
        layout: {
            column: 6
        }
    }, {
        type: 'number',
        name: 'maximum',
        label: 'Maximum selection',
        min: 0,
        max: 4,
        conditional: {
            field: 'type',
            type: 'equal',
            value: 'checkbox'
        },
        layout: {
            column: 6
        }
    }, {
        type: 'toggle',
        name: 'default',
        details: 'This field has a default value',
        conditional: {
            'field': 'type',
            'type': 'matches',
            'value': 'text|textarea|select|radio',
        }
    }, {
        type: 'text',
        name: 'value',
        label: 'Default value',
        conditional: {
            'field': 'default',
            'type': 'equal',
            'value': true,
        }
    }, {
        type: 'toggle',
        name: 'required',
        details: 'This field is required',
        checked: true
    }]
};
const repeater = Repeater.create(document.getElementById('repeater'), schema, new Repeater.BootstrapAdapter());

Nesting

In this last example we leverage two advanced functions: dynamic schema/options and field syncing.

const schema = {
    collapsed: 'name',
    fields: [{
        type: 'text',
        name: 'name',
        label: 'Name',
        transform: {
            type: 'slug',
            target: 'slug'
        },
        layout: {
            column: 6
        },
        required: true
    }, {
        type: 'text',
        name: 'slug',
        label: 'Slug',
        layout: {
            column: 6
        },
        required: true
    }, {
        type: 'select',
        name: 'type',
        label: 'Type',
        options: (field) => {
            const nesting = field.item.repeater.nestingLevel ?? 0;
            const ret = [
                { text: 'Single-line text' },
                { textarea: 'Multi-line text' },
                { select: 'Selection box' },
                { repeater: 'Repeater' }
            ];
            if (nesting === 2) {
                ret.pop();
            }
            return ret;
        },
        required: true
    }, {
        type: 'number',
        name: 'maxlength',
        label: 'Maximum length',
        min: 0,
        max: 150,
        step: 10,
        conditional: {
            type: 'equal',
            field: 'type',
            value: 'textarea'
        }
    }, {
        type: 'select',
        name: 'collapsed',
        label: 'Collapsed',
        sync: {
            field: 'schema',
            callback: (field, other, items) => {
                if (items !== null) {
                    field.select.innerHTML = '';
                    items.forEach(item => {
                        if (item.name && item.slug && item.type === 'text') {
                            field.select.insertAdjacentHTML('beforeend', `<option value="${item.slug}">${item.name}</option>`);
                        }
                    });
                }
                field.select.disabled = other.nestedRepeater.items.length === 0;
            }
        },
        options: [],
        conditional: {
            type: 'equal',
            field: 'type',
            value: 'repeater'
        }
    }, {
        type: 'repeater',
        name: 'options',
        label: 'Options',
        schema: {
            item: 'Option',
            button: 'Add option',
            collapsed: 'label',
            fields: [{
                type: 'text',
                name: 'label',
                label: 'Label',
                transform: {
                    type: 'slug',
                    target: 'value'
                },
                layout: {
                    column: 6
                }
            }, {
                type: 'text',
                name: 'value',
                label: 'Value',
                layout: {
                    column: 6
                }
            }]
        },
        conditional: {
            type: 'equal',
            field: 'type',
            value: 'select'
        }
    }, {
        type: 'repeater',
        name: 'schema',
        label: 'Schema',
        schema: (repeater) => {
            const nesting = repeater.nestingLevel ?? 0;
            if (nesting === 2) {
                schema.fields = schema.fields.filter((field) => field.type !== 'repeater');
            }
            return schema;
        },
        conditional: {
            type: 'equal',
            field: 'type',
            value: 'repeater'
        }
    }]
};
const repeater = Repeater.create(document.getElementById('repeater'), schema, new Repeater.BootstrapAdapter());

Please see the README for more details on how to contibute to the project or build the library.

Licensing

This software is released under the MIT license.

Credits

Lead coder: biohzrdmx <github.com/biohzrdmx>

Buy Me a Coffee at ko-fi.com

Copyright © 2025 biohzrdmx. All rights reserved.