Skip to content

Instantly share code, notes, and snippets.

@ndimatteo
Last active June 22, 2021 13:33
Show Gist options
  • Save ndimatteo/2a8fa57034b15c0ed32d96eb67c6e45f to your computer and use it in GitHub Desktop.
Save ndimatteo/2a8fa57034b15c0ed32d96eb67c6e45f to your computer and use it in GitHub Desktop.
Sanity Conditional Fields (async)

Async Conditional Fields for Sanity

✨ Asynchronous ✨ multiple fields ✨ Sanity UI ✨ Easy! ✨

Show multiple fields in an object field, based on a condition set through the "options" object. Simply return true/false from "condition" function, even asynchronously! You have access to the entire document object, and the context() function

Install

First, drop the conditional-field.js file somewhere in your Studio, I like putting custom components in a "components" folder, matching my front-end setup.

Then, from your schema import the file:

import ConditionalFields from '../components/conditional-field'

Finally, you can begin wrapping the fields you want to show/hide based on a condition in an object field like this:

{
  name: 'objectTitle',
  title: 'object Title'
  type: 'object',
  options: {
	condition: (document: obj, context: func) => bool
 }
  fields : []
}

Examples

The Classic: Internal/External Link toggle

It's common to offer links to internal "pages" and external URLs. Here's an example that lets you toggle between both options and only show the corresponding fields necessary for each.

{
  title: 'Link',
  name: 'customLink',
  type: 'object',
  fields: [
    {
      title: 'Type',
      name: 'type',
      type: 'string',
      options: {
        list: [
          { title: 'Internal Page', value: 'internal' },
          { title: 'External URL', value: 'external' }
        ],
        layout: 'radio',
        direction: 'horizontal'
      }
    },
    {
      name: 'internal',
      type: 'object',
      inputComponent: ConditionalFields,
      fields: [
        {
          title: 'Internal Page',
          name: 'page',
          type: 'reference',
          to: [{ type: 'page' }]
        }
      ],
      options: {
        condition: (_, context) => context().type === 'internal'
      }
    },
    {
      name: 'external',
      type: 'object',
      inputComponent: ConditionalFields,
      fields: [
        {
          title: 'External URL',
          name: 'url',
          type: 'url'
        }
      ],
      options: {
        condition: (_, context) => context().type === 'external'
      }
    }
  ]
}

The WordPress: Set Homepage/Posts page

Say you have a dynamic "page" document and you want to recreate the famous Wordpress "reading" settings where the user can define their "Homepage" and "Posts page", with conditional fields you can do this pretty easily! Here, we fetch a page reference set in a generalSettings "singleton" and compare it against the current document to determine if we should show some additional fields for the designated "Blog" page document only.

{
  name: 'blog',
  type: 'object',
  inputComponent: ConditionalFields,
  fields: [
    // your conditional fields
  ],
  options: {
    condition: (document, _) => {
      return client
        .fetch(
          `*[_type == "page" && _id == *[_type=="generalSettings"][0].blog->_id] | order(_updatedAt desc)[0]`
        )
        .then(blog => {
          if (!blog?.slug) return false
          return document.slug.current === blog.slug.current
        })
    }
  }
}
import React from 'react'
import PropTypes from 'prop-types'
import { FormBuilderInput } from '@sanity/form-builder/lib/FormBuilderInput'
import { withDocument, withValuePath } from 'part:@sanity/form-builder'
import { Stack } from '@sanity/ui'
import { setIfMissing } from '@sanity/form-builder/PatchEvent'
const isFunction = obj => !!(obj && obj.constructor && obj.call && obj.apply)
class ConditionalFields extends React.PureComponent {
static propTypes = {
type: PropTypes.shape({
title: PropTypes.string,
name: PropTypes.string.isRequired,
fields: PropTypes.array.isRequired,
options: PropTypes.shape({
condition: PropTypes.func.isRequired
}).isRequired
}).isRequired,
level: PropTypes.number,
value: PropTypes.shape({
_type: PropTypes.string
}),
onFocus: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired
}
firstFieldInput = React.createRef()
state = { showFields: false }
focus() {
this.firstFieldInput.current && this.firstFieldInput.current.focus()
}
getContext(level = 1) {
// gets value path from withValuePath HOC, and applies path to document
// we remove the last 𝑥 elements from the valuePath
const valuePath = this.props.getValuePath()
const removeItems = -Math.abs(level)
return valuePath.length + removeItems <= 0
? this.props.document
: valuePath.slice(0, removeItems).reduce((context, current) => {
// basic string path
if (typeof current === 'string') {
return context[current] || {}
}
// object path with key used on arrays
if (
typeof current === 'object' &&
Array.isArray(context) &&
current._key
) {
return (
context.filter(
item => item._key && item._key === current._key
)[0] || {}
)
}
}, this.props.document)
}
handleFieldChange = (field, fieldPatchEvent) => {
// Whenever the field input emits a patch event, we need to make sure each of the included patches
// are prefixed with its field name, e.g. going from:
// {path: [], set: <nextvalue>} to {path: [<fieldName>], set: <nextValue>}
// and ensure this input's value exists
const { onChange, type } = this.props
const event = fieldPatchEvent
.prefixAll(field.name)
.prepend(setIfMissing({ _type: type.name }))
onChange(event)
}
handleConditionCheck = async () => {
const { document, type } = this.props
const condition =
(isFunction(type.options.condition) && type.options.condition) ||
function() {
return true
}
const showFields = await condition(document, this.getContext.bind(this))
this.setState({ showFields })
}
async componentDidMount() {
await this.handleConditionCheck()
}
async componentDidUpdate() {
await this.handleConditionCheck()
}
render() {
const { type, value, level, onFocus, onBlur } = this.props
if (!this.state.showFields) return <></>
return (
<Stack space={[3, 3, 4, 5]}>
{type.fields.map((field, i) => (
<div key={i}>
<FormBuilderInput
level={level + 1}
ref={i === 0 ? this.firstFieldInput : null}
key={field.name}
type={field.type}
value={value && value[field.name]}
onChange={patchEvent => this.handleFieldChange(field, patchEvent)}
path={[field.name]}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
))}
</Stack>
)
}
}
export default withValuePath(withDocument(ConditionalFields))
@ndimatteo
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment