Setting the value of a read-only string field based on the value of other fields in a document using custom components or document actions in Sanity.io

19 replies
Last updated: Aug 14, 2020
This has most likely been answered before but I'm not having much luck searching. I'd like to set the value of a read only string field based on the value of 3 other fields in the document, 2 of which are references. I currently have a custom component that wraps all 4 fields but I'm having a hard time getting the value I need from the reference fields. Is there a way to do this?
Aug 11, 2020, 6:08 PM
Here is my custom component for reference
import PropTypes from 'prop-types'
import React from 'react'
import client from 'part:@sanity/base/client'
import Fieldset from 'part:@sanity/components/fieldsets/default'
import {setIfMissing} from 'part:@sanity/form-builder/patch-event'
import {FormBuilderInput} from 'part:@sanity/form-builder'

export default class CustomObjectInput extends React.PureComponent {
  static propTypes = {
    type: PropTypes.shape({
      title: PropTypes.string,
      name: PropTypes.string
    }).isRequired,
    level: PropTypes.number,
    value: PropTypes.shape({
      _type: PropTypes.string
    }),
    focusPath: PropTypes.array.isRequired,
    onFocus: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    onBlur: PropTypes.func.isRequired
  }

  firstFieldInput = React.createRef()

  handleFieldChange = (field, fieldPatchEvent) => {
    const {onChange, type , value} = this.props

    let region = value.region || ''
    let language = value.language || ''
    let course = value.courseType

    let name = `${region} ${language} ${course}`
    
    // Whenever the field input emits a patch event, we need to make sure to 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
    onChange(fieldPatchEvent.prefixAll(field.name).prepend(setIfMissing({_type: type.name})))
  }

  focus() {
    this.firstFieldInput.current.focus()
  }

  render() {
    const {type, value, level, focusPath, onFocus, onBlur} = this.props
    return (
      <Fieldset>
        <div >
          {type.fields.map((field, i) => (
            // Delegate to the generic FormBuilderInput. It will resolve and insert the actual input component
            // for the given field type
            <div style={{marginBottom: `16px`}}>
              <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]}
                focusPath={focusPath}
                onFocus={onFocus}
                onBlur={onBlur}
              />
            </div>
          ))}
        </div>
      </Fieldset>
    )
  }
}
Aug 11, 2020, 6:08 PM
I need to be able to get the name field from the region and language references.
Aug 11, 2020, 6:09 PM
Perhaps a different approach here would be to use Document Actions instead of the custom component? https://www.sanity.io/docs/document-actions
You could make a new publish action that fills out the read only fields just before publishing.
Aug 12, 2020, 7:24 PM
To get the data from the referenced documents you can use the client to query for the those documents

import client from 'part:@sanity/base/client'

const doc = await client.getDocument(your-ref-doc-id)
Aug 12, 2020, 7:28 PM
An alternative route would be to skip filling in data from the other documents all togheter. It's kind of an anti-pattern in my mind.
Instead you should be able to include the data from the referenced documents in your front end queries quite easily. Especially if you're using groq in the front end.
Aug 12, 2020, 7:30 PM
Hey, thanks for getting back to me. I'm probably missing out a vital step somewhere but I've created the action and registered it following the docs above. My action, for now, is almost exactly the example you posted.
import { useState, useEffect } from 'react'
import { useDocumentOperation } from '@sanity/react-hooks'

export function UpdateInstanceName(props) {
  const {patch, publish} = useDocumentOperation(props.id, props.type)
  const [isPublishing, setIsPublishing] = useState(false)
  
  useEffect(() => {
    // if the isPublishing state was set to true and the draft has changed
    // to become `null` the document has been published
    console.log(isPublishing && !props.draft)
    if (isPublishing && !props.draft) {
      setIsPublishing(false)
    }
  }, [props.draft])
  
  return {
    disabled: publish.disabled,
    label: isPublishing ? 'Publishing…' : 'Publish',
    onHandle: () => {
      console.log("handle Something")
      // This will update the button text 
      setIsPublishing(true)
      
      // Set publishedAt to current date and time
      patch.execute([{set: {name: "Test Name"}}])
      
      // Perform the publish
      publish.execute()
      
      // Signal that the action is completed
      props.onComplete() 
    }
  }
}
The first
console.log
happens so I know it's hooked up but it seems like the
onHandle
callback is never called as the
console.log
within it never logs. Can you see what I'm doing wrong?
Aug 13, 2020, 8:58 AM
Hi again! That seem to work for me. Did you register the action properly? https://www.sanity.io/docs/document-actions#2-register-and-resolve-document-actions-3254d0188d3e
Aug 13, 2020, 10:08 AM
You probably also want to replace the default publish action for that document type, which you can do like in this example https://www.sanity.io/docs/document-actions#selectively-replacing-builtin-actions-b34d4d68c564
Aug 13, 2020, 10:16 AM
Sorry to keep going on about it but I think I'm not getting my head properly around it. As I said before the first
console.log
within the useEffect logs so that should indicate that it's properly registered, right? It's the
console.log
in the
onHandle
callback that doesn't log when I publish the document, should it? I assumed it should but maybe that's wrong.
Aug 14, 2020, 7:48 AM
How does your
resolveDocumentActions.js
look?
Aug 14, 2020, 8:22 AM
Also, if you're not rewriting the default Publish action, your action will be in the dropdown arrow next to the Publish button. For testing purposes it could be wise to label the action differently:

    label: isPublishing ? 'Testing…' : 'My test action',
Aug 14, 2020, 8:25 AM
Thanks for continuing to help, my
resolveDocumentActions.js
looks like this:
import defaultResolve from 'part:@sanity/base/document-actions'

import { UpdateInstanceName } from '../actions/updateInstanceName'

export default function resolveDocumentActions(props) {
  return [...defaultResolve(props), UpdateInstanceName]
}
Aug 14, 2020, 8:28 AM
And you are sure you are executing the right action from the dropdown menu?
Aug 14, 2020, 8:29 AM
I changed the label as you suggested and it appears in the menu now. So if I click that instead of publish it works as I expect. Thank you.
Aug 14, 2020, 8:30 AM
Or rather it always appeared in the menu but I was getting confused about my Publish action and the default Publish action.
Aug 14, 2020, 8:32 AM
Cool!
If you want to replace the Publish
button you will have to do this:

import defaultResolve, {PublishAction} from 'part:@sanity/base/document-actions'
import {UpdateInstanceName} from './updateInstanceName'

export default function resolveDocumentActions(props) {
  return defaultResolve(props)
    .map(Action =>
      Action === PublishAction ? UpdateInstanceName : Action
    )
}
Aug 14, 2020, 8:32 AM
Amazing, thank you.
Aug 14, 2020, 8:33 AM
You're welcome!
Aug 14, 2020, 8:37 AM

Sanity– build remarkable experiences at scale

The Sanity Composable Content Cloud is the 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?