Skip to content

Forms

Repeatable entries

Add, remove and reorder entries of one shape: work, education, profiles.

import { Repeater } from '@jsonresume/ui'Source(opens in a new tab)

Examples

Work history

CEO at Pied Piper

Engineer at Hooli

Show code
repeater/work.tsx
'use client';

import { Field, Input, Repeater, Textarea } from '@jsonresume/ui';
import { useState } from 'react';
import * as styles from './work.css';

interface Job {
  id: number;
  name: string;
  position: string;
  summary: string;
}

let nextId = 3;

export default function RepeaterWork() {
  const [work, setWork] = useState<Job[]>([
    {
      id: 1,
      name: 'Pied Piper',
      position: 'CEO',
      summary: 'Built a compression platform.',
    },
    { id: 2, name: 'Hooli', position: 'Engineer', summary: '' },
  ]);
  return (
    <div className={styles.root}>
      <Repeater
        items={work}
        onChange={setWork}
        getKey={(job) => job.id}
        create={() => ({ id: nextId++, name: '', position: '', summary: '' })}
        itemLabel={(job) =>
          [job.position, job.name].filter(Boolean).join(' at ') ||
          'New position'
        }
        noun="position"
        addLabel="Add position"
        renderItem={(job, update) => (
          <>
            <Field label="Company">
              <Input
                value={job.name}
                onChange={(e) => update({ ...job, name: e.target.value })}
              />
            </Field>
            <Field label="Position">
              <Input
                value={job.position}
                onChange={(e) => update({ ...job, position: e.target.value })}
              />
            </Field>
            <Field label="Summary">
              <Textarea
                value={job.summary}
                onChange={(e) => update({ ...job, summary: e.target.value })}
              />
            </Field>
          </>
        )}
      />
    </div>
  );
}
repeater/work.css.ts
import { style } from '@vanilla-extract/css';

export const root = style({ width: 'min(100%, 32rem)' });

Accessibility

  • Each entry is a region named by its title, with a heading.
  • Move and remove buttons name the entry they act on; changes are announced.

Props

<Repeater>

Also accepts the attributes of the element Repeater renders.
PropTypeDefaultDescription
create required() => T–A blank entry for Add.
itemLabel required(item: T, index: number) => string–Heading for each entry, e.g. the job title or "Untitled position".
items requiredT[]–
onChange required(items: T[]) => void–
renderItem required(item: T, update: (next: T) => void, index: number) => ReactNode–The fields for one entry.
addLabelstringAdd entry
getKey((item: T, index: number) => string | number)(_item, i) => iStable keys keep focus on the right entry when reordering.
headingLevel23453Heading level for each entry's title, to fit the page outline.
nounstringentryNames the kind of entry in announcements, e.g. "position".