How to create a custom input component in Sanity for updating fields based on dropdown selection

5 replies
Last updated: Oct 20, 2022
hello, I had a quick search but couldn't find what I was looking for,I have two objects (displayOptions & management)
In the management object I have a dropdown list with statuses and if a status equals a value then it would either check or uncheck a boolean field in my displayOptions field, for example if the status equals 'extended' then the extended checkbox would be true,
I've included a simplified version of my objects, is there a way to do this?

displayOptions.js:

export default {
  name: 'displayOptions',
  title: 'Display Options',
  type: 'object',
  fields: [
    { 
      name: 'shortlist',
      title: 'Shortlist',
      type: 'boolean',
      initialValue: false,
      options: { layout: 'checkbox' },
    },
    { 
      name: 'extended',
      title: 'Extended',
      type: 'boolean',
      description: 'TODO:',
      initialValue: false,
      options: { layout: 'checkbox' },
    },
    { 
      name: 'omitted',
      title: 'Omitted',
      type: 'boolean',
      initialValue: false,
      options: { layout: 'checkbox' },
    },
  ],
}
management.js:

export default {
  name: 'management',
  title: 'Management',
  type: 'object',
  fields: [
    { 
      name: 'accessId', 
      title: 'Access ID', 
      type: 'string' 
    },
    { 
      name: 'hotelStatusType', 
      title: 'Hotel Status Type',
      type: 'tag',
      options: {
        predefinedTags: [
          {label: 'Extended, value: 'extended'},
          {label: 'Another Status', value: 'anotherStatus'},],
      }
    },
  ],
}
AI Update

I can see you're looking to automatically sync a status dropdown with corresponding boolean checkboxes. While this seems straightforward, Sanity schemas don't support automatic bidirectional field synchronization out of the box. However, there are several practical approaches depending on your exact needs.

The Core Challenge

Sanity's schema system doesn't have built-in "computed fields" or automatic field synchronization. Each field stores its own independent value, and there's no native way to make one field automatically update another when it changes.

Option 1: Use Conditional hidden Instead (Simplest)

Rather than trying to sync checkbox values with the status, consider whether you actually need the checkboxes at all. If the status dropdown already represents the state, you could use conditional fields to show/hide different content based on the status:

// management.js
{
  name: 'hotelStatusType', 
  title: 'Hotel Status Type',
  type: 'string',  // Changed from 'tag' to 'string' for standard dropdown
  options: {
    list: [
      {title: 'Extended', value: 'extended'},
      {title: 'Shortlist', value: 'shortlist'},
      {title: 'Omitted', value: 'omitted'},
    ]
  }
}

// Then in other fields that depend on this status:
{
  name: 'extendedDetails',
  type: 'object',
  hidden: ({document}) => document?.management?.hotelStatusType !== 'extended',
  // ... fields specific to extended status
}

This approach eliminates redundant data storage and keeps your single source of truth in the status field. The hidden callback receives the entire document context, allowing you to conditionally show/hide fields anywhere in your schema based on the status value.

Option 2: Visual Indicators Without Redundant Storage

If you need to display the boolean state visually but don't want to store duplicate data, you could make the displayOptions fields read-only and use conditional logic to show which ones are "active":

// displayOptions.js
fields: [
  { 
    name: 'extended',
    title: 'Extended',
    type: 'boolean',
    readOnly: true,
    hidden: ({document}) => document?.management?.hotelStatusType !== 'extended',
    initialValue: true, // Always true when shown
  },
  { 
    name: 'shortlist',
    title: 'Shortlist',
    type: 'boolean',
    readOnly: true,
    hidden: ({document}) => document?.management?.hotelStatusType !== 'shortlist',
    initialValue: true,
  },
  { 
    name: 'omitted',
    title: 'Omitted',
    type: 'boolean',
    readOnly: true,
    hidden: ({document}) => document?.management?.hotelStatusType !== 'omitted',
    initialValue: true,
  },
]

This displays checkboxes that reflect the status but prevents manual editing and doesn't actually store redundant boolean values.

Option 3: Document Actions with Manual Sync (User-Triggered)

If you want synchronization to happen in the Studio but don't need it to be completely automatic, you could create a custom document action that users trigger manually (like a "Sync Display Options" button):

// In your document actions resolver
import {useDocumentOperation} from 'sanity'

export function SyncDisplayOptionsAction(props) {
  const {patch, publish} = useDocumentOperation(props.id, props.type)
  
  return {
    label: 'Sync Display Options',
    onHandle: () => {
      const status = props.draft?.management?.hotelStatusType || 
                     props.published?.management?.hotelStatusType
      
      patch.execute([
        {set: {
          'displayOptions.extended': status === 'extended',
          'displayOptions.shortlist': status === 'shortlist', 
          'displayOptions.omitted': status === 'omitted'
        }}
      ])
    }
  }
}

This gives editors explicit control over when synchronization happens.

Option 4: Validation to Ensure Consistency

Instead of automatic synchronization, you could use field validation to warn editors when the checkboxes don't match the status:

// In displayOptions.js
validation: Rule => Rule.custom((displayOptions, context) => {
  const status = context.document?.management?.hotelStatusType
  const matchingField = displayOptions?.[status]
  
  if (status && !matchingField) {
    return `Display options don't match status: ${status}`
  }
  return true
})

Why Not Custom Input Components with Patch Interception?

You might see suggestions about custom input components that intercept PatchEvent operations to automatically update other fields. While PatchEvent is a legitimate API for custom components to update their own field values, using it to update other fields across the document has significant drawbacks:

  • Not a documented pattern: The official documentation doesn't recommend cross-field patching from input components
  • Fragile: May break with Studio updates or create race conditions
  • User confusion: Changes happening "magically" in different parts of the form can be disorienting
  • Validation complexity: Can create conflicts with validation logic and real-time collaboration

My Recommendation

Start with Option 1 (conditional hidden) if possible. Ask yourself: do you really need both the status field AND the boolean checkboxes stored as separate data? If the status dropdown fully represents the state, use that as your single source of truth and conditionally show/hide other fields based on it.

If you need visual indicators without data redundancy, use Option 2 (read-only conditional display).

If you genuinely need both fields to store values independently (perhaps for historical reasons or complex querying), use Option 3 (manual document action) to give editors explicit control over synchronization.

The key principle is to avoid storing the same information in multiple places unless absolutely necessary. Redundant data inevitably leads to inconsistencies. When you do need related fields, use conditional visibility, validation, or explicit user actions rather than trying to make the Studio automatically synchronize them behind the scenes.

Note on your schema: I changed the type: 'tag' to type: 'string' with a list option, which is the standard way to create dropdowns in Sanity. The 'tag' type isn't a built-in Sanity field type unless you have a custom plugin installed.

Hey
user Q
! Assuming these are sibling fields in the same document and not objects within an array (but feel free to correct me if I have that wrong!), you'll need to create a custom input component that you wrap in a withDocument HOC . I believe you could make either the
hotelStatusType
or
extendedFields
a custom component that checks the value of the sibling field on the document then patches itself.
Hi
user M
, they are objects within the same document, I'm fairly new to sanity and custom input components but those links look promising, so I'll look into them, thank you! 🙂
You're welcome! Just as a warning, custom input components seem super complex when you first approach them (and they kind of are 🙂 ) but once you start working with them you find that a great deal of it can be copy/pasted from the boilerplate props in the docs.
user M
Thank you for pointing me in the right direction! I got something working so I'm posting a simplified version here in case it helps anyone else - using document props I get my hotelStatusDropdown and with useEffect, set my checkbox if the title matches 🙂

import React, { useEffect } from 'react';
import { withDocument } from 'part:@sanity/form-builder';
import { Checkbox } from '@sanity/ui';
import PatchEvent, { set, unset } from '@sanity/form-builder/PatchEvent';
import { useState } from 'react';
import styles from '../../brandStyling/styles/styles.css';

const StatusNotification = React.forwardRef((props, ref) => {
  const {
    value, // Current field value
    onChange, // Method to handle patch events
    type,
  } = props;

  // The status dropdown that determines display options
  const hotelStatusType = props.document.management.hotelStatusType;

  // Set the conditions where each display option checkbox will be true or not
  let hotelInGuide, hotelShortlist;
  if (
    hotelStatusType === 'old' ||
    hotelStatusType === 'New'
  ) {
    hotelInGuide = true;
  }
  let hotelOmitted = hotelStatusType === 'Omitted' ? true : false;

  // Hooks
  const [inputReadOnly, setInputReadOnly] = useState(false);

  // Functions
  const truePatchEvent = () => {
    onChange(PatchEvent.from('true' ? set(true) : set(false)));
    setInputReadOnly(true);
  };
  const falsePatchEvent = () => {
    onChange(PatchEvent.from('false' ? set(false) : set(true)));
    setInputReadOnly(true);
  };

  /** On page load & anytime the hotel status dropdown is changed we need to
   * update display option checkboxes
   * set to readOnly if applicable
   */
  useEffect(() => {
    console.log('title:', type.title, 'statusType:', hotelStatusType);
    // Check to see what checkbox is current
    // In Guide Checkbox
    if (type.title === 'In Guide') {
      if (hotelInGuide) {
        console.log('in guide');
        truePatchEvent();
      } else if (hotelOmitted) {
        console.log('in guide gets unchecked - omitted!');
        // uncheck in guide as the hotel is omitted
        falsePatchEvent();
      } else {
        console.log('not in guide');
        setInputReadOnly(false);
      }
    }

    // Omitted Checkbox - should always be readonly?
    if (type.title === 'Omitted') {
      if (hotelOmitted) {
        console.log('omitted');
        truePatchEvent();
      } else {
        console.log('not omitted');
        falsePatchEvent();
      }
    }
  }, [hotelStatusType]);

  // Creates a change handler for patching data - this visualises whether the checkbox is 'checked' or not
  const handleChange = React.useCallback(
    // useCallback will help with performance
    (event) => {
      console.log('event', event.target.checked, event.target);
      const inputValue = event.target.checked; // get current value
      onChange(PatchEvent.from(inputValue ? set(true) : set(false)));
    },
    [onChange]
  );

  return (
    <div>
      <label className={inputReadOnly ? styles.labelReadOnly : styles.label}>
        <Checkbox
          checked={value}
          onChange={handleChange}
          readOnly={inputReadOnly}
        />
        <span className={styles.checkboxText}>{type.title}</span>
      </label>
    </div>
  );
});

export default withDocument(StatusNotification);
Thanks so much for sharing your solution
user Q
!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?