nextjs-cms

Conditional Fields

Show a field only when other fields have certain values. Hidden fields are not validated and not saved.

Long forms are easier when editors only see what applies. conditionalRules makes a field appear when rules about other fields match.

import { and, or } from 'nextjs-cms/core/fields'

A first example

const linkType = selectField({
    name: 'link_type',
    required: true,
    defaultValue: 'internal',
    options: [
        { value: 'internal', label: 'A page on this site' },
        { value: 'external', label: 'An external URL' },
    ],
})

const page = selectField({
    name: 'page_id',
    section: pagesSection,
    required: true,
    conditionalRules: [{ field: linkType, condition: 'equals', value: 'internal' }],
})

const url = textField({
    name: 'url',
    required: true,
    conditionalRules: [{ field: linkType, condition: 'equals', value: 'external' }],
})

Editors see either the page picker or the URL box, and each is required only while visible.

Rule shape

{
    field: FieldConfig        // the controlling field (the object, not its name)
    condition: Condition      // see below
    value: string | number | boolean | null
}
ConditionMatches when the controlling field's value...
equalsequals value
notEqualsdiffers from value (also true when empty)
containscontains value as a substring (case-sensitive)
notContainsdoesn't contain value (also true when empty)
greaterThanis a number > value
lessThanis a number < value
greaterThanOrEqualsis a number >= value
lessThanOrEqualsis a number <= value

Semantics worth knowing:

  • Values are compared as strings (1 equals '1'), except the four numeric conditions, which require both sides to be numbers and are false otherwise.
  • When the controlling field is empty, only the negative conditions (notEquals, notContains) match.
  • For checkboxField controllers, compare with true or false.
  • contains works well with multi-selects and tags, whose values are comma-separated: { field: features, condition: 'contains', value: 'pool' }.

One rule vs. several

A single rule is a one-element array. Two or more rules must be wrapped in and() or or(). A bare multi-rule array is a type error, so the intent is always explicit:

// One rule
conditionalRules: [{ field: type, condition: 'equals', value: 'event' }]

// All must match
conditionalRules: and(
    { field: type, condition: 'equals', value: 'event' },
    { field: isOnline, condition: 'equals', value: false },
)

// At least one must match
conditionalRules: or(
    { field: status, condition: 'equals', value: 'scheduled' },
    { field: status, condition: 'equals', value: 'published' },
)

Groups don't nest: and() and or() take rules, not other groups. When you need "A and (B or C)", restructure the condition. Often a single contains or notEquals rule expresses it, or an extra select field makes the intent explicit.

What "hidden" means

The same evaluator runs in the browser (to show/hide) and on the server (to save), so a field is never shown but ignored, or hidden but saved:

Visible (rules match)Hidden (rules don't match)
Rendered✓✗
required enforced✓✗
Validated (length, range, file type...)✓✗
Written to the database✓✗ (the column keeps its previous value)
ColumnNullable, even with required: trueNullable (hidden fields have no value)

Patterns

Show extra fields for one option

const type = selectField({
    name: 'type',
    required: true,
    options: [
        { value: 'article', label: 'Article' },
        { value: 'video', label: 'Video' },
        { value: 'podcast', label: 'Podcast' },
    ],
})

const body = richTextField({
    name: 'body',
    required: true,
    conditionalRules: [{ field: type, condition: 'equals', value: 'article' }],
})

const videoUrl = textField({
    name: 'video_url',
    required: true,
    conditionalRules: [{ field: type, condition: 'equals', value: 'video' }],
})

const audio = documentField({
    name: 'audio_transcript',
    fileType: ['pdf', 'txt'],
    conditionalRules: [{ field: type, condition: 'equals', value: 'podcast' }],
})

Toggle a whole block with a checkbox

import type { ConditionalRulesConfig } from 'nextjs-cms/core/types'

const hasCta = checkboxField({ name: 'has_cta', label: 'Show a call-to-action' })
const whenCta: ConditionalRulesConfig = [{ field: hasCta, condition: 'equals', value: true }]

const ctaLabel = textField({ name: 'cta_label', required: true, conditionalRules: whenCta })
const ctaUrl = textField({ name: 'cta_url', required: true, conditionalRules: whenCta })

Thresholds

const price = numberField({ name: 'price', format: 'float', minValue: 0, maxValue: 1_000_000 })

const shippingNote = textAreaField({
    name: 'free_shipping_note',
    conditionalRules: [{ field: price, condition: 'greaterThanOrEquals', value: 100 }],
})

Depend on a tag or multi-select

const amenities = selectMultipleField({
    name: 'amenities',
    options: [
        { value: 'pool', label: 'Pool' },
        { value: 'gym', label: 'Gym' },
        { value: 'parking', label: 'Parking' },
    ],
})

const poolHours = textField({
    name: 'pool_hours',
    conditionalRules: [{ field: amenities, condition: 'contains', value: 'pool' }],
})
Warning:

Don't make a field depend on itself, and avoid chains where a hidden field controls another field. If A hides B, rules that read B see an empty value.

Good to know

  • The controlling field must be in the same section.
  • Conditional fields work inside field groups, and across groups.
  • Changing rules doesn't change the table (except nullability when you add or remove the first rule on a required field), so a refresh is usually enough.