static-cms/packages/docs/content/docs/custom-widgets.mdx
2023-02-02 10:21:04 -05:00

425 lines
20 KiB
Plaintext

---
group: Customization
title: Creating Custom Widgets
weight: 40
---
The 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 `registerPreviewTemplate` 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`).
**NOTE**: `createClass` is still provided, allowing for the creation of react class components. However it has now been deprecated and will be removed in `v2.0.0`.
## Register Widget
Register a custom widget.
```js
// 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](https://reactjs.org/docs/components-and-props.html)<br />\| string | <ul><li>`React Function Component` - The react component that renders the control. See [Control Component](#control-component)</li><li>`string` - Name of a registered widget whose control should be used (includes built in widgets).</li></ul> |
| preview | [React Function Component](https://reactjs.org/docs/components-and-props.html) | _Optional_. Renders the widget preview. See [Preview Component](#preview-component) |
| options | object | _Optional_. Widget options. See [Options](#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 |
| field | object | The field configuration for the current widget. See [Widget Options](/docs/widgets#common-widget-options) |
| collection | object | The collection configuration for the current widget. See [Collections](/docs/collection-overview) |
| config | object | The current Static CMS config. See [configuration options](/docs/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 |
| isDisabled | 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 | Specifices if the widget is within a `list` widget |
| isFieldDuplicate | function | Function that given a field configuration, returns if that field is a duplicate |
| isFieldHidden | function | Function that given a field configuration, returns if that field is hidden |
| getAsset | Async function | __Deprecated__ Function that given a url returns (as a promise) a loaded asset |
| locale | string<br />\| undefined | The current locale of the editor |
| mediaPaths | object | Key/value object of control IDs (passed to the media library) mapping to media paths |
| clearMediaControl | function | Clears a control ID's value from the internal store |
| openMediaLibrary | function | Opens the media library popup. See [Open Media Library](#open-media-library) |
| removeInsertedMedia | function | Removes draft media for a give control ID |
| removeMediaControl | function | Clears a control ID completely from the internal store |
| query | function | Runs a search on another collection. See [Query](#query) |
| i18n | object | The current i18n settings |
| t | function | Translates a given key to the current locale |
#### Open Media Library
`openMediaLibrary` allows you to open up the media library popup. It accepts the following props:
| Param | Type | Default | Description |
| ------------- | --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| controlID | string | | _Optional_ A unique identifier to which the uploaded media will be linked |
| forImage | boolean | `false` | _Optional_ If `true`, restricts upload to image files only |
| value | string<br />list of strings | | _Optional_ The current selected media value |
| allowMultiple | boolean | | _Optional_ Allow multiple files or images to be uploaded at once. Only used on media libraries that support multi upload |
| replaceIndex | number | | _Optional_ The index of the image in an list. Ignored if ` allowMultiple` is `false` |
| config | object | | _Optional_ Media library config options. Available options depend on the media library being used |
| field | object | | _Optional_ The current field configuration |
#### 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](/docs/widgets#common-widget-options) |
| collection | object | The collection configuration for the current widget. See [Collections](/docs/collection-overview) |
| config | object | The current Static CMS config. See [configuration options](/docs/configuration-options) |
| entry | object | Object with a `data` field that contains the current value of all widgets in the editor |
| getAsset | Async function | __Deprecated__ Function that given a url returns (as a promise) a loaded asset |
### 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](#advanced-field-validation) |
| schema | JSON Schema object | _Optional_. Enforces a schema for the widget's field configuration |
### Example
<CodeTabs>
```js
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 });
```
```jsx
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 });
```
```tsx
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 });
```
</CodeTabs>
`admin/config.yml` (or `admin/config.js`)
<CodeTabs>
```yaml
collections:
- name: posts
label: Posts
folder: content/posts
fields:
- name: title
label: Title
widget: string
- name: categories
label: Categories
widget: categories
separator: __
```
```js
collections: [
{
name: 'posts',
label: 'Posts',
folder: 'content/posts',
fields: [
{
name: 'title'
label: 'Title'
widget: 'string'
},
{
name: 'categories'
label: 'Categories'
widget: 'categories'
separator: '__'
}
]
}
]
```
</CodeTabs>
## Advanced field validation
All widget fields, including those for built-in widgets, [include basic validation](/docs/widgets/#common-widget-options) 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
```javascript
const validator = () => {
// Do internal validation
return true;
};
```
#### Has Error
```javascript
const validator = () => {
// Do internal validation
return false;
};
```
#### Error With Type
```javascript
const validator = () => {
// Do internal validation
return { type: 'custom-error' };
};
```
#### Error With Type and Message
_Useful for returning custom error messages_
```javascript
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.
```javascript
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.
- `useMediaAsset` - Transforms your stored url into a usable url for displaying as a preview.
<CodeTabs>
```js
const FileControl = ({ collection, field, value, entry, onChange }) => {
const handleOpenMediaLibrary = useMediaInsert(value, { field, controlID }, onChange);
const assetSource = useMediaAsset(value, collection, field, entry);
return [
h('button', { type: 'button', onClick: handleOpenMediaLibrary }, 'Upload'),
h('img', { role: 'presentation', src: assetSource })
];
};
```
```jsx
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
const FileControl = ({ collection, field, value, entry, onChange }) => {
const handleOpenMediaLibrary = useMediaInsert(value, { field, controlID }, onChange);
const assetSource = useMediaAsset(value, collection, field, entry);
return (
<>
<button type="button" onClick={handleOpenMediaLibrary}>
Upload
</button>
<img role="presentation" src={assetSource} />
</>
);
};
```
```tsx
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import type { WidgetControlProps } from '@staticcms/core/interface';
import type { FC } from 'react';
const FileControl: FC<WidgetControlProps<string, MyField>> = ({
collection,
field,
value,
entry,
onChange,
}) => {
const handleOpenMediaLibrary = useMediaInsert(internalValue, { field, controlID }, onChange);
const assetSource = useMediaAsset(value, collection, field, entry);
return (
<>
<button type="button" onClick={handleOpenMediaLibrary}>
Upload
</button>
<img role="presentation" src={assetSource} />
</>
);
};
```
</CodeTabs>