How to Set the Value of a Read Only String Field in a Document Based on Other Fields?

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?
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>
    )
  }
}
I need to be able to get the name field from the region and language references.
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.
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)
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.
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?
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
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
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.
How does your
resolveDocumentActions.js
look?
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',
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]
}
And you are sure you are executing the right action from the dropdown menu?
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.
Or rather it always appeared in the menu but I was getting confused about my Publish action and the default Publish action.
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
    )
}
Amazing, thank you.
You're welcome!

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?