Is it possible to generate a field value of a document every time it is edited in Sanity.io?

13 replies
Last updated: Feb 12, 2021
I'm wondering if it's possible to generate a field value of a document every time the document is edited? Kind of like an 'onChange' hook? Context: I generate URL paths based on a document's nested categories, but these are complex to query with GROQ from our client. For this reason I'd like to add a
path
field / value to these documents, which I'd need to update any time the document/the nested categories are modified.
AI Update

For your use case of automatically updating a path field whenever a document or its nested categories change, you have a few solid options. Let me walk through the most practical approaches:

Option 1: Custom Input Component (Client-Side)

You can create a custom input component that listens to changes in your document and automatically updates the path field. This uses useFormValue to watch for changes:

import { useFormValue } from 'sanity'
import { useEffect } from 'react'
import { set, unset } from 'sanity'

export function PathInput(props) {
  const { onChange, value } = props
  const categories = useFormValue(['categories'])
  
  useEffect(() => {
    if (categories) {
      const generatedPath = generatePathFromCategories(categories)
      if (generatedPath !== value) {
        onChange(generatedPath ? set(generatedPath) : unset())
      }
    }
  }, [categories, value, onChange])
  
  return (
    <div>
      <input type="text" value={value || ''} readOnly />
    </div>
  )
}

Then in your schema:

{
  name: 'path',
  type: 'string',
  components: {
    input: PathInput
  }
}

Pros: Runs in real-time as users edit, no server-side setup needed
Cons: Only works when the document itself is open in Studio; won't update if a referenced category changes

Sanity Functions can automatically update fields when documents are published. This is the modern, recommended approach for reacting to content changes. Note that Functions are currently an experimental feature.

First, initialize blueprints in your project:

npx sanity blueprints init
npx sanity blueprints add function --name update-path

Then configure your function in sanity.blueprint.ts:

import { defineBlueprint, defineDocumentFunction } from '@sanity/blueprints'

export default defineBlueprint({
  resources: [
    defineDocumentFunction({
      type: 'sanity.function.document',
      name: 'update-path',
      src: './functions/update-path',
      event: {
        on: ['create', 'update'],
        filter: '_type == "yourDocumentType"',
      },
    }),
  ],
})

And implement the handler in ./functions/update-path/index.ts:

import type { SanityDocumentEventHandler } from '@sanity/blueprints'

export const handler: SanityDocumentEventHandler = async ({ document, client }) => {
  const newPath = generatePathFromCategories(document.categories)
  
  // Prevent infinite loops - only update if path changed
  if (document.path !== newPath) {
    await client.patch(document._id).set({ path: newPath }).commit()
  }
}

Deploy with:

npx sanity blueprints deploy

Pros: Runs server-side, can react to changes in referenced documents if you set up additional function triggers
Cons: Experimental feature (APIs may change), requires Growth plan or higher, more complex setup

Important: As noted in the Functions documentation, Functions don't currently prevent recursive loops automatically, so make sure to check if the path actually changed before patching the document.

Option 3: Custom Document Action

You can create a custom document action using the useDocumentOperation hook that computes the path when users click "Publish". This gives you control over when the computation happens but requires user interaction.

import { useDocumentOperation } from 'sanity'

export function PublishWithPathAction(props) {
  const { patch, publish } = useDocumentOperation(props.id, props.type)
  
  return {
    label: 'Publish',
    onHandle: () => {
      const newPath = generatePathFromCategories(props.draft.categories)
      patch.execute([{ set: { path: newPath } }])
      publish.execute()
      props.onComplete()
    }
  }
}

Recommendation

If you need the path to update when referenced category documents change (not just when the main document changes), Sanity Functions are your best bet despite being experimental. If you only need updates when the document itself is edited and users will have it open in Studio, the custom input component approach is simpler and doesn't require server-side infrastructure.

For the custom input approach, you can also use useFormValue to access referenced document data if you need to generate paths based on the full category structure, not just the references themselves.

Hi User. Just to clarify, when you say edited, do you mean edited and published?
user E
, yes, I sent you a DM, I have code to give to given to me by User, the great 🙂
user A
Either would be fine, as long as I can make changes that go live once the document is published, which could be either directly upon publishing or if published later.
user L
cool, I've replied in DM, but perhaps you can share the code publicly here too for anyone searching in the future.
Ok, so you should create a file and include that at the top of he schema file:
import UpdateRelatedFields from '../../../plugins/updateRelatedFields/UpdateRelatedFields'
Next, add 
inputComponent
 to the schema field:
 {
      name: 'related',
      title: 'Related',
      type: 'string',
      inputComponent: DisplayRelatedFields
    },

Then, here is the code for the updater:
import PropTypes from 'prop-types'
import React from 'react'
// eslint-disable-next-line import/no-unresolved
import {
  FormBuilderInput,
  withDocument,
  withValuePath
}
// eslint-disable-next-line import/no-unresolved
  from 'part:@sanity/form-builder'

// eslint-disable-next-line import/no-unresolved
import client from 'part:@sanity/base/client'

const handleUpdateRelatedFields = (value, props) => {
  if (props.document._id === undefined) return

  const {document} = props
  const {title} = document
  client
    .patch(props.document._id)
    .setIfMissing({
      metaTitle: title,
      careers: {
        title: `Rollins ${title} Careers`
      },
      metaDescription: title,
      metaKeywords: [title]
    })
    .commit()
    .catch((error) => console.log(error))
}

class UpdateRelatedFields extends React.Component {
  // Declare shape of React properties
  // eslint-disable-next-line import/no-unresolved
  static propTypes = {
    type: PropTypes.shape({
      title: PropTypes.string
    }).isRequired,
    level: PropTypes.number,
    focusPath: PropTypes.array
  }

  render () {
    const {value, level, focusPath, onFocus, onChange} = this.props

    const {inputComponent, ...type} = this.props.type

    // Render component
    return (
      <div style={{marginBottom: 20}}>
        <FormBuilderInput
          level={level}
          type={type}
          value={value}
          onChange={onChange}
          path={[]}
          focusPath={focusPath}
          onFocus={onFocus}
          onBlur={(event) => handleUpdateRelatedFields(event, this.props)}
        />
      </div>
    )
  }
}

export default withValuePath(withDocument(UpdateRelatedFields))
You need this:

import PropTypes from 'prop-types'
import React from 'react'
// eslint-disable-next-line import/no-unresolved
import {
  FormBuilderInput,
  withDocument,
  withValuePath
}
Here, the onBlur makes it happen:

// Render component
    return (
      <div style={{marginBottom: 20}}>
        <FormBuilderInput
          level={level}
          type={type}
          value={value}
          onChange={onChange}
          path={[]}
          focusPath={focusPath}
          onFocus={onFocus}
          onBlur={(event) => handleUpdateRelatedFields(event, this.props)}
        />
      </div>
    )
  }
[and
handleUpdateRelatedFields
passes the event and props....
And this is what does the update:

const {title} = document
  client
    .patch(props.document._id)
    .setIfMissing({
      metaTitle: title,
      careers: {
        title: `Rollins ${title} Careers`
      },
      metaDescription: title,
      metaKeywords: [title]
    })
    .commit()
    .catch((error) => console.log(error))

user T
there is a new/better way to do this with Sanity UI components? That's a lot of code to update one field based on another.
That's the intended outcome of the UI project, but it's not something someone should wait on when working on something.
The current level UI should be used for is replacing any custom markup and/or styling. If you go looking into the the various pieces of form-builder are working now, you can see some of UI in action (we're in the process of putting all the default form pieces into UI).

As an example, here's the component return for a Slug schema type:


<FormField
        title={type.title}
        description={type.description}
        level={level}
        __unstable_markers={markers}
        __unstable_presence={presence}
        inputId={inputId}
      >
        <Stack space={3}>
          <Flex>
            <Box flex={1}>
              <TextInput
                id={inputId}
                ref={forwardedRef}
                customValidity={errors.length > 0 ? errors[0].item.message : ''}
                disabled={isUpdating}
                onChange={handleChange}
                onFocus={handleFocus}
                value={value?.current || ''}
                readOnly={readOnly}
              />
              {generateState?.status === 'error' && (
                <Card padding={2} tone="critical">
                  {generateState.error.message}
                </Card>
              )}
            </Box>
            <Box marginLeft={1}>
              <Button
                mode="ghost"
                type="button"
                disabled={readOnly || isUpdating}
                onClick={handleGenerateSlug}
                onFocus={handleFocus}
                text={generateState?.status === 'pending' ? 'Generating…' : 'Generate'}
              />
            </Box>
          </Flex>
        </Stack>
      </FormField>
Thanks, User!
Similarly, if we have users and groups, we can add a relation to groups in the user schema, or vice versa, or we can create a "person-in-the-middle", userGroup and then have a third screen where we can add a user to a group, but that's inconvenient. I have original code from User on how to display "related" or "used in" fields to, for example, display all users in a group from the user side, but I was wondering if there's a new/better way to do that...
I know that new media manager is doing that, so maybe that's a hint. Ideally, we would bidirectional access to both, namely: A) being able to add/delete/edit a user from the group side, and b) add/delete/edit a group from the user side, so any thoughts on how to do this better than what we have from now more than a year ago would be great.
user T
I'm using User's suggestion, of which I might change parts to use the new Sanity UI. I'm using withDocument in an attempt to use the document's values to patch the value of a form field, but I need to follow references to get the values I need. Is there any way to follow references in withDocument? Or to query references from within a custom input component?

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?