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_idor_index: they're generated.validate: receives the rows, including their_idand_type.The common options
help,width,inline,required,readOnlyandhidden.
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 inschema._id: a unique id, handy as Reactkey._index: a sort key. Rows are already sorted when you get them._labeland_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
schemaper 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'}}}.