docs: update examples (#411)

This commit is contained in:
Daniel Lautzenheiser 2023-01-23 13:20:17 -05:00 committed by GitHub
parent fcfbd6de13
commit cab7166eb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 585 additions and 158 deletions

View File

@ -374,12 +374,12 @@ export function getAdditionalLink(id: string): AdditionalLink | undefined {
/**
* Markdown editor shortcodes
*/
export function registerShortcode(name: string, config: ShortcodeConfig) {
export function registerShortcode<P = {}>(name: string, config: ShortcodeConfig<P>) {
if (registry.backends[name]) {
console.error(`Shortcode [${name}] already registered. Please choose a different name.`);
return;
}
registry.shortcodes[name] = config;
registry.shortcodes[name] = config as unknown as ShortcodeConfig;
}
export function getShortcode(name: string): ShortcodeConfig {

View File

@ -50,6 +50,8 @@ CMS.registerAdditionalLink({
### Custom Page
<CodeTabs>
```js
const CustomPage = () => {
return h('div', {}, 'I am a custom page!');
@ -64,3 +66,37 @@ CMS.registerAdditionalLink({
},
});
```
```jsx
const CustomPage = () => {
return <div>I am a custom page!</div>;
};
CMS.registerAdditionalLink({
id: 'custom-page',
title: 'Custom Page',
data: CustomPage,
options: {
icon: 'page',
},
});
```
```tsx
import type { FC } from 'react';
const CustomPage: FC = () => {
return <div>I am a custom page!</div>;
};
CMS.registerAdditionalLink({
id: 'custom-page',
title: 'Custom Page',
data: CustomPage,
options: {
icon: 'page',
},
});
```
</CodeTabs>

View File

@ -19,6 +19,8 @@ Custom icons can be used with [Collections](/docs/collection-overview) or [Custo
This example uses Font Awesome to supply the icon.
<CodeTabs>
```js
import { faHouse } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -27,6 +29,24 @@ import CMS from '@staticcms/core';
CMS.registerIcon('house', () => h(FontAwesomeIcon, {icon=faHouse, size="lg"}));
```
```jsx
import { faHouse } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import CMS from '@staticcms/core';
CMS.registerIcon('house', () => <FontAwesomeIcon icon={faHouse} size="lg" />);
```
```tsx
import { faHouse } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import CMS from '@staticcms/core';
CMS.registerIcon('house', () => <FontAwesomeIcon icon={faHouse} size="lg" />);
```
</CodeTabs>
## Usage
### Collection

View File

@ -34,124 +34,278 @@ The following parameters will be passed to your `react_component` during render:
### Example
```html
<script src="https://unpkg.com/@staticcms/app@^1.0.0/dist/static-cms-app.js"></script>
<script>
const PostPreview = ({ widgetFor, getAsset, entry }) => {
const [imageUrl, setImageUrl] = useState('');
const image = useMemo(() => entry.data.image, [entry.data.image]);
<CodeTabs>
useEffect(() => {
let alive = true;
```js
const PostPreview = ({ widgetFor, getAsset, entry, collection, field }) => {
const imageUrl = useMediaAsset(entry.data.image, collection, field, entry);
const loadImage = async () => {
const imageAsset = await getAsset(image);
if (alive) {
setImageUrl(imageAsset.toString());
}
};
return h(
'div',
{},
h('h1', {}, entry.data.title),
h('img', { src: imageUrl }),
h('div', { className: 'text' }, widgetFor('body')),
);
};
loadImage();
return () => {
alive = false;
};
}, [image]);
return h(
'div',
{},
h('h1', {}, entry.data.title),
h('img', { src: imageUrl }),
h('div', { classtitle: 'text' }, widgetFor('body')),
);
});
CMS.registerPreviewTemplate('posts', PostPreview);
</script>
CMS.registerPreviewTemplate('posts', PostPreview);
```
```jsx
import CMS, { useMediaAsset } from '@staticcms/core';
const PostPreview = ({ widgetFor, getAsset, entry, collection, field }) => {
const imageUrl = useMediaAsset(entry.data.image, collection, field, entry);
return (
<div>
<h1>{entry.data.title}</h1>
<img src={imageUrl} />
<div className='text'>{widgetFor('body')}</div>
</div>
);
};
CMS.registerPreviewTemplate('posts', PostPreview);
```
```tsx
import CMS, { useMediaAsset } from '@staticcms/core';
import type { TemplatePreviewProps } from '@staticcms/core';
/**
* The type for 'entry.data'
*/
interface Post {
image: string;
title: string;
body: string;
}
const PostPreview = ({ widgetFor, getAsset, entry, collection, field }: TemplatePreviewProps<Post>) => {
const imageUrl = useMediaAsset(entry.data.image, collection, field, entry);
return (
<div>
<h1>{entry.data.title}</h1>
<img src={imageUrl} />
<div className='text'>{widgetFor('body')}</div>
</div>
);
};
CMS.registerPreviewTemplate('posts', PostPreview);
```
</CodeTabs>
### Lists and Objects
The API for accessing the individual fields of list- and object-type entries is similar to the API for accessing fields in standard entries, but there are a few key differences. Access to these nested fields is facilitated through the `widgetsFor` function, which is passed to the preview template component during render.
**List Example:**
#### List Template Example
```html
<script>
// For list fields, the widgetFor function returns an array of objects
// that you can map over in your template. If our field is a list of
// authors containing two entries, with fields `name` and `description`,
// the return value of `widgetsFor` would look like this:
//
// [{
// data: { title: 'Mathias', description: 'Co-Founder'},
// widgets: { title: (<WidgetComponent>), description: (WidgetComponent>)}
// },
// {
// data: { title: 'Chris', description: 'Co-Founder'},
// widgets: { title: (<WidgetComponent>), description: (WidgetComponent>)}
// }]
//
// Templating would look something like this:
const AuthorsPreview = ({ widgetsFor }) => {
return h(
'div',
{},
For list fields, the widgetFor function returns an array of objects that you can map over in your template. If your field is a list of authors containing two entries, with fields `name` and `description`, the return value of `widgetsFor` would look like this:
// This is a static header that would only be rendered once for the entire list
h('h1', {}, 'Authors'),
// Here we provide a simple mapping function that will be applied to each
// object in the array of authors
widgetsFor('authors').map(function (author, index) {
return h(
'div',
{ key: index },
h('hr', {}),
h('strong', {}, author.data.name),
author.widgets.description,
);
}),
);
};
CMS.registerPreviewTemplate('authors', AuthorsPreview);
</script>
```js
[{
data: { title: 'Mathias', description: 'Co-Founder'},
widgets: { title: <WidgetComponent>, description: <WidgetComponent>}
},
{
data: { title: 'Chris', description: 'Co-Founder'},
widgets: { title: <WidgetComponent>, description: <WidgetComponent>}
}]
```
**Object Example:**
<CodeTabs>
```html
<script>
// Object fields are simpler than lists - instead of `widgetsFor` returning
// an array of objects, it returns a single object. Accessing the shape of
// that object is the same as the shape of objects returned for list fields:
//
// {
// data: { front_limit: 0, author: 'Chris' },
// widgets: { front_limit: (<WidgetComponent>), author: (WidgetComponent>)}
// }
const GeneralPreview = ({ entry, widgetsFor }) => {
const title = entry.data.site_title;
const posts = entry.data.posts;
```js
const AuthorsPreview = ({ widgetsFor }) => {
return h(
'div',
{},
return h(
'div',
{},
h('h1', {}, title),
h(
'dl',
{},
h('dt', {}, 'Posts on Frontpage'),
h('dd', {}, widgetsFor('posts').widgets.front_limit || 0),
// This is a static header that would only be rendered once for the entire list
h('h1', {}, 'Authors'),
h('dt', {}, 'Default Author'),
h('dd', {}, widgetsFor('posts').data.author || 'None'),
),
);
};
// Here we provide a simple mapping function that will be applied to each
// object in the array of authors
widgetsFor('authors').map(function (author, index) {
return h(
'div',
{ key: index },
h('hr', {}),
h('strong', {}, author.data.name),
author.widgets.description,
);
}),
);
};
CMS.registerPreviewTemplate('general', GeneralPreview);
</script>
CMS.registerPreviewTemplate('authors', AuthorsPreview);
```
```jsx
import CMS from '@staticcms/core';
const AuthorsPreview = ({ widgetsFor }) => {
return (
<div>
{/* This is a static header that would only be rendered once for the entire list */}
<h1>Authors</h1>
{/* Here we provide a simple mapping function that will be applied to each object in the array of authors */}
{widgetsFor('authors').map((author, index) => (
<div key={index}>
<hr />
<strong>{author.data.name}</strong>
{author.widgets.description}
</div>
))}
</div>
);
};
CMS.registerPreviewTemplate('authors', AuthorsPreview);
```
```tsx
import CMS from '@staticcms/core';
import type { TemplatePreviewProps } from '@staticcms/core';
interface Author {
name: string;
description: string;
}
/**
* The type for 'entry.data'
*/
interface Authors {
authors: Author[];
}
const AuthorsPreview = ({ widgetsFor }: TemplatePreviewProps<Authors>) => {
return (
<div>
{/* This is a static header that would only be rendered once for the entire list */}
<h1>Authors</h1>
{/* Here we provide a simple mapping function that will be applied to each object in the array of authors */}
{widgetsFor('authors').map((author, index) => (
<div key={index}>
<hr />
<strong>{author.data.name}</strong>
{author.widgets.description}
</div>
))}
</div>
);
};
CMS.registerPreviewTemplate('authors', AuthorsPreview);
```
</CodeTabs>
#### Object Example
Object fields are simpler than lists - instead of `widgetsFor` returning an array of objects, it returns a single object. Accessing the shape of that object is the same as the shape of objects returned for list fields:
```js
{
data: { front_limit: 0, author: 'Chris' },
widgets: { front_limit: <WidgetComponent>, author: <WidgetComponent>}
}
```
<CodeTabs>
```js
const GeneralPreview = ({ entry, widgetsFor }) => {
const title = entry.data.site_title;
const posts = entry.data.posts;
return h(
'div',
{},
h('h1', {}, title),
h(
'dl',
{},
h('dt', {}, 'Posts on Frontpage'),
h('dd', {}, widgetsFor('posts').widgets.front_limit || 0),
h('dt', {}, 'Default Author'),
h('dd', {}, widgetsFor('posts').data.author || 'None'),
),
);
};
CMS.registerPreviewTemplate('general', GeneralPreview);
```
```jsx
import CMS from '@staticcms/core';
const GeneralPreview = ({ entry, widgetsFor }) => {
const title = entry.data.site_title;
const posts = entry.data.posts;
return (
<div>
<h1>{title}</h1>
<dl>
<dt>Posts on Frontpage</dt>
<dd>{widgetsFor('posts').widgets.front_limit || 0)}</dd>
<dt>Default Author</dt>
<dd>{widgetsFor('posts').data.author || 'None')}</dd>
</dl>
</div>
);
};
CMS.registerPreviewTemplate('general', GeneralPreview);
```
```tsx
import CMS from '@staticcms/core';
import type { TemplatePreviewProps } from '@staticcms/core';
interface Posts {
front_limit?: number;
author?: string;
}
/**
* The type for 'entry.data'
*/
interface GeneralSettings {
site_title: string;
posts: Posts;
}
const GeneralPreview = ({ entry, widgetsFor }: TemplatePreviewProps<GeneralSettings>) => {
const title = entry.data.site_title;
const posts = entry.data.posts;
return (
<div>
<h1>{title}</h1>
<dl>
<dt>Posts on Frontpage</dt>
<dd>{widgetsFor('posts').widgets.front_limit || 0)}</dd>
<dt>Default Author</dt>
<dd>{widgetsFor('posts').data.author || 'None')}</dd>
</dl>
</div>
);
};
CMS.registerPreviewTemplate('general', GeneralPreview);
```
</CodeTabs>

View File

@ -24,6 +24,7 @@ CMS.registerWidget(name, control, [preview], [{ schema }]);
// Using npm module import
import CMS from '@staticcms/core';
CMS.registerWidget(name, control, [preview], [{ schema }]);
```
@ -120,49 +121,136 @@ Register widget takes an optional object of options. These options include:
### Example
`admin/index.html`
<CodeTabs>
```html
<script src="https://unpkg.com/@staticcms/app@^1.0.0/dist/static-cms-app.js"></script>
<script>
const CategoriesControl = ({ label, value, field, onChange }) => {
const separator = useMemo(() => field.separator ?? ', ', [field.separator]);
```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]);
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: this.handleChange,
})
});
};
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(function (val, index) {
return h('li', { key: index }, val);
}),
);
};
const CategoriesPreview = ({ value }) => {
return h(
'ul',
{},
value.map((val, index) => {
return h('li', { key: index }, val);
}),
);
};
const schema = {
properties: {
separator: { type: 'string' },
},
};
const schema = {
properties: {
separator: { type: 'string' },
},
};
CMS.registerWidget('categories', CategoriesControl, CategoriesPreview, { schema });
</script>
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>
@ -272,48 +360,61 @@ If you want to use the media library in your custom widget you will need to use
<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 handleOpenMediaLibrary = useMediaInsert(value, { field, controlID }, onChange);
const assetSource = useMediaAsset(value, collection, field, entry);
return (
<>
<button type="button" onClick={handleOpenMediaLibrary}>Upload</button>
<button type="button" onClick={handleOpenMediaLibrary}>
Upload
</button>
<img role="presentation" src={assetSource} />
</>
);
};
```
```ts
```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 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>
<button type="button" onClick={handleOpenMediaLibrary}>
Upload
</button>
<img role="presentation" src={assetSource} />
</>
);

View File

@ -60,6 +60,10 @@ Shortcodes can be added to customize the Markdown editor via `registerShortcode`
### Usage
```markdown
[youtube|p6h-rYSVX90]
```
<CodeTabs>
```js
@ -117,8 +121,110 @@ CMS.registerShortcode('youtube', {
});
```
```markdown
[youtube|p6h-rYSVX90]
```jsx
import CMS from '@staticcms/core';
CMS.registerShortcode('youtube', {
label: 'YouTube',
openTag: '[',
closeTag: ']',
separator: '|',
toProps: args => {
if (args.length > 0) {
return { src: args[0] };
}
return { src: '' };
},
toArgs: ({ src }) => {
return [src];
},
control: ({ src, onChange }) => {
return (
<span>
<input
key="control-input"
value={src}
onChange={event => {
onChange({ src: event.target.value });
}}
/>
<iframe
key="control-preview"
width="420"
height="315"
src={`https://www.youtube.com/embed/${src}`}
/>
</span>
);
},
preview: ({ src }) => {
return (
<span>
<iframe
width="420"
height="315"
src={`https://www.youtube.com/embed/${src}`}
/>
</span>
);
},
});
```
```tsx
import CMS from '@staticcms/core';
interface YouTubeShortcodeProps {
src: string;
}
CMS.registerShortcode<YouTubeShortcodeProps>('youtube', {
label: 'YouTube',
openTag: '[',
closeTag: ']',
separator: '|',
toProps: args => {
if (args.length > 0) {
return { src: args[0] };
}
return { src: '' };
},
toArgs: ({ src }) => {
return [src];
},
control: ({ src, onChange }) => {
return (
<span>
<input
key="control-input"
value={src}
onChange={event => {
onChange({ src: event.target.value });
}}
/>
<iframe
key="control-preview"
width="420"
height="315"
src={`https://www.youtube.com/embed/${src}`}
/>
</span>
);
},
preview: ({ src }) => {
return (
<span>
<iframe
width="420"
height="315"
src={`https://www.youtube.com/embed/${src}`}
/>
</span>
);
},
});
```
</CodeTabs>

View File

@ -55,6 +55,11 @@ const supportedLanguages: Record<string, CodeLanguage> = {
grammar: Prism.languages.javascript,
language: 'javascript',
},
'language-jsx': {
title: 'JSX',
grammar: Prism.languages.jsx,
language: 'javascript',
},
'language-markdown': {
title: 'Markdown',
grammar: Prism.languages.markdown,
@ -65,6 +70,11 @@ const supportedLanguages: Record<string, CodeLanguage> = {
grammar: Prism.languages.typescript,
language: 'typescript',
},
'language-tsx': {
title: 'Typescript',
grammar: Prism.languages.tsx,
language: 'typescript',
},
};
interface TabData {