1. Home
  2. Docs
  3. docs
  4. Customizing Static CMS
  5. Creating Custom Widgets

Creating Custom Widgets

Static CMS exposes a window.CMS global object that you can use to register custom widgets via registerWidget. The same object is also the default export if you import Static CMS as an npm module.

React Components Inline

The registerWidget requires you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process.

However, although possible, it may be cumbersome or even impractical to add a React build phase. For this reason, Static CMS exposes some constructs globally to allow you to create components inline: h (alias for React.createElement) as well some basic hooks (useState, useMemo, useEffect, useCallback).

Register Widget

Register a custom widget.

// Using global window object
CMS.registerWidget(name, control, [preview], [{ schema }]);

// Using npm module import
import CMS from '@staticcms/core';

CMS.registerWidget(name, control, [preview], [{ schema }]);

Params

Param

Type

Description

name

string

Widget name, allows this widget to be used via the field widget property in config

control

React Function Component
| string

React Function Component – The react component that renders the control. See Control Componentstring – Name of a registered widget whose control should be used (includes built in widgets).

preview

React Function Component

Optional. Renders the widget preview. See Preview Component

options

object

Optional. Widget options. See Options

Control Component

The react component that renders the control. It receives the following props:

Param

Type

Description

label

string

The label for the widget

value

An valid widget value

The current value of the widget

onChange

function

Function to be called when the value changes. Accepts a valid widget value

clearChildValidation

function

Clears all validation errors for children of the widget

field

object

The field configuration for the current widget. See Widget Options

collection

object

The collection configuration for the current widget. See Collections

collectionFile

object

The collection file configuration for the current widget if entry is part of a File Collection

config

object

The current Static CMS config. See configuration options

entry

object

Object with a data field that contains the current value of all widgets in the editor

path

string

. separated string donating the path to the current widget within the entry

hasErrors

boolean

Specifies if there are validation errors with the current widget

fieldsErrors

object

Key/value object of field names mapping to validation errors

disabled

boolean

Specifies if the widget control should be disabled

submitted

boolean

Specifies if a save attempt has been made in the editor session

forList

boolean

Specifies if the widget is within a list widget

listItemPath

string
| undefined

. separated string donating the path to the closet parent list item within the entry

forSingleList

boolean

Specifies if the widget is within a singleton list widget (string array, number array, etc)

duplicate

function

Specifies if that field is an i18n duplicate

hidden

function

Specifies if that field should be hidden

locale

string
| undefined

The current locale of the editor

query

function

Runs a search on another collection. See Query

i18n

object

The current i18n settings

t

function

Translates a given key to the current locale

Query

query allows you to search the entries of a given collection. It accepts the following props:

Param

Type

Default

Description

namespace

string

Unique identifier for search

collectionName

string

The collection to be searched

searchFields

list of strings

The Fields to be searched within the target collection

searchTerm

string

The term to search with

file

string

Optional The file in a file collection to search. Ignored on folder collections

limit

string

Optional The number of results to return. If not specified, all results are returned

Preview Component

The react component that renders the preview. It receives the following props:

Param

Type

Description

value

An valid widget value

The current value of the widget

field

object

The field configuration for the current widget. See Widget Options

collection

object

The collection configuration for the current widget. See Collections

config

object

The current Static CMS config. See configuration options

entry

object

Object with a data field that contains the current value of all widgets in the editor

Options

Register widget takes an optional object of options. These options include:

Param

Type

Description

validator

function

Optional. Validates the value of the widget

getValidValue

string

Optional. Given the current value, returns a valid value. See Advanced field validation

schema

JSON Schema object

Optional. Enforces a schema for the widget’s field configuration

Example

const CategoriesControl = ({ label, value, field, onChange }) => {
  const separator = useMemo(() => field.separator ?? ', ', [field.separator]);

  const handleChange = useCallback((e) => {
    onChange(e.target.value.split(separator).map(e => e.trim()));
  }, [separator, onChange]);

  return h('div', {}, {
    h('label', { for: 'inputId' }, label),
    h('input', {
      id: 'inputId',
      type: 'text',
      value: value ? value.join(separator) : '',
      onChange: handleChange,
    })
  });
};

const CategoriesPreview = ({ value }) => {
  return h(
    'ul',
    {},
    value.map((val, index) => {
      return h('li', { key: index }, val);
    }),
  );
};

const schema = {
  properties: {
    separator: { type: 'string' },
  },
};

CMS.registerWidget('categories', CategoriesControl, CategoriesPreview, { schema });
import CMS from '@staticcms/core';

const CategoriesControl = ({ label, value, field, onChange }) => {
  const separator = useMemo(() => field.separator ?? ', ', [field.separator]);

  const handleChange = useCallback(
    e => {
      onChange(e.target.value.split(separator).map(e => e.trim()));
    },
    [separator, onChange],
  );

  return (
    <div>
      <label for="inputId">{label}</label>
      <input
        id="inputId"
        type="text"
        value={value ? value.join(separator) : ''}
        onChange={handleChange}
      />
    </div>
  );
};

const CategoriesPreview = ({ value }) => {
  return (
    <ul>
      {value.map((val, index) => {
        return <li key={index}>{value}</li>;
      })}
    </ul>
  );
};

const schema = {
  properties: {
    separator: { type: 'string' },
  },
};

CMS.registerWidget('categories', CategoriesControl, CategoriesPreview, { schema });
import CMS from '@staticcms/core';

import type { WidgetControlProps, WidgetPreviewProps } from '@staticcms/core';

interface CategoriesField {
  widget: 'categories';
}

const CategoriesControl = ({
  label,
  value,
  field,
  onChange,
}: WidgetControlProps<string[], CategoriesField>) => {
  const separator = useMemo(() => field.separator ?? ', ', [field.separator]);

  const handleChange = useCallback(
    e => {
      onChange(e.target.value.split(separator).map(e => e.trim()));
    },
    [separator, onChange],
  );

  return (
    <div>
      <label for="inputId">{label}</label>
      <input
        id="inputId"
        type="text"
        value={value ? value.join(separator) : ''}
        onChange={handleChange}
      />
    </div>
  );
};

const CategoriesPreview = ({ value }: WidgetPreviewProps<string[], CategoriesField>) => {
  return (
    <ul>
      {value.map((val, index) => {
        return <li key={index}>{value}</li>;
      })}
    </ul>
  );
};

const schema = {
  properties: {
    separator: { type: 'string' },
  },
};

CMS.registerWidget('categories', CategoriesControl, CategoriesPreview, { schema });

admin/config.yml (or admin/config.js)

collections:
  - name: posts
    label: Posts
    folder: content/posts
    fields:
      - name: title
        label: Title
        widget: string
      - name: categories
        label: Categories
        widget: categories
        separator: __
collections: [
  {
    name: 'posts',
    label: 'Posts',
    folder: 'content/posts',
    fields: [
      {
        name: 'title'
        label: 'Title'
        widget: 'string'
      },
      {
        name: 'categories'
        label: 'Categories'
        widget: 'categories'
        separator: '__'
      }
    ]
  }
]

Advanced field validation

All widget fields, including those for built-in widgets, include basic validation capability using the required and pattern options.

With custom widgets, the widget can also optionally pass in a validator method to perform custom validations, in addition to presence and pattern. The validator function will be automatically called, and it can return either a boolean value, an object with a type and error message or a promise.

Examples

No Errors

const validator = () => {
  // Do internal validation
  return true;
};

Has Error

const validator = () => {
  // Do internal validation
  return false;
};

Error With Type

const validator = () => {
  // Do internal validation
  return { type: 'custom-error' };
};

Error With Type and Message

Useful for returning custom error messages

const validator = () => {
  // Do internal validation
  return { type: 'custom-error', message: 'Your error message.' };
};

Promise

You can also return a promise from validator. The promise can return boolean value, an object with a type and error message or a promise.

const validator = () => {
  return this.existingPromise;
};

Interacting With The Media Library

If you want to use the media library in your custom widget you will need to use the useMediaInsert and useMediaAsset hooks.

  • useMediaInsert – Takes the current url to your media, details about your field (including a unique ID) and a callback method for when new media is uploaded. If you want to select folders instead of files, set the forFolder variable in options.
  • useMediaAsset – Transforms your stored url into a usable url for displaying as a preview.
const FileControl = ({ collection, field, value, entry, onChange }) => {
  const handleChange = ({ path }) => {
    onChange(path);
  };

  const handleOpenMediaLibrary = useMediaInsert(value, { collection, field, controlID }, onChange);

  const assetSource = useMediaAsset(value, collection, field, entry);

  return [
    h('button', { type: 'button', onClick: handleOpenMediaLibrary }, 'Upload'),
    h('img', { role: 'presentation', src: assetSource }),
  ];
};
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';

const FileControl = ({ collection, field, value, entry, onChange }) => {
  const handleChange = ({ path }) => {
    onChange(path);
  };

  const handleOpenMediaLibrary = useMediaInsert(
    value,
    { collection, field, controlID },
    handleChange,
  );

  const assetSource = useMediaAsset(value, collection, field, entry);

  return (
    <>
      <button type="button" onClick={handleOpenMediaLibrary}>
        Upload
      </button>
      <img role="presentation" src={assetSource} />
    </>
  );
};
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';

import type { WidgetControlProps, MediaPath } from '@staticcms/core/interface';
import type { FC } from 'react';

const FileControl: FC<WidgetControlProps<string, MyField>> = ({
  collection,
  field,
  value,
  entry,
  onChange,
}) => {
  const handleChange = ({ path }: MediaPath) => {
    onChange(path);
  };

  const handleOpenMediaLibrary = useMediaInsert(
    internalValue,
    { collection, field, controlID },
    onChange,
  );

  const assetSource = useMediaAsset(value, collection, field, entry);

  return (
    <>
      <button type="button" onClick={handleOpenMediaLibrary}>
        Upload
      </button>
      <img role="presentation" src={assetSource} />
    </>
  );
};