How To Use Document Fields
This is the document editor.
🔥 For real, try it out! click anywhere to start editing.
It's got layout blocks
and all the usual formatting options
You can insert links
and format text with **markdown syntax**
We're really excited to show you what we've built, and what you can build with it!
The really cool stuff is behind the + button on the right of the toolbar – these are the Custom Components.
This one is the Notice, but you can build your own, by just defining their prop types (like you do your Keystone schema) and providing a React Compoent to render the preview.
They store structured data, and can be inserted (and edited!) anywhere in the document. You can even link them to other item in your database with the Realtionship field type.
They can also have props that are edited with an inline form, for more complex use cases (including conditional fields)
Try inserting a Hero component and you'll see how it works. Remember, you can build your own, so your content authors can insert components from your website's Design System, and your front-end still gets structured data to render!
Everything above this line 👇🏻 is editable. Expand the block below to see how the data is stored.
View Document Structure
[
{
"type": "heading",
"children": [
{
"text": "This is the document editor."
}
],
"level": 1
},
{
"type": "paragraph",
"children": [
{
"text": "🔥 For real",
"bold": true
},
{
"text": ", "
},
{
"text": "try it out",
"italic": true
},
{
"text": "! click anywhere to start editing."
}
]
},
{
"type": "layout",
"layout": [
1,
1
],
"children": [
{
"type": "layout-area",
"children": [
{
"type": "paragraph",
"children": [
{
"text": "It's got layout blocks"
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "and all the usual "
},
{
"text": "formatting",
"code": true
},
{
"text": " options"
}
]
}
]
},
{
"type": "layout-area",
"children": [
{
"type": "paragraph",
"children": [
{
"text": "You can insert "
},
{
"type": "link",
"href": "https://next.keystonejs.com/",
"children": [
{
"text": "links"
}
]
},
{
"text": ""
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "and format text with **"
},
{
"text": "markdown syntax",
"bold": true
},
{
"text": "**"
}
]
}
]
}
]
},
{
"type": "component-block",
"component": "quote",
"props": {},
"children": [
{
"type": "component-block-prop",
"propPath": [
"content"
],
"children": [
{
"type": "paragraph",
"children": [
{
"text": "We're really excited to show you what we've built, and what you can build with it!"
}
]
}
]
},
{
"type": "component-inline-prop",
"propPath": [
"attribution"
],
"children": [
{
"text": "The KeystoneJS Team"
}
]
}
]
},
{
"type": "component-block",
"component": "notice",
"props": {
"intent": "info"
},
"children": [
{
"type": "component-block-prop",
"propPath": [
"content"
],
"children": [
{
"type": "paragraph",
"children": [
{
"text": "The really cool stuff is behind the "
},
{
"text": "+",
"code": true,
"bold": true
},
{
"text": " button on the right of the toolbar – these are the "
},
{
"text": "Custom Components",
"bold": true
},
{
"text": "."
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "This one is the "
},
{
"text": "Notice",
"bold": true
},
{
"text": ", but you can build your own, by just defining their prop types (like you do your Keystone schema) and providing a React Compoent to render the preview."
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "They store structured data, and can be inserted (and edited!) anywhere in the document. You can even link them to other item in your database with the Realtionship field type."
}
]
}
]
}
]
},
{
"type": "component-block",
"component": "notice",
"props": {
"intent": "success"
},
"children": [
{
"type": "component-block-prop",
"propPath": [
"content"
],
"children": [
{
"type": "paragraph",
"children": [
{
"text": "They can also have props that are edited with an inline form, for more complex use cases (including conditional fields)"
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "Try inserting a "
},
{
"text": "Hero",
"bold": true
},
{
"text": " component and you'll see how it works. Remember, you can build your own, so your content authors can insert components from your website's Design System, and your front-end still gets structured data to render!"
}
]
}
]
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "Everything above this line 👇🏻 is editable. Expand the block below to see how the data is stored."
}
]
}
]General Features
The specific features of the editor that are enabled can be customised. Try disabling certain features below. It will update the code block below and the editor above.
import { config, createSchema, list } from '@keystone-next/keystone/schema';import { document } from '@keystone-next/fields-document';export default config({lists: createSchema({ListName: list({fields: {fieldName: document({formatting: true,links: true,layouts: [[1, 1],[1, 1, 1],[2, 1],[1, 2],[1, 2, 1]],dividers: true}/* ... */}),/* ... */},}),/* ... */}),/* ... */});
Inline Relationships
The document field can have inline relationships for things like mentions. These are not stored like relationship fields on lists, they are stored as ids in the document structure.
import { config, createSchema, list } from '@keystone-next/keystone/schema';import { document } from '@keystone-next/fields-document';export default config({lists: createSchema({ListName: list({fields: {fieldName: document({relationships: {mention: {kind: 'inline',listKey: 'User',label: 'Mention',selection: 'some GraphQL selection',},},/* ... */}),/* ... */},}),/* ... */}),/* ... */});
By default, only the ids for relationships are sent in document.
If you'd like to include the label and extra data from the selection specified in the relationship, you can pass hydrateRelationships: true to the field.
query {allPosts {content(hydrateRelationships: true) {document}}}
Whenever a value is saved to the document field, the extra data from the selection and the label on relationships are removed if they exist so only the id is stored.
Component Blocks
Component blocks allow you to add custom blocks to the editor that can accept unstructured content and render a form that renders arbitrary React components for input.
To add component blocks, you need to create a file somewhere and export component blocks from there
component-blocks.tsx
import React from 'react';import { component, fields } from '@keystone-next/fields-document/component-blocks';// naming the export componentBlocks is important because the Admin UI// expects to find the components like on the componentBlocks exportexport const componentBlocks = {quote: component({component: ({ attribution, content }) => {return (<divstyle={{borderLeft: '3px solid #CBD5E0',paddingLeft: 16,}}><div style={{ fontStyle: 'italic', color: '#4A5568' }}>{content}</div><div style={{ fontWeight: 'bold', color: '#718096' }}><NotEditable>— </NotEditable>{attribution}</div></div>);},label: 'Quote',props: {content: fields.child({kind: 'block',placeholder: 'Quote...',formatting: { inlineMarks: 'inherit', softBreaks: 'inherit' },links: 'inherit',}),attribution: fields.child({ kind: 'inline', placeholder: 'Attribution...' }),},chromeless: true,}),};
You need to import the componentBlocks and pass it to the document field along with the path to the file with the component blocks to views.
keystone.ts
import { config, createSchema, list } from '@keystone-next/keystone/schema';import { document } from '@keystone-next/fields-document';import { componentBlocks } from './component-blocks';export default config({lists: createSchema({ListName: list({fields: {fieldName: document({views: require.resolve('./component-blocks'),componentBlocks,}),},}),}),});
In the document editor at the top of this page, the Quote(shown above), Notice and Hero are implemented as component blocks, see the implemention for Notice and Hero by expanding this.
/** @jsx */import { jsx } from '@keystone-ui/core';import { component, fields } from '@keystone-next/fields-document/component-blocks';export const componentBlocks = {notice: component({component: function Notice({ content, intent }) {const { palette, radii, spacing } = useTheme();const intentMap = {info: {background: palette.blue100,foreground: palette.blue700,icon: noticeIconMap.info,},error: {background: palette.red100,foreground: palette.red700,icon: noticeIconMap.error,},warning: {background: palette.yellow100,foreground: palette.yellow700,icon: noticeIconMap.warning,},success: {background: palette.green100,foreground: palette.green700,icon: noticeIconMap.success,},};const intentConfig = intentMap[intent.value];return (<divcss={{borderRadius: radii.small,display: 'flex',paddingLeft: spacing.medium,paddingRight: spacing.medium,}}style={{background: intentConfig.background,}}><divcontentEditable={false}css={{color: intentConfig.foreground,marginRight: spacing.small,marginTop: '1em',userSelect: 'none',}}><intentConfig.icon /></div><div css={{ flex: 1 }}>{content}</div></div>);},label: 'Notice',chromeless: true,props: {intent: fields.select({label: 'Intent',options: [{ value: 'info', label: 'Info' },{ value: 'warning', label: 'Warning' },{ value: 'error', label: 'Error' },{ value: 'success', label: 'Success' },] as const,defaultValue: 'info',}),content: fields.child({kind: 'block',placeholder: '',formatting: 'inherit',dividers: 'inherit',links: 'inherit',relationships: 'inherit',}),},toolbar({ props, onRemove }) {return (<ToolbarGroup>{props.intent.options.map(opt => {const Icon = noticeIconMap[opt.value];return (<Tooltip key={opt.value} content={opt.label} weight="subtle">{attrs => (<ToolbarButtonisSelected={props.intent.value === opt.value}onClick={() => {props.intent.onChange(opt.value);}}{...attrs}><Icon size="small" /></ToolbarButton>)}</Tooltip>);})}<ToolbarSeparator /><Tooltip content="Remove" weight="subtle">{attrs => (<ToolbarButton variant="destructive" onClick={onRemove} {...attrs}><Trash2Icon size="small" /></ToolbarButton>)}</Tooltip></ToolbarGroup>);},}),};
Fields
There are a variety
Child Fields
Form Fields
@keystone-next/keystone/component-blocks ships with a set of form fields for common purposes:
fields.text({ label: '...', defaultValue: '...' })fields.url({ label: '...', defaultValue: '...' })fields.select({ label: '...', options: [{ label:'A', value:'a' }, { label:'B', value:'b' }] defaultValue: 'a' })fields.checkbox({ label: '...', defaultValue: false })
You can write your own form fields that conform to this API.
type FormField<Value, Options> = {kind: 'form';Input(props: {value: Value;onChange(value: Value): void;autoFocus: boolean;/*** This will be true when validate has returned false and the user has attempted to close the form* or when the form is open and they attempt to save the item*/forceValidation: boolean;}): ReactElement | null;/*** The options are config about the field that are available on the* preview props when rendering the toolbar and preview component*/options: Options;defaultValue: Value;/*** validate will be called in two cases:* - on the client in the editor when a user is changing the value.* Returning `false` will block closing the form* and saving the item.* - on the server when a change is recieved before allowing it to be saved* if `true` is returned* @param value The value of the form field. You should NOT trust* this value to be of the correct type because it could come from* a potentially malicious client*/validate(value: unknown): boolean;};
Object Fields
To nest a group of component block fields, you can use fields.object
import { fields } from '@keystone-next/fields-document/component-blocks';fields.object({a: fields.text({ label: 'A' }),a: fields.text({ label: 'B' }),});
Relationship Fields
To use relationship fields on component blocks, you need to add a relationship to the document field config.
import { config, createSchema, list } from '@keystone-next/keystone/schema';import { document } from '@keystone-next/fields-document';export default config({lists: createSchema({ListName: list({fields: {fieldName: document({relationships: {featuredAuthors: {kind: 'prop',listKey: 'User',selection: 'posts { id title }',many: true,},},/* ... */}),/* ... */},}),/* ... */}),/* ... */});
You can reference the key of the relationship in the relationship and in the form, it will render a relationship select like the relationship field on lists.
import { fields } from '@keystone-next/fields-document/component-blocks';fields.relationship({ label: 'Authors', relationship: 'featuredAuthors' });
Note: Like inline relationships, relationship fields on component blocks are not stored like relationship fields on lists, they are stored as ids in the document structure.
Objects
import { fields } from '@keystone-next/fields-document/component-blocks';fields.object({text: fields.text({ label: 'Text' }),child: fields.placeholder({ placeholder: 'Content...' }),});
Conditional Fields
You can conditionally show different fields with fields.conditional, they require a form field with a value that is either a string or a boolean as the discriminant and an object of fields for the values.
import { fields } from '@keystone-next/fields-document/component-blocks';fields.conditional(fields.checkbox({ label: 'Show Call to action' }), {true: fields.object({url: fields.url({ label: 'URL' }),content: fields.child({ kind: 'inline', placehodler: 'Call to Action' }),}),false: fields.empty(),});
You might find
fields.empty()useful which stores and renders nothing if you want to have a field in one case and nothing anything in another
Preview Props
Chromeless
If you want to give your component blocks a more native feel in the editor, you can set chromeless: true.
When you disable it, the generated form is disabled.
In the editor at the top of this page, the Notice and Quote blocks are chromeless and the Hero has the chrome enabled.
You will likely want to provide a custom toolbar when you set chromeless: true.
component({chromeless: false,});
Toolbar
component({chromeless: false,});
Rendering
To render the document in a React app, use the @keystone-next/document-renderer package.
import { DocumentRenderer } from '@keystone-next/document-renderer';<DocumentRenderer document={document} />;
Overriding the default renderers
import { DocumentRenderer, DocumentRendererProps } from '@keystone-next/document-renderer';const renderers: DocumentRendererProps['renderers'] = {// use your editor's autocomplete to see what other renderers you can overrideinline: {bold: ({ children }) => {return <strong>{children}</strong>;},},block: {paragraph: ({ children, textAlign }) => {return <p style={{ textAlign }}>{children}</p>;},},};<DocumentRenderer document={document} renderers={renderers} />;
Rendering Component blocks
Typing props for rendering component blocks
If you're using TypeScript, you can infer the props types for component with InferRenderersForComponentBlocks from @keystone-next/fields-document/component-blocks.
import { DocumentRenderer } from '@keystone-next/document-renderer';import { InferRenderersForComponentBlocks } from '@keystone-next/fields-document/component-blocks';import { componentBlocks } from '../path/to/your/custom/views';const componentBlockRenderers: InferRenderersForComponentBlocks<typeof componentBlocks> = {someComponentBlock: props => {// props will be inferred from your component blocks},};<DocumentRenderer document={document} componentBlocks={componentBlockRenderers} />;