nextjs-cms

selectField

A dropdown with one choice. Options can be a fixed list, rows from any table, or items from another section.

import { selectField } from 'nextjs-cms/core/fields'

A select gets its options from exactly one of three sources:

SourceOptionColumnUse for
Static listoptionsENUM(...)Statuses, types, sizes: values known in advance
Another sectionsectionINT or VARCHAR, matching that section's identifierCategories, authors, brands managed in the CMS
A lookup tabledbINT or VARCHARLists maintained outside the CMS (countries, currencies)

Basic usage

selectField({
    name: 'status',
    options: [
        { value: 'draft', label: 'Draft' },
        { value: 'published', label: 'Published' },
    ],
})

With every option

selectField({
    name: 'brand_id',
    label: { en: 'Brand', ar: 'العلامة التجارية' },
    required: true,
    order: 3,
    localized: false,
    conditionalRules: [{ field: hasBrand, condition: 'equals', value: true }],

    // one source:
    section: brandsSection,
    // options: [...],
    // db: { table, identifier, label, orderBy, identifierType, labelType },

    checkValueExists: true,
    destinationDb: undefined, // or { table, itemIdentifier, selectIdentifier }
})

Options

OptionTypeDefaultDescription
options{ value: string | number; label: string }[]noneStatic options (at least one).
sectiona has-items or category sectionnoneUse another section's items as options.
db{ table, identifier, label, orderBy?, identifierType?, labelType? }noneRead options from a table.
checkValueExistsbooleanfalseOn save, verify the submitted value is a real option.
destinationDb{ table, itemIdentifier, selectIdentifier }noneStore the value in a join table instead of a column.
defaultValuestring | numbernonePre-selected value.

Plus the common options. Exactly one of options, section, or db is required; TypeScript rejects combinations.

Options in detail

options: static list

selectField({
    name: 'size',
    required: true,
    defaultValue: 'm',
    options: [
        { value: 's', label: 'Small' },
        { value: 'm', label: 'Medium' },
        { value: 'l', label: 'Large' },
    ],
})

The column is an ENUM('s', 'm', 'l'), so MySQL itself rejects anything else. Adding an option later changes the ENUM: run update-sections. Removing an option that rows still use makes the ALTER fail until you update those rows.

Labels are plain strings. Store the stable value and translate labels on your website if needed.

section: another section's items

import brandsSection from './brands.section'

selectField({ name: 'brand_id', label: 'Brand', section: brandsSection })
  • Options are the rows of brands: the label is its headingField, the value its identifier.
  • Order follows the section's orderByField (or its identifier).
  • Column matches the identifier type: INT for the default id, VARCHAR(255) for a text identifier.
  • Works with has-items and category sections. For nested categories the form shows one dropdown per level; pair it with destinationDb to store every level.
Warning:

Don't point db at a table owned by another section. update-sections refuses it and tells you to use section instead.

db: a lookup table

selectField({
    name: 'country_code',
    db: {
        table: 'countries',
        identifier: 'code', // stored value
        label: 'name', // shown text
        orderBy: 'name',
        identifierType: 'text', // 'text' (default) | 'number'
        labelType: 'text',
    },
})
Sub-optionDescription
tableTable with the options.
identifierColumn stored as the value.
labelColumn shown to editors.
orderByColumn to sort options by. Optional.
identifierType'text' or 'number'. Sets the lookup table's identifier column type and this field's column type (VARCHAR(255) or INT). Default 'text'.
labelType'text' or 'number'. Type of the label column. Default 'text'.

update-sections creates the lookup table if it doesn't exist (with the identifier, label, and orderBy columns), so you can fill it with a seed script or SQL. It's not editable in the admin; if editors should manage the list, make it a section and use section instead.

checkValueExists

By default, the server trusts that the submitted value came from the dropdown. With checkValueExists: true, it verifies the value exists among the options before saving, and rejects tampered or stale values:

selectField({ name: 'plan_id', section: plansSection, checkValueExists: true })

Static options are already protected by the ENUM column. Turn this on for section and db selects where a wrong id would silently corrupt data.

destinationDb

Stores the selection in a separate table with one row per value, and gives the section's table no column for this field.

selectField({
    name: 'category',
    section: categoriesSection,
    destinationDb: {
        table: 'product_category', // created by update-sections
        itemIdentifier: 'product_id', // this item's identifier
        selectIdentifier: 'category_id', // the chosen option's value
    },
})

For a single select, this mostly matters with nested categories (one row per chosen level). For many-to-many, see selectMultipleField and the Relationships guide.

Examples

Status with a default and dependent field

const status = selectField({
    name: 'status',
    required: true,
    defaultValue: 'draft',
    options: [
        { value: 'draft', label: 'Draft' },
        { value: 'scheduled', label: 'Scheduled' },
        { value: 'published', label: 'Published' },
    ],
})

const publishAt = dateField({
    name: 'publish_at',
    format: 'datetime',
    required: true,
    conditionalRules: [{ field: status, condition: 'equals', value: 'scheduled' }],
})

Author picked from a team section

import teamSection from './team.section'

selectField({ name: 'author_id', label: 'Author', section: teamSection, required: true, checkValueExists: true })

Numeric options

selectField({
    name: 'rating',
    options: [1, 2, 3, 4, 5].map((n) => ({ value: n, label: '★'.repeat(n) })),
})

Storage

SourceColumnValue
optionsENUM('a','b',...)The option's value
sectionINT or VARCHAR(255)The item's identifier
dbINT or VARCHAR(255) (from identifierType)The identifier column's value
with destinationDbno column; rows in the destination table