Skip to content

Commit cde7efc

Browse files
feat: custom form fields (#10)
* feat: custom form fields * feat: validation and separate package * docs: update import
1 parent de90c73 commit cde7efc

File tree

14 files changed

+5331
-2624
lines changed

14 files changed

+5331
-2624
lines changed

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,15 +170,45 @@ import {formSchema} from '@sanity/form-toolkit/form-schema'
170170

171171
export default defineConfig({
172172
//...
173-
plugins: [formSchema()],
173+
plugins: [
174+
formSchema({
175+
// Optionally, use your own schemas for additional formFields
176+
fields: [
177+
defineField({
178+
name: 'myField',
179+
type: 'myObjectType',
180+
}),
181+
],
182+
}),
183+
],
184+
})
185+
```
186+
187+
This will create a "form" document type.
188+
Then, add a field to your schema with type `form`
189+
190+
```ts
191+
// ./src/schemaTypes/page.ts
192+
import {defineField, defineType} from 'sanity'
193+
194+
export default defineType({
195+
name: 'page',
196+
type: 'document',
197+
fields: [
198+
defineField({
199+
name: 'form',
200+
type: 'reference',
201+
to: [{type: 'form'}],
202+
}),
203+
],
174204
})
175205
```
176206

177-
Then pass a `form` document to the `FormRenderer` component
207+
Finally, pass a `form` document to the `FormRenderer` component
178208

179209
```tsx
180210
import React, {type FC} from 'react'
181-
import {FormRenderer, type FormDataProps} from '@sanity/form-toolkit/form-schema'
211+
import {FormRenderer, type FormDataProps} from '@sanity/form-toolkit/form-renderer'
182212

183213
interface NativeFormExampleProps {
184214
formData: FormDataProps

package-lock.json

Lines changed: 5130 additions & 2414 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
"import": "./dist/form-schema/index.mjs",
4040
"default": "./dist/form-schema/index.js"
4141
},
42+
"./form-renderer": {
43+
"source": "./src/form-renderer/index.ts",
44+
"import": "./dist/form-renderer/index.mjs",
45+
"default": "./dist/form-renderer/index.js"
46+
},
4247
"./package.json": "./package.json"
4348
},
4449
"main": "./dist/index.js",
@@ -55,6 +60,9 @@
5560
],
5661
"form-schema": [
5762
"./dist/form-schema/index.d.ts"
63+
],
64+
"form-renderer": [
65+
"./dist/form-renderer/index.d.ts"
5866
]
5967
}
6068
},

src/form-schema/components/default-field.tsx renamed to src/form-renderer/components/default-field.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import type {ChangeEvent, FC, LegacyRef} from 'react'
33
import type {FieldComponentProps} from './types'
44

55
export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}) => {
6-
const {type, label, name, options = {}, choices = []} = field
6+
const {type, label, name, options = {}, choices = [], validation = []} = field
77
if (!type || !name) return null
8-
8+
const validationRules = validation.reduce((acc: Record<string, string>, v) => {
9+
acc[v.type] = v.value
10+
return acc
11+
}, {})
912
const {value, onChange, onBlur, ref} = fieldState
1013

1114
const handleChange = (
@@ -34,10 +37,11 @@ export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}
3437
<textarea
3538
ref={ref as LegacyRef<HTMLTextAreaElement>}
3639
name={name}
37-
value={value ?? ''}
3840
onChange={handleChange}
3941
onBlur={onBlur}
4042
placeholder={options.placeholder}
43+
{...validationRules}
44+
value={value ?? ''}
4145
/>
4246
)
4347

@@ -48,6 +52,7 @@ export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}
4852
name={name}
4953
value={value ?? ''}
5054
onChange={handleChange}
55+
{...validationRules}
5156
onBlur={onBlur}
5257
>
5358
{choices?.map((choice, i) => (
@@ -69,6 +74,7 @@ export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}
6974
checked={value === choice.value}
7075
onChange={handleChange}
7176
onBlur={onBlur}
77+
{...validationRules}
7278
/>
7379
{choice.label}
7480
</label>
@@ -85,6 +91,7 @@ export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}
8591
checked={Array.isArray(value) ? value.includes(choice.value) : value === choice.value}
8692
onChange={(e) => handleCheckboxChange(e, choice.value)}
8793
onBlur={onBlur}
94+
{...validationRules}
8895
/>
8996
{choice.label}
9097
</label>
@@ -98,6 +105,7 @@ export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}
98105
name={name}
99106
value={value ?? options.defaultValue ?? ''}
100107
onChange={handleChange}
108+
{...validationRules}
101109
onBlur={onBlur}
102110
placeholder={options.placeholder}
103111
/>
File renamed without changes.
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// types.ts
21
export type ValidationRule = {
32
type: string
43
value: string
@@ -32,7 +31,6 @@ export type FormDataProps = {
3231
current: string
3332
}
3433
fields?: FormField[]
35-
3634
submitButton?: {
3735
text: string
3836
position: 'left' | 'center' | 'right'

src/form-renderer/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {FormRenderer} from './components/form-renderer'
2+
import type {FormDataProps} from './components/types'
3+
export type {FormDataProps}
4+
export {FormRenderer}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type {StringInputProps} from 'sanity'
2+
import {useFormValue} from 'sanity'
3+
4+
import type {FormField} from '../../form-renderer/components/types'
5+
import {validationTypesByFieldType} from '../schema-types/form-field'
6+
7+
export const ValidationType = (props: StringInputProps) => {
8+
const {type} = useFormValue([...props.path.slice(0, 2)]) as FormField
9+
if (!type) return props.renderDefault(props)
10+
if (props.schemaType?.options) {
11+
props.schemaType.options.list = validationTypesByFieldType[type]
12+
}
13+
return props.renderDefault(props)
14+
}

src/form-schema/index.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {definePlugin} from 'sanity'
1+
import {definePlugin, type FieldDefinition} from 'sanity'
22

33
// import {structureTool} from 'sanity/structure'
4-
import {FormRenderer} from './components/form-renderer'
4+
// import {FormRenderer} from './components/form-renderer'
55
import {schema} from './schema-types'
66
// import {defaultDocumentNode} from './structure'
77

@@ -18,12 +18,18 @@ import {schema} from './schema-types'
1818
* })
1919
* ```
2020
*/
21-
export type {FormDataProps} from './components/types'
22-
export {FormRenderer}
23-
export const formSchema = definePlugin(() => {
21+
export type FieldsOption = Array<FieldDefinition>
22+
interface FormSchemaPluginOptions {
23+
/**
24+
* Array of field definitions to be used in the form schema.
25+
*/
26+
fields?: FieldsOption
27+
}
28+
29+
export const formSchema = definePlugin(({fields = []}: FormSchemaPluginOptions) => {
2430
return {
2531
name: 'form-toolkit_form-schema',
26-
schema,
32+
schema: schema(fields),
2733
// plugins: [structureTool({defaultDocumentNode})],
2834
}
2935
})

src/form-schema/schema-types/form-field.ts

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import {LuTextCursorInput} from 'react-icons/lu'
22
import {defineField, defineType} from 'sanity'
33

4+
import {ValidationType} from '../components/validation-type'
45
interface ValidationContextDocument {
56
fields?: Array<{
67
name: string
78
type?: string
89
}>
910
}
11+
1012
// Validation options by field type
11-
export const validationTypesByFieldType = {
12-
checkbox: ['minSelectedCount', 'maxSelectedCount', 'custom'],
13-
color: ['custom'],
14-
date: ['minDate', 'maxDate', 'custom'],
15-
'datetime-local': ['minDate', 'maxDate', 'custom'],
16-
email: ['pattern', 'custom'],
17-
file: ['maxSize', 'fileType', 'custom'],
18-
hidden: ['custom'],
19-
number: ['min', 'max', 'custom'],
20-
// password: ['minLength', 'pattern', 'custom'],
21-
radio: ['custom'],
22-
range: ['min', 'max', 'step', 'custom'],
23-
select: ['custom'],
24-
tel: ['pattern', 'custom'],
25-
text: ['minLength', 'maxLength', 'pattern', 'custom'],
26-
textarea: ['minLength', 'maxLength', 'custom'],
27-
time: ['custom'],
28-
url: ['pattern', 'custom'],
13+
export const validationTypesByFieldType: Record<string, string[]> = {
14+
checkbox: ['minSelectedCount', 'maxSelectedCount'],
15+
color: [],
16+
date: ['minDate', 'maxDate'],
17+
'datetime-local': ['minDate', 'maxDate'],
18+
email: ['pattern'],
19+
file: ['maxSize', 'fileType'],
20+
hidden: [],
21+
number: ['min', 'max'],
22+
// password: ['minLength', 'pattern'],
23+
radio: [],
24+
range: ['min', 'max', 'step'],
25+
select: [],
26+
tel: ['pattern'],
27+
text: ['minLength', 'maxLength', 'pattern'],
28+
textarea: ['minLength', 'maxLength'],
29+
time: [],
30+
url: ['pattern'],
2931
}
3032
export const formFieldType = defineType({
3133
name: 'formField',
@@ -114,40 +116,52 @@ export const formFieldType = defineType({
114116
type: 'boolean',
115117
initialValue: false,
116118
}),
117-
// defineField({
118-
// name: 'validation',
119-
// title: 'Validation Rules',
120-
// type: 'array',
121-
// of: [
122-
// {
123-
// type: 'object',
124-
// fields: [
125-
// defineField({
126-
// name: 'type',
127-
// title: 'Validation Type',
128-
// type: 'string',
129-
130-
// hidden: ({parent}) => !parent?.type,
131-
// options: {
132-
// // TODO: I think this needs to be a custom input component?
133-
// // list: ({parent}) => (parent?.type ? validationTypesByFieldType[parent.type] : []),
134-
// list: [],
135-
// },
136-
// }),
137-
// defineField({
138-
// name: 'value',
139-
// title: 'Value',
140-
// type: 'string',
141-
// }),
142-
// defineField({
143-
// name: 'message',
144-
// title: 'Error Message',
145-
// type: 'string',
146-
// }),
147-
// ],
148-
// },
149-
// ],
150-
// }),
119+
defineField({
120+
name: 'validation',
121+
title: 'Validation Rules',
122+
type: 'array',
123+
hidden: ({parent}) => {
124+
if (!parent?.type) return true
125+
const validationTypes = validationTypesByFieldType[parent.type]
126+
return !validationTypes || validationTypes.length === 0
127+
},
128+
of: [
129+
{
130+
type: 'object',
131+
fields: [
132+
defineField({
133+
name: 'type',
134+
title: 'Validation Type',
135+
type: 'string',
136+
options: {
137+
// TODO: I think this needs to be a custom input component?
138+
// list: ({parent}) => (parent?.type ? validationTypesByFieldType[parent.type] : []),
139+
list: [],
140+
},
141+
components: {
142+
input: ValidationType,
143+
},
144+
}),
145+
defineField({
146+
name: 'value',
147+
title: 'Value',
148+
type: 'string',
149+
}),
150+
defineField({
151+
name: 'message',
152+
title: 'Error Message',
153+
type: 'string',
154+
}),
155+
],
156+
preview: {
157+
select: {
158+
title: 'type',
159+
subtitle: 'value',
160+
},
161+
},
162+
},
163+
],
164+
}),
151165
defineField({
152166
name: 'choices',
153167
title: 'Choices',

0 commit comments

Comments
 (0)