Portable Text Validator

By Corey Ward

Generate a custom validator function for Portable Text fields using built-in routines

blockValidator.js

/**
 * Validator-generator for Portable Text fields
 *
 * @param {Object} options
 * @param {Boolean} options.required Require a value to be set in this field?
 * @param {Boolean} options.noEmptyBlocks Prevent zero-length blocks?
 * @param {Boolean} options.validateLinks Ensure links have required attributes?
 * @param {Boolean} options.styleRequired Ensure blocks have an associated style?
 */
export const blockValidator = options => {
  const { required, ...customValidators } = options

  return Rule =>
    [
      required && Rule.required(),
      ...Object.entries(customValidators)
        .filter(([, value]) => value)
        .map(([name]) => Rule.custom(validators[name])),
    ].filter(Boolean)
}

export default blockValidator({
  required: true,
  noEmptyBlocks: true,
  styleRequired: true,
  validateLinks: true,
})

const validators = {
  // https://www.sanity.io/docs/validation#validating-children-9e69d5db6f72
  noEmptyBlocks: blocks => {
    const emptyBlocks = (blocks || []).filter(
      block =>
        block._type === "block" &&
        block.children.every(
          span => span._type === "span" && span.text.trim() === ""
        )
    )

    const emptyPaths = emptyBlocks.map(
      (block, index) => [{ _key: block._key }] || [index]
    )

    return emptyPaths.length === 0
      ? true
      : {
          message: "Paragraph cannot be empty",
          paths: emptyPaths,
        }
  },

  // Links without href attributes
  validateLinks: blocks => {
    const errorPaths = (blocks || [])
      .filter(
        block =>
          block._type === "block" &&
          block.markDefs.some(
            def => def._type === "link" && !(def.href && def.href.trim() !== "")
          )
      )
      .map(block => [{ _key: block._key }])

    return errorPaths.length === 0
      ? true
      : {
          message: "Links must have a url set",
          paths: errorPaths,
        }
  },

  // Ensure all blocks have a `style`
  styleRequired: blocks => {
    const emptyPaths = (blocks || [])
      .filter(block => block._type === "block" && !block.style)
      .map((block, index) => [{ _key: block._key }] || [index])

    return emptyPaths.length === 0
      ? true
      : {
          message: "Must have a style selected",
          paths: emptyPaths,
        }
  },
}

This makes it easy to add these common validation patterns to Portable Text fields, as well as provides a structure to create custom validators:

  • Ensure no empty blocks exist
  • Ensure all links have href
  • Ensure all blocks have associated style type
  • Ensuring a value has been provided

Contributor

Other schemas by author

Auto-reload Studio when changes are deployed

Drop this into your Studio to let editors know when there's a more recent version of your Studio available, making sure they have the latest fields and validations.

Go to Auto-reload Studio when changes are deployed