Index
Edit

Custom Input Widgets

Extending Sanity with your own real-time user interface components is surprisingly easy.

Creating custom form-builder widgets is straight-forward. 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

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 '@sanity/form-builder/PatchEvent'

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
  };

  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))}
        />
      </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.

Note: You will find more information about the patch format used internally by the form-builder below.

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'
}

Advanced

The Slider component from the example above is functional, but very rudimentary. Other things you might also want are:

  • Rendering of validation errors and/or warnings [TODO]
  • Display presence/realtime information [TODO]
  • Coherent styling

Styling

The easiest way to get coherent styling is to use one of our pre-packaged components found [TODO here] or in the Storybook [TODO] Here you'll find a comprehensive set of pre-made composable React components that will give your custom widgets a style that matches the rest of the studio.

Lets try to 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 '@sanity/form-builder/PatchEvent' 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 }; 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))} /> </FormField> ) } }

Now it should look a lot better, but it is still does not look exactly what you envisioned. Maybe it needs more unicorns?

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

.slider {
  /*
    ...awesome unicorn styling
    see: https://github.com/sanity-io/sanity/blob/f7f8ec60ce14be37e497cc9f7e72a3d3b921a108/packages/example-studio/components/Slider/styles.css
  */
}
import React, {PropTypes} from 'react'
import FormField from 'part:@sanity/components/formfields/default'
import styles from './slider.css'
import PatchEvent, {set, unset} from '@sanity/form-builder/PatchEvent' 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 }; 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))} /> </FormField> ) } }

Magic, huh?

Previous: PluginsNext: Deployment