Schema design: Let editors choose from multiple source fields or custom value

8 replies
Last updated: Feb 8, 2022
Hi everyone! I’m trying to design a schema that will accept data from a source system and allow our content editors to make a choice as to which piece of data to show on our front-end website.
For example, I have a document type called Project. In our source system, a project can have a short name, a long name and an alias. For each project in the source system, I want to push all three fields into each Project document in Sanity to be read-only and have the content editors decide which version of the project name will be published to the website.
If none of the above meet the requirements, then they can choose an “Other” field that can be selected.

Attached is a sketch of what I had in my head from a UI perspective. Has anyone tried something similar? I’m presuming it’ll require a custom input component. Any tips for making this as simple as possible from a schema/UI perspective? Thanks!
AI Update

Your use case is definitely achievable and yes, you'll want a custom input component! This is a great pattern for handling data from external systems while giving editors control over what gets published.

Schema Structure

First, set up your schema to store all the data:

{
  name: 'project',
  type: 'document',
  fields: [
    {
      name: 'sourceData',
      type: 'object',
      readOnly: true, // Lock these fields from manual editing
      fields: [
        {name: 'shortName', type: 'string'},
        {name: 'longName', type: 'string'},
        {name: 'alias', type: 'string'}
      ]
    },
    {
      name: 'displayName',
      type: 'object',
      components: {
        input: ProjectNameSelector // Your custom component
      },
      fields: [
        {
          name: 'selected',
          type: 'string',
          options: {
            list: ['shortName', 'longName', 'alias', 'custom']
          }
        },
        {
          name: 'customValue',
          type: 'string'
        }
      ]
    }
  ]
}

Custom Input Component

Based on the custom input components documentation, here's a simplified approach:

import {set, unset} from 'sanity'
import {Stack, Radio, TextInput, Card, Text} from '@sanity/ui'

export function ProjectNameSelector(props) {
  const {value, onChange, elementProps} = props
  const sourceData = props.document?.sourceData || {}
  
  const handleSelectionChange = (selection) => {
    onChange(set({
      selected: selection,
      customValue: value?.customValue
    }))
  }
  
  const handleCustomChange = (event) => {
    onChange(set({
      selected: 'custom',
      customValue: event.target.value
    }))
  }
  
  return (
    <Stack space={3}>
      <Radio
        checked={value?.selected === 'shortName'}
        onChange={() => handleSelectionChange('shortName')}
      >
        <Text>Short Name: {sourceData.shortName}</Text>
      </Radio>
      
      <Radio
        checked={value?.selected === 'longName'}
        onChange={() => handleSelectionChange('longName')}
      >
        <Text>Long Name: {sourceData.longName}</Text>
      </Radio>
      
      <Radio
        checked={value?.selected === 'alias'}
        onChange={() => handleSelectionChange('alias')}
      >
        <Text>Alias: {sourceData.alias}</Text>
      </Radio>
      
      <Card padding={3} border>
        <Stack space={2}>
          <Radio
            checked={value?.selected === 'custom'}
            onChange={() => handleSelectionChange('custom')}
          >
            <Text>Custom</Text>
          </Radio>
          {value?.selected === 'custom' && (
            <TextInput
              value={value?.customValue || ''}
              onChange={handleCustomChange}
            />
          )}
        </Stack>
      </Card>
    </Stack>
  )
}

Querying the Final Value

When querying for your frontend, you can use GROQ to resolve the selected value:

*[_type == "project"] {
  displayName: select(
    displayName.selected == "shortName" => sourceData.shortName,
    displayName.selected == "longName" => sourceData.longName,
    displayName.selected == "alias" => sourceData.alias,
    displayName.selected == "custom" => displayName.customValue
  )
}

Tips

  1. Keep source data separate: The readOnly: true on sourceData prevents accidental edits
  2. Use Sanity UI components: The @sanity/ui library (Radio, Card, TextInput) keeps your component looking native
  3. Access document context: Use props.document to read other fields (like your source data)
  4. Handle patches properly: Use the set() helper from the sanity package as shown in the custom input components guide

This pattern works really well for systems-of-record scenarios where you want source data preserved but give editors control over presentation. You could even enhance it further by showing a preview of what the selected name looks like in your frontend design!

Show original thread
8 replies
Hey User! Seems totally possible. This is effectively a string field with a few tweaks: 1. It's getting the first three radio options from an external source and 2. It's got a single input field that's pretty much just a radio option with a
<TextInput>
primitive .
You're correct in that you'll need to use a custom input component here. If you're having trouble conceptualizing it, I can mock up one that has similar functionality.
Thanks
user M
that certainly seems simple. Schema wise, I couldn’t find an example of dynamically defining the list of strings. Is that what you’re suggesting?
Correct! I wrote a guide a few months ago that can take you through dynamically getting list options.
Wonderful, I’ll take a look! Thanks again!
You're welcome!
+10 points for great looking excalidraw wireframes!
Thanks
user U
I only discovered it recently but I’m loving it!
user N
I never even knew about it. Super cool!

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?