Custom Fields
Keystone provides a collection of field types which you can use to build your system. If you need a field type which isn't provided, or you need a specialised version of an existing field type, you can define your own custom field type.
There are two parts to a field type:
- The backend portion which defines what data is stored in the database and how it appears in the GraphQL API.
- The frontend portion which defines how the field looks and behaves in the Admin UI.
The general approach to creating a custom field type is to take an existing field type and make the appropriate changes for your use case. In this guide we're going to create a field type myInt
which recreates the integer
field type.
For inspiration, see the source for the fields that Keystone provides and the Custom Fields example project.
Backend
The backend portion is the entry point to the field type. We define our field type myInt
and the corresponding type MyIntFieldConfig
which defines the accepted configuration options.
import {BaseListTypeInfo,FieldTypeFunc,CommonFieldConfig,fieldType,orderDirectionEnum,} from '@keystone-6/core/types';import { graphql } from '@keystone-6/core';export type MyIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =CommonFieldConfig<ListTypeInfo> & {isIndexed?: boolean | 'unique';};export const myInt =<ListTypeInfo extends BaseListTypeInfo>({isIndexed,...config}: MyIntFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>meta =>fieldType({kind: 'scalar',mode: 'optional',scalar: 'Int',index: isIndexed === true ? 'index' : isIndexed || undefined,})({...config,input: {create: { arg: graphql.arg({ type: graphql.Int }) },update: { arg: graphql.arg({ type: graphql.Int }) },orderBy: { arg: graphql.arg({ type: orderDirectionEnum }) },},output: graphql.field({ type: graphql.Int }),views: './view',});
DB Field
fieldType
is called with the db field which defines what the field should store in the database. Here it's an integer (scalar: 'Int'
) but there are other kinds which you can find in the type definitions for DBField
.
Inputs
The input
object defines the GraphQL inputs for the field type.
input: {create: { arg: graphql.arg({ type: graphql.Int }) },update: { arg: graphql.arg({ type: graphql.Int }) },orderBy: { arg: graphql.arg({ type: orderDirectionEnum }) },},
You can also provide resolvers to transform the value coming from GraphQL into the value that is passed to Prisma.
input: {create: { arg: graphql.arg({ type: graphql.Int }), resolve: (val, context) => val },update: { arg: graphql.arg({ type: graphql.Int }), resolve: (val, context) => val },orderBy: { arg: graphql.arg({ type: orderDirectionEnum }), resolve: (val, context) => val },},
Output
The output field defines what can be fetched from the field:
output: graphql.field({ type: graphql.Int })
A resolver can also be provided:
output: graphql.field({type: graphql.Int,resolve({ value, item }, args, context, info) {return value;}})
Frontend
The frontend portion of a field must be in a seperate file that the backend implementation points to with the views
option.
The views
option is resolved as though it is an import from some file in the project directory.
views: './view',
Controller
The controller
export defines the functional parts of the frontend of a field.
// view.tsxexport const controller = (config: FieldControllerConfig): FieldController<string, string> => {return {path: config.path,label: config.label,graphqlSelection: config.path,defaultValue: '',deserialize: data => {const value = data[config.path];return typeof value === 'number' ? value + '' : '';},serialize: value => ({ [config.path]: value === '' ? null : parseInt(value, 10) }),};};
Field
The Field
export is a React component which is used in the item view and the create modal that allows users to view and edit the value of the field.
// view.tsximport { FieldContainer, FieldLabel, TextInput } from '@keystone-ui/fields';import { FieldProps } from '@keystone-6/core/types';export const Field = ({ field, value, onChange, autoFocus }: FieldProps<typeof controller>) => (<FieldContainer><FieldLabel htmlFor={field.path}>{field.label}</FieldLabel>{onChange ? (<TextInputid={field.path}autoFocus={autoFocus}type="number"onChange={event => {onChange(event.target.value.replace(/[^\d-]/g, ''));}}value={value}/>) : (value)}</FieldContainer>);
Cell
The Cell
export is a React component which is shown in the table on the list view. Note it does not allow modifying the value.
// view.tsximport { CellLink, CellContainer } from '@keystone-6/core/admin-ui/components';import { CellComponent } from '@keystone-6/core/types';export const Cell: CellComponent = ({ item, field, linkTo }) => {let value = item[field.path] + '';return linkTo ? <CellLink {...linkTo}>{value}</CellLink> : <CellContainer>{value}</CellContainer>;};Cell.supportsLinkTo = true;
CardValue
The CardValue
export is a React component which is shown on the item view in relationship fields with displayMode: 'cards'
when the related item is not being edited. Note it does not allow modifying the value.
// view.tsximport { FieldContainer, FieldLabel } from '@keystone-ui/fields';import { CardValueComponent } from '@keystone-6/core/types';export const CardValue: CardValueComponent = ({ item, field }) => {return (<FieldContainer><FieldLabel>{field.label}</FieldLabel>{item[field.path]}</FieldContainer>);};