425 lines
20 KiB
Plaintext
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>
|