Index
Edit

Custom Input Widgets

Extending the Content Studio with your own UI components is surprisingly easy. Any React component can be used as an input, as long as it follows a few core conventions:

Given the props:

  • value - the current value or undefined if no value is present
  • onChange - a function to call when the value should be updated

It must:

  • Present an interface that allows editing props.value, e.g. through an <input ...> element
  • Call props.onChange with a patch describing the operation that should be applied on the value

In addition every input component should implement a focus method, that sets focus on the underlying input element.

It's worth noting that all input widgets are controlled components. This means that calling props.onChange with a patch describing the mutation is the only way to update and receive an updated props.value. In addition, all input components must be able to handle undefined as its props.value

Example: Implement a custom Slider

Lets say we'd like to use a custom slider component for editing schema types of number that has a range option, e.g.:

{
  name: 'rating',
  title: 'Rating',
  type: 'number',
  options: {
range: {min: 0, max: 10, step: 0.2}
} }

Lets create a simple Slider component:

import React, {PropTypes} from 'react'

import PatchEvent, {set, unset} from 'part:@sanity/form-builder/patch-event'

const createPatchFrom = value => PatchEvent.from(value === '' ? unset() : set(Number(value)))

export default class Slider extends React.Component {
  static propTypes = {
    type: PropTypes.shape({
      title: PropTypes.string,
      options: PropTypes.shape({
        min: PropTypes.number.isRequired,
        max: PropTypes.number.isRequired,
        step: PropTypes.number
      }).isRequired
    }).isRequired,
value: PropTypes.number,
onChange: PropTypes.func.isRequired
}; // this is called by the form builder whenever this input should receive focus
focus() {
this._inputElement.focus()
}
render() { const {type, value, onChange} = this.props const {min, max, step} = type.options.range return ( <div> <h2>{type.title}</h2> <input type="range" min={min} max={max} step={step}
value={value === undefined ? '' : value}
onChange={event => onChange(createPatchFrom(event.target.value))}
ref={element => this._inputElement = element}
/> </div> ) } }

This slider provides an input with value of props.value and updates it by calling props.onChange whenever the user changes the value.

Note: All form builder input components should be able to receive undefined as props.value. This indicates "no value". However, <input> components in React will interpret a value of undefined as "this is not a controlled component", and warn if the same input is later passed a non-undefined value. As a consequence, it is usually a good idea to pass something other than undefined to an underlying <input>-component. If you are unsure what default value to use for signalling "empty value", it is usually safe to use an empty string or null.

Also note: You'll find more information below, regarding the patch format used internally by the form-builder.

Now, this widget is functionally complete, but it won't appear in forms yet. To achieve that, we'll need the last missing piece:

Mapping schema types to custom widgets

There are two ways to associate a schema type with a custom input widget:

  1. Set the one-off inputComponent property on the type/field
  2. Implement a custom input resolver to control the input widget for a set of types that matches certain criteria.

Setting inputComponent

The inputComponent property can be set on all types and fields and will override the default input widget.

import MyCustomStringInput from '../components/MyCustomStringInput'
//...
{
  type: 'object',
  name: 'typeWithCustomStringInput',
  fields: [
    {
      type: 'string',
      name: 'myString',
inputComponent: MyCustomStringInput
} //... ] }

Implement a custom input resolver

If you want more fine grained, programmatic control, you can implement a custom input resolver. To do this, you must provide a function that implements the part:

part:@sanity/form-builder/input-resolver

Example

We start by creating an empty file, lets just call it inputResolver.js and, for now, just paste in the following snippet:

export default function resolveInput(type) {
  //
}

Note: The relative path of this file is not important, we are free to organize the code in our sanity studio as we see fit, but it is usually a good idea to put modules that implements single parts in a folder named ./parts, and components in ./components

Next up, we register it as implementation of the part "part:@sanity/form-builder/input-resolver" in our sanity.json:

{
  "implements": "part:@sanity/form-builder/input-resolver",
  "path": "./inputResolver.js"
}
import Slider from './Slider'

export default function resolveInput(type) {
  if (type.name === 'number' && type.options && type.options.range) {
    return Slider
  }
}

That's it. You should now see a dull, but workable slider in your forms for number types/fields that has a range option.

Tip: You can use the resolveInput() function above to support other custom input widgets too, or have fine grained control over which components that should be used for different types. If it returns nothing/undefined, the default input widget will be used instead.

Patch format

Currently the following patch types are supported:

{
  type: 'set' | 'unset' | 'setIfMissing',
  path?: Array<string>,
  value?: any
}

A good convention is to create an unset patch if the value should be considered "empty".

path is optional and only needed if your input deals with complex data structures. For example if you've made a custom geopoint widget and wishes to only update the latitude property, you can address it in the paths array:

{
  type: 'set',
  path: ['latitude'],
  value: '59.918055'
}

Styling the Slider

Let's wrap our Slider using a the <FormField /> component from the @sanity/components plugin:

import React, {PropTypes} from 'react'
import FormField from 'part:@sanity/components/formfields/default'
import PatchEvent, {set, unset} from 'part:@sanity/form-builder/patch-event' const createPatchFrom = value => PatchEvent.from(value === '' ? unset() : set(Number(value))) export default class Slider extends React.Component { static propTypes = { type: PropTypes.shape({ title: PropTypes.string, options: PropTypes.shape({ min: PropTypes.number.isRequired, max: PropTypes.number.isRequired, step: PropTypes.number }).isRequired }).isRequired, value: PropTypes.number, onChange: PropTypes.func.isRequired }; focus() { this._inputElement.focus() } render() { const {type, value, onChange} = this.props const {min, max, step} = type.options.range return (
<FormField label={type.title} description={type.description}> <input
type="range" min={min} max={max} step={step} value={value === undefined ? '' : value} onChange={event => onChange(createPatchFrom(event.target.value))} ref={element => this._inputElement = element} /> </FormField> ) } }

This looks better, but we're not quite there yet. We need a unicorn!

Ok, lets add a CSS file: ./slider.css

.slider {
  /*
    ...awesome unicorn styling
    see: https://github.com/sanity-io/unicorn-slider/blob/master/src/components/slider.css
  */
}
import React, {PropTypes} from 'react'
import FormField from 'part:@sanity/components/formfields/default'
import styles from './slider.css'
import PatchEvent, {set, unset} from 'part:@sanity/form-builder/patch-event' const createPatchFrom = value => PatchEvent.from(value === '' ? unset() : set(Number(value))) export default class Slider extends React.Component { static propTypes = { type: PropTypes.shape({ title: PropTypes.string, options: PropTypes.shape({ min: PropTypes.number.isRequired, max: PropTypes.number.isRequired, step: PropTypes.number }).isRequired }).isRequired, value: PropTypes.number, onChange: PropTypes.func.isRequired }; focus() { this._inputElement.focus() } render() { const {type, value, onChange} = this.props return ( <FormField label={type.title} description={type.description}> <input type="range" className={styles.slider}
min={min}
max={max} step={step} value={value === undefined ? '' : value} onChange={event => onChange(createPatch(event.target.value))} ref={element => this._inputElement = element} /> </FormField> ) } }

Magic, huh?

Previous: PluginsNext: Example Plugins