Auto-populate fields from API: Document action vs inline button like slug generator

3 replies
Last updated: Aug 18, 2022
I want to implement a document type so that users could fill one field and the rest of the fields would be populated automatically from an external api call based on that
I followed these docs to create a document action:
https://www.sanity.io/docs/document-actions This seems to support what I want to do, but I think using the publish button drop down menu is not the best UX. Ideally I would like something similar to the button that generates a slug based on the value of another field, so that the action would be close to where the editor has to input the item #. Is this passible with document actions? Or should I be doing this some other way?
AI Update

You're on the right track! Document actions aren't the best fit for this use case since they're designed for document-level operations in a dropdown menu. What you actually want is a custom input component with a button right next to the field, similar to how the slug generator works.

The Form Components API is exactly what you need. You can create a custom input component that includes a button positioned near your field to trigger the API call and populate other fields.

Here's a practical example of how to implement this:

import { StringInputProps, set, useFormValue } from 'sanity'
import { Button, Stack, TextInput } from '@sanity/ui'
import { useState } from 'react'

export function ItemNumberInput(props: StringInputProps) {
  const { onChange, value, elementProps } = props
  const [loading, setLoading] = useState(false)

  const handleFetch = async () => {
    if (!value) return
    
    setLoading(true)
    try {
      const response = await fetch(`https://your-api.com/items/${value}`)
      const data = await response.json()
      
      // Populate multiple fields using onChange with paths
      onChange([
        set(value), // Keep current field value
        set(data.title, ['title']), // Populate title field
        set(data.description, ['description']), // Populate description
        set(data.category, ['category']), // Add more fields as needed
      ])
    } catch (error) {
      console.error('Failed to fetch item data:', error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <Stack space={3}>
      <TextInput
        {...elementProps}
        value={value || ''}
        onChange={(event) => onChange(set(event.currentTarget.value))}
      />
      <Button
        text="Fetch Item Details"
        onClick={handleFetch}
        disabled={!value || loading}
        tone="primary"
      />
    </Stack>
  )
}

Then in your schema:

defineField({
  name: 'itemNumber',
  type: 'string',
  title: 'Item Number',
  components: {
    input: ItemNumberInput
  }
})

Key advantages of this approach:

  1. Better UX - The button appears right where editors need it, next to the input field (just like the slug generator)
  2. Real-time updates - Uses Sanity's patch system to update fields immediately
  3. Access to document context - You can read other field values using useFormValue() if needed
  4. Conditional logic - Easy to disable the button when the field is empty or show loading states

For more sophisticated implementations, check out these guides:

If you need to populate fields conditionally only if they're empty, you can use setIfMissing instead of set:

import { setIfMissing } from 'sanity'

onChange([
  set(value),
  setIfMissing(data.title, ['title']), // Only set if title is empty
])

You can also access the entire document context if you need to make decisions based on other field values:

const document = useFormValue([]) as any

This approach gives you much more control than document actions and creates exactly the UX pattern you're looking for - a button-triggered action right next to the field where editors are working!

Show original thread
3 replies
You can also accomplish this with a custom input component. This is a very simple example component I made a while back that fetches from a specified url, then writes to another field in the document.

import React from 'react';
import { Card, TextInput, Button } from '@sanity/ui';
import { FormField } from '@sanity/base/components';
import PatchEvent, { set, unset } from '@sanity/form-builder/PatchEvent';
import { useId } from '@reach/auto-id';
import { withDocument } from 'part:@sanity/form-builder';
import { studioClient } from '../../lib/utils/studioClient';

const UrlWithButton = React.forwardRef((props, ref) => {
  const {
    type, // Schema information
    value, // Current field value
    readOnly, // Boolean if field is not editable
    markers, // Markers including validation rules
    presence, // Presence information for collaborative avatars
    compareValue, // Value to check for "edited" functionality
    onFocus, // Method to handle focus state
    onBlur, // Method to handle blur state
    onChange, // Method to handle patch events,
    document,
  } = props;

  // Creates a change handler for patching data
  const handleChange = React.useCallback(
    // useCallback will help with performance
    event => {
      const inputValue = event.currentTarget.value; // get current value
      // if the value exists, set the data, if not, unset the data
      onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()));
    },
    [onChange]
  );

  //Fetches the URL specfied then uses the JS client to write to the current document
  const handleClick = async () => {
    const res = await fetch(value).then(res => res.json());
    studioClient
      .patch(document._id)
      .set({
        fetchObject: {
          ...document.fetchObject,
          fieldToWrite: res.data[3].breed,
        },
      })
      .commit();
  };

  const inputId = useId();

  return (
    <FormField
      description={type.description} // Creates description from schema
      title={type.title} // Creates label from schema title
      __unstable_markers={markers} // Handles all markers including validation
      __unstable_presence={presence} // Handles presence avatars
      compareValue={compareValue} // Handles "edited" status
      inputId={inputId} // Allows the label to connect to the input field
    >
      <Card padding={2}>
        <TextInput
          id={inputId} // A unique ID for this input
          fontSize={2}
          padding={3}
          space={[3, 3, 4]}
          value={value} // Current field value
          readOnly={readOnly} // If "readOnly" is defined make this field read only
          onFocus={onFocus} // Handles focus events
          onBlur={onBlur} // Handles blur events
          ref={ref}
          onChange={handleChange} // A function to call when the input value changes
        />
      </Card>
      <Card padding={2}>
        <Button
          fontSize={2}
          padding={3}
          text='Fetch'
          tone='primary'
          onClick={handleClick}
        />
      </Card>
    </FormField>
  );
});
export default withDocument(UrlWithButton);

user M
I just ran across this incidentally but what a cool use case.
Thanks
user S
!

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?