๐Ÿ˜Ž Discover cool tips and tricks for customization in our next Developer Deep Dive virtual event - sign up now!

GROQ Reference Lookup + Add & Replace

By Kevin Green

Run GROQ queries in the studio to populate an array module.

Collection.js

import React from 'react';
import Emoji from 'a11y-react-emoji';

import ReferenceLookup from '../../components/reference-lookup'

const Icon = () => <Emoji style={{fontSize: '2em'}} symbol="๐Ÿ–" />;

export default {
	name: 'collection',
	title: 'Collection',
	icon: Icon,
	type: 'document',
	fields: [
		//
		// === Main ===
		//

		{
      name: 'items',
      type: 'object',
      inputComponent: ReferenceLookup,
      fields: [
        {
          title: 'Query',
          name: 'query',
          type: 'text',
					rows: 2,
          description:
            'Create a Query to look up products/items, quickly fetched many products instead of one at a time:'
        },
      ],
      options: {
        target: 'modules'
      }
    },
		{
			title: 'Modular Items',
			name: 'modules',
			type: 'array',
			of: [
				{
					type: 'reference', title: 'Product Type', name:'productLanding', to: { type: 'productLanding' }
				},
				{
					type: 'reference', title: 'Product Map', name:'productMap', to: { type: 'productMap' }
				},
				{
					type: 'imageAlt'
				}
			]
		}
	]
}

reference-lookup.js

import sanityClient from "part:@sanity/base/client"
import PropTypes from 'prop-types'
import React from 'react'
import { setIfMissing } from 'part:@sanity/form-builder/patch-event'
import {
  FormBuilderInput,
  withDocument,
  withValuePath
} from 'part:@sanity/form-builder'
import {
  Button,
  Card,
  Flex
} from '@sanity/ui'
import fieldStyle from '@sanity/form-builder/lib/inputs/ObjectInput/styles/Field.css'


const isFunction = obj => !!(obj && obj.constructor && obj.call && obj.apply)

class ReferenceLookup extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      items: [],
      openQuery: false
    }
    this.fetchReferences = this.fetchReferences.bind(this)
  }
  static propTypes = {
    type: PropTypes.shape({
      title: PropTypes.string,
      name: PropTypes.string.isRequired,
      fields: PropTypes.array.isRequired,
    }).isRequired,
    level: PropTypes.number,
    value: PropTypes.shape({
      _type: PropTypes.string
    }),
    onFocus: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    onBlur: PropTypes.func.isRequired
  }

  firstFieldInput = React.createRef()

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

  getContext(level = 1) {

    const valuePath = this.props.getValuePath()
    const removeItems = -Math.abs(level)
    return valuePath.length + removeItems <= 0
      ? this.props.document
      : valuePath.slice(0, removeItems).reduce((context, current) => {
        // basic string path
        if (typeof current === 'string') {
          return context[current] || {}
        }

        // object path with key used on arrays
        if (
          typeof current === 'object' &&
          Array.isArray(context) &&
          current._key
        ) {
          return (
            context.filter(
              item => item._key && item._key === current._key
            )[0] || {}
          )
        }
      }, this.props.document)
  }

  handleFieldChange = (field, fieldPatchEvent) => {
    // Whenever the field input emits a patch event, we need to make sure 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

    const { onChange, type } = this.props
    const event = fieldPatchEvent
      .prefixAll(field.name)
      .prepend(setIfMissing({ _type: type.name }))

    onChange(event)
  }

  fetchReferences = async (query) => {
    const fetchedItems = await sanityClient.fetch(query)
    try {
      this.setState({
        items: fetchedItems
      })
    } catch (error) {
      console.log('error', error)
    }
  }

  handleQuery = () => {
    const { value, type } = this.props

    if (value.query) {
      this.fetchReferences(value.query)
    }
  }

  handleSet = async (setType) => {
    const { document, type } = this.props
    let currentModules = document[type.options.target] || []
    if (setType === 'replace') {
      currentModules = document[type.options.target].filter(module => {
        return module._type !== 'reference'
      })
    }
    const stateItems = this.state.items.map(({ _id, _type }) => ({
      _type,
      _ref: _id,
      _key: _id,
      _weak: true
    }))

    const newItems = [
      ...currentModules,
      ...stateItems
    ]
    const currentDoc = {
      _id: document._id,
      [type.options.target]: newItems
    }
    let tx = sanityClient.transaction()
    tx = tx.patch(document._id, p => p.set(currentDoc))
    const result = await tx.commit()
    try {
      this.setState({
        items: []
      })
    } catch (err) {
      console.log(err)
    }
  }

  render() {
    const { type, value, level, onFocus, onBlur } = this.props

    return (
      <React.Fragment>
        {type.fields.map((field, i) => (

          <React.Fragment>
            {this.state.openQuery && (
              <div className={fieldStyle.root} key={i} style={{ marginBottom: '8px', marginTop: '8px' }}>
                <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]}
                  onFocus={onFocus}
                  onBlur={onBlur}
                />
              </div>
            )}

            {this.state.openQuery === true && field.name === 'query' && (
              <Button tone='primary'
                padding={[2, 2, 2]} onClick={() => this.handleQuery()} text='Run Query' />
            )}
            {this.state.openQuery === true && field.name === 'query' && this.state.items.length > 0 && (
              <React.Fragment>
                <div style={{ display: 'block', fontSize: '12px', marginTop: '8px' }} >Found {this.state.items.length} items, what would you like to do?</div>
                <div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
                  <Card padding={[0, 2]}><Button tone='primary'
                    padding={[2, 2, 2]} onClick={() => this.handleSet('replace')} text={`Replace Products with ${this.state.items.length} Items`} /></Card>
                  <Card padding={[2, 2]}><Button tone='primary'
                    padding={[2, 2, 2]} onClick={() => this.handleSet('add')} text={`Add ${this.state.items.length} Items`} /></Card>
                </div>
              </React.Fragment>
            )}
            {this.state.openQuery === true && field.name === 'query' && (
              <div style={{ fontSize: '12px', marginBottom: '10px', marginTop: '8px' }}>Learn about queries via the <a href='https://www.sanity.io/docs/query-cheat-sheet'>Sanity Cheat Sheet</a></div>
            )}
            <Flex align='flex-end'>
              <Button padding={[2, 2, 2]} onClick={() => this.setState({ openQuery: !this.state.openQuery })} text={this.state.openQuery ? 'Close Query' : 'Toggle Custom Query'} />
            </Flex>
          </React.Fragment>
        ))}
      </React.Fragment>
    )
  }
}

export default withValuePath(withDocument(ReferenceLookup))

The reference component purpose is to quickly use GROQ inline in your content studio to lookup items of varied complexity and add them to an array. This was the result of me needed to build large arrays of content like products for Shopify like tagging/population.

As a developer it's easy to write a query and search for specific types of content to quickly add to a modular array input. Not only that but I wanted to make sure manually entered content was maintained as well, as a result you can either replace items or add them to the module when using the input. Probably needs some refactoring per use case but a great start for building out custom fetching into your sanity array instances.

Contributor