Unlock seamless workflows and faster delivery with our latest releases - Join the deep dive

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.
Feb 11, 2021, 11:12 PM
Hi User. Just to clarify, when you say edited, do you mean edited and published?
Feb 11, 2021, 11:20 PM
user E
, yes, I sent you a DM, I have code to give to given to me by User, the great 🙂
Feb 12, 2021, 1:21 AM
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.
Feb 12, 2021, 8:10 AM
user L
cool, I've replied in DM, but perhaps you can share the code publicly here too for anyone searching in the future.
Feb 12, 2021, 8:21 AM
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
    },

Feb 12, 2021, 7:14 PM
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))

Feb 12, 2021, 7:15 PM
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.
Feb 12, 2021, 7:16 PM
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>
Feb 12, 2021, 7:33 PM
Thanks, User!
Feb 12, 2021, 7:34 PM
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...
Feb 12, 2021, 8:02 PM
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.
Feb 12, 2021, 8:03 PM
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?
Feb 12, 2021, 8:24 PM
Feb 12, 2021, 9:04 PM

Sanity– build remarkable experiences at scale

Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?