Skip to content

List

A list field holds an ordered list of rows, and every row has one of the types in its schema. Editors add, reorder and remove rows. Lists are how you build pages out of blocks: a hero, a text section, a gallery, in any order.

import {Config, Field} from 'alinea'

const TextBlock = Config.type('Text', {
  fields: {
    title: Field.text('Title'),
    text: Field.richText('Text')
  }
})

const ImageBlock = Config.type('Image', {
  fields: {
    image: Field.image('Image'),
    caption: Field.text('Caption')
  }
})

export const Page = Config.document('Page', {
  fields: {
    blocks: Field.list('Blocks', {
      schema: {TextBlock, ImageBlock}
    })
  }
})

Options

  • schema (required): the row types, keyed by name. The key is stored in each row's _type, so renaming it orphans existing rows.

  • min: mark the field as invalid while it has fewer rows.

  • max: hide the add buttons once the list has this many rows, and mark the field as invalid when it has more.

  • initialValue: rows to start with, without _id or _index: they're generated.

  • validate: receives the rows, including their _id and _type.

  • The common options help, width, inline, required, readOnly and hidden.

Row types are ordinary Types: they can have tabs, an icon that is shown in the add menu and nested lists. They don't need a title or path and don't have to be in the schema.

Value

The value is an array of rows. Next to its fields, every row has:

  • _type: the key of its type in schema.

  • _id: a unique id, handy as React key.

  • _index: a sort key. Rows are already sorted when you get them.

  • _label and _anchor: optional. Editors can give a row a custom label (shown in the dashboard) and an anchor, so links can point to that row (/page#anchor). Anchors are unique within an entry.

When you query a list, link and image fields inside rows are resolved like fields at the top level.

Rendering

Switch on _type to render each row. Infer turns the list's schema into a union type, so TypeScript knows the fields of every branch:

import {Config, Field, type Infer} from 'alinea'

const TextBlock = Config.type('Text', {
  fields: {text: Field.text('Text', {multiline: true})}
})

const ImageBlock = Config.type('Image', {
  fields: {image: Field.image('Image')}
})

export const blocks = {TextBlock, ImageBlock}

export const Page = Config.document('Page', {
  fields: {blocks: Field.list('Blocks', {schema: blocks})}
})

type Block = Infer<typeof Page>['blocks'][number]

export function Blocks({blocks}: {blocks: Array<Block>}) {
  return blocks.map(block => {
    switch (block._type) {
      case 'TextBlock':
        return <p key={block._id} id={block._anchor}>{block.text}</p>
      case 'ImageBlock':
        return <img key={block._id} src={block.image.src} alt={block.image.alt} />
    }
  })
}

Good to know

  • Different pages often allow different blocks. Keep block types in shared modules and compose a schema per page type, instead of one list that allows everything everywhere.

  • Types used in a list are registered by the list, not by the schema. The same type object can be used in several lists.

  • Filter entries on list contents with includes, for example all pages that contain a video block: filter: {blocks: {includes: {_type: 'VideoBlock'}}}.