Keystone Nextpreview

How To Use Document Fields

The document field type is a highly customizable rich text editor that lets content creators quickly and easily edit content in your system.

It's built with Slate, stores your content as JSON-structured data, and lets you do things like:

  • Embed multi-column layouts (that your front-end can decide how to render)
  • Insert relationships to other items in your Keystone database
  • Define your own custom blocks based on React Components
  • Choose between automatically generated forms for editing Component Props, or create your own custom Component UI and Toolbars

Try the demo ↓

This is the document editor.

Here’s just a few things you can do with it:

Add layout blocks

and all the usual formatting options

You can insert 

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 KeystoneJS Team

The really cool stuff is behind the + button on the right of the toolbar – these are the Custom Components.

This component 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 Component 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 items in your database with the Relationship 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!

This is the end of the editable document. Expand the block below to see how the data is stored ↓

View the Document Structure
[
  {
    "type": "heading",
    "children": [
      {
        "text": "This is the document editor."
      }
    ],
    "level": 1
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "Here’s just a few things you can do with it:"
      }
    ]
  },
  {
    "type": "layout",
    "layout": [
      1,
      1
    ],
    "children": [
      {
        "type": "layout-area",
        "children": [
          {
            "type": "paragraph",
            "children": [
              {
                "text": "Add 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 component 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 Component 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 items in your database with the "
              },
              {
                "text": "Relationship",
                "bold": true
              },
              {
                "text": " 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": "This is the end of the editable document. Expand the block below to see how the data is stored ↓",
        "bold": true
      }
    ]
  }
]

Customize the editor

You can configure which fields appear in the editor based on what your team needs. Try adding and removing features below ↓ to see them update in the editor above ↑.

Bold
Code
Italic
Keyboard
Strikethrough
Subscript
Superscript
Underline
Blockquote
Code Block
Unordered List
Ordered List
H1
H2
H3
H4
H5
H6
Center
End
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 also 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. To include the label and extra data from the selection specified in the relationship, 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 let you 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 export
export const componentBlocks = {
quote: component({
component: ({ attribution, content }) => {
return (
<div
style={{
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 ui.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({
ui: {
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 implementation 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 (
<div
css={{
borderRadius: radii.small,
display: 'flex',
paddingLeft: spacing.medium,
paddingRight: spacing.medium,
}}
style={{
background: intentConfig.background,
}}
>
<div
contentEditable={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 => (
<ToolbarButton
isSelected={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 received 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', placeholder: '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 override
inline: {
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} />;

On this page