Recipe: Wrapping the default input widget in Sanity CMS

This post shares a recipe for creating a Custom Input Widget in Sanity CMS that wraps the default input widget.

This approach is useful when you want to intercept onChange events and/or add html around the default input widget.

Consider this WrappedDefaultInput custom input widget.

import React, { Fragment } from "react";
import { FormBuilderInput } from "part:@sanity/form-builder";

export default class WrappedDefaultInput extends React.Component {
  // class property, removes need for constructor to set initial state
  state = {
    hasOnChangeBeenCalled: false
  };

  // class property with es6 arrow function binds 'this' for us.
  customOnChangeHandler = patchEvent => {
    console.log("Debug:", { patchEvent });
    if (!this.state.hasOnChangeBeenCalled) {
      this.setState({ hasOnChangeBeenCalled: true });
    }
    this.props.onChange(patchEvent);
  };

  render() {
    const { type = {}, value } = this.props;
    // remove inputComponent property to prevent infinite loop caused by
    // FormBuilderInput resolving to WrappedDefaultInput again and again.
    const { inputComponent, ...restOfType } = type;
    const updatedProps = {
      ...this.props,
      type: restOfType,
      // add a custom onChange function that intercepts the FormBuilderInput's
      // onChange call and logs it out.
      onChange: this.customOnChangeHandler
    };
    return (
      <Fragment>
        <p>
          Has onChange been called:
          {this.state.hasOnChangeBeenCalled ? "true" : "false"}
        </p>
        <FormBuilderInput {...updatedProps} />
        <p>Current value of this schema field: {JSON.stringify(value)} </p>
      </Fragment>
    );
  }
}

Key things to note about this widget:

  • It will work for any schema field.
  • It takes the schema field configuration and removes the custom input widget declaration before passing the updatedProps to the default FormBuilderInput. This avoids crashing the Sanity Studio with an infinite loop where the FormBuilderInput would keep resolving to the WrappedDefaultInput.
  • This approach enables us to add html or css around a complex default input without having to re-create that input.
  • It makes it easier to debug and modify complex onChange events.
Screenshot of custom input component in use.

Here's a screenshot of a personal project with the WrappedDefaultInput in action.

In the screenshot you can see that I’ve wrapped a non-trivial schema field object type that not only has an image field but also string type fields for attribution and caption. Located above and below the default input we find output from our custom input widget. To the right we log out Sanity patchEvents in the browser’s web developer console.

I got to think a lot about future-compatibility when I wrote this code. What I’m especially pleased with is how I pass all of Sanity’s props to the FormBuilderInput (minus the custom input widget). This widget doesn’t need to enumerate all the properties it receives just make sure to pass them on, and it allows for props to change in the future. This is very much in keeping with the thoughts put forth in Maybe Not by Rich Hickey.

My blog is still built with Hugo, but it’s cool to experiment with Sanity since I build projects with it at Netlife.

If this post helps you out, do give me a shoutout on Twitter. :)