1import {
2 CheckboxField,
3 CountryField,
4 EmailField,
5 SelectField,
6 StateField,
7 TextAreaField,
8 TextField,
9} from '@payloadcms/plugin-form-builder/types'
10import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
11import { RichText } from '@payloadcms/richtext-lexical/react'
12import {
13 Control,
14 Controller,
15 FieldErrorsImpl,
16 FieldValues,
17 useFormContext,
18 UseFormRegister,
19} from 'react-hook-form'
20
21const stateOptions = [
22 { label: 'Alabama', value: 'AL' },
23 { label: 'Alaska', value: 'AK' },
24 { label: 'Arizona', value: 'AZ' },
25 { label: 'Arkansas', value: 'AR' },
26 { label: 'California', value: 'CA' },
27
28]
29
30const countryOptions = [
31 {
32 label: 'United Kingdom',
33 value: 'GB',
34 },
35 {
36 label: 'United States',
37 value: 'US',
38 },
39
40]
41
42export type FormGroupProps = {
43 errors: Partial<FieldErrorsImpl>
44 register: UseFormRegister<FieldValues>
45 control?: Control
46 stacked?: boolean
47}
48
49export const FormField: React.FC<{
50 children: React.ReactNode
51 className?: string
52 width?: number | string
53 stacked?: boolean
54}> = ({ children, className, width, stacked = true }) => {
55 return (
56 <div
57 className={[
58 className ? className : 'flex justify-start items-start gap-2 [&>label]:min-w-30',
59 stacked && 'flex-col',
60 ]
61 .filter(Boolean)
62 .join(' ')}
63 style={{ maxWidth: width ? `${width}%` : undefined }}
64 >
65 {children}
66 </div>
67 )
68}
69
70export const Error = ({ name }: { name: string }) => {
71 const {
72 formState: { errors },
73 } = useFormContext()
74 return (
75 <div className="mt-2 text-red-600 text-sm">
76 {(errors[name]?.message as string) || 'This field is required'}
77 </div>
78 )
79}
80
81export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => {
82 return <div className="w-full my-12">{message && <RichText data={message} />}</div>
83}
84
85export const Checkbox: React.FC<FormGroupProps & CheckboxField> = ({
86 name,
87 defaultValue,
88 width,
89 errors,
90 label,
91 register,
92 required,
93}) => {
94 const props = register(name, { required: required })
95 const { setValue } = useFormContext()
96
97 return (
98 <FormField width={width} stacked={false} className="flex items-center flex-row gap-3">
99 <input
100 type="checkbox"
101 defaultChecked={defaultValue}
102 id={name}
103 {...props}
104 onChange={(checked) => {
105 setValue(props.name, checked)
106 }}
107 />
108 <label htmlFor={name}>
109 {required && (
110 <span className="text-red-500">
111 * <span className="sr-only">(required)</span>
112 </span>
113 )}
114 {label}
115 </label>
116
117 {errors[name] && <Error name={name} />}
118 </FormField>
119 )
120}
121
122export const Text: React.FC<FormGroupProps & TextField> = ({
123 name,
124 defaultValue,
125 errors,
126 label,
127 register,
128 required,
129 width,
130 stacked,
131}) => {
132 const props = register(name, { required: required })
133 const { setValue } = useFormContext()
134
135 return (
136 <FormField width={width} stacked={stacked}>
137 <label htmlFor={name}>
138 {label}
139
140 {required && (
141 <span className="text-red-500">
142 * <span className="sr-only">(required)</span>
143 </span>
144 )}
145 </label>
146 <input
147 defaultValue={defaultValue}
148 id={name}
149 type="text"
150 onChange={(value) => {
151 setValue(props.name, value)
152 }}
153 {...register(name, { required })}
154 className="w-full"
155 />
156
157 {errors[name] && <Error name={name} />}
158 </FormField>
159 )
160}
161
162export const Textarea: React.FC<FormGroupProps & TextAreaField & { rows?: number }> = ({
163 name,
164 defaultValue,
165 rows,
166 errors,
167 label,
168 register,
169 required,
170 width,
171 stacked,
172}) => {
173 const props = register(name, { required: required })
174 const { setValue } = useFormContext()
175
176 return (
177 <FormField width={width} stacked={stacked}>
178 <label htmlFor={name}>
179 {label}
180 {required && (
181 <span className="text-red-500">
182 * <span className="sr-only">(required)</span>
183 </span>
184 )}
185 </label>
186
187 <textarea
188 defaultValue={defaultValue}
189 id={name}
190 rows={rows}
191 cols={width}
192 onChange={(value) => {
193 setValue(props.name, value)
194 }}
195 {...register(name, { required: required })}
196 className="w-full"
197 />
198
199 {errors[name] && <Error name={name} />}
200 </FormField>
201 )
202}
203
204export const State: React.FC<FormGroupProps & StateField> = ({
205 name,
206 defaultValue,
207 control,
208 errors,
209 label,
210 register,
211 required,
212 width,
213 stacked,
214}) => {
215 const props = register(name, { required: required })
216 const { setValue } = useFormContext()
217
218 return (
219 <FormField width={width} stacked={stacked}>
220 <label className="" htmlFor={name}>
221 {label}
222 {required && (
223 <span className="text-red-500">
224 * <span className="sr-only">(required)</span>
225 </span>
226 )}
227 </label>
228 <Controller
229 control={control}
230 defaultValue={defaultValue}
231 name={name}
232 render={({ field: { value } }) => {
233 const controlledValue = stateOptions.find((t) => t.value === value)
234
235 return (
236 <select
237 onChange={(e) => {
238 setValue(props.name, e.target.value)
239 }}
240 value={controlledValue?.value}
241 className="w-full"
242 >
243 {stateOptions.map(({ label, value }) => {
244 return (
245 <option key={value} value={value}>
246 {label}
247 </option>
248 )
249 })}
250 </select>
251 )
252 }}
253 rules={{ required }}
254 />
255
256 {errors[name] && <Error name={name} />}
257 </FormField>
258 )
259}
260
261export const Country: React.FC<FormGroupProps & CountryField> = ({
262 name,
263 defaultValue,
264 control,
265 errors,
266 label,
267 register,
268 required,
269 width,
270 stacked,
271}) => {
272 const props = register(name, { required: required })
273 const { setValue } = useFormContext()
274
275 return (
276 <FormField width={width} stacked={stacked}>
277 <label className="" htmlFor={name}>
278 {label}
279
280 {required && (
281 <span className="text-red-500">
282 * <span className="sr-only">(required)</span>
283 </span>
284 )}
285 </label>
286 <Controller
287 control={control}
288 defaultValue={defaultValue}
289 name={name}
290 render={({ field: { value } }) => {
291 const controlledValue = countryOptions.find((t) => t.value === value)
292
293 return (
294 <select
295 onChange={(e) => {
296 setValue(props.name, e.target.value)
297 }}
298 value={controlledValue?.value}
299 defaultValue={defaultValue}
300 className="w-full"
301 >
302 {countryOptions.map(({ label, value }) => {
303 return (
304 <option key={value} value={value}>
305 {label}
306 </option>
307 )
308 })}
309 </select>
310 )
311 }}
312 rules={{ required }}
313 />
314
315 {errors[name] && <Error name={name} />}
316 </FormField>
317 )
318}
319
320export const Select: React.FC<FormGroupProps & SelectField> = ({
321 name,
322 defaultValue,
323 control,
324 options,
325 errors,
326 label,
327 register,
328 required,
329 width,
330 stacked,
331}) => {
332 const props = register(name, { required: required })
333 const { setValue } = useFormContext()
334
335 return (
336 <FormField width={width} stacked={stacked}>
337 <label htmlFor={name}>
338 {label}
339 {required && (
340 <span className="text-red-500">
341 * <span className="sr-only">(required)</span>
342 </span>
343 )}
344 </label>
345 <Controller
346 control={control}
347 defaultValue={defaultValue}
348 name={name}
349 render={({ field: { onChange, value } }) => {
350 const controlledValue = options.find((t) => t.value === value)
351
352 return (
353 <select
354 onChange={(e) => {
355 setValue(props.name, e.target.value)
356 }}
357 value={controlledValue?.value}
358 className="w-full"
359 >
360 {options.map(({ label, value }) => {
361 return (
362 <option key={value} value={value}>
363 {label}
364 </option>
365 )
366 })}
367 </select>
368 )
369 }}
370 rules={{ required }}
371 />
372
373 {errors[name] && <Error name={name} />}
374 </FormField>
375 )
376}
377
378export const Email: React.FC<FormGroupProps & EmailField> = ({
379 name,
380 defaultValue,
381 errors,
382 label,
383 register,
384 required,
385 width,
386 stacked,
387}) => {
388 const props = register(name, { required: required })
389 const { setValue } = useFormContext()
390
391 return (
392 <FormField width={width} stacked={stacked}>
393 <label htmlFor={name}>
394 {label}
395 {required && (
396 <span className="text-red-500">
397 * <span className="sr-only">(required)</span>
398 </span>
399 )}
400 </label>
401 <input
402 defaultValue={defaultValue}
403 id={name}
404 type="text"
405 onChange={(value) => {
406 setValue(props.name, value)
407 }}
408 {...register(name, { pattern: /^\S[^\s@]*@\S+$/, required })}
409 className="w-full"
410 />
411
412 {errors[name] && <Error name={name} />}
413 </FormField>
414 )
415}
416
417
418export const fields = {
419 text: Text,
420 textarea: Textarea,
421 email: Email,
422 select: Select,
423 state: State,
424 country: Country,
425 checkbox: Checkbox,
426 message: Message
427}