Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zod schema array of objects #233

Open
pavkout opened this issue Nov 30, 2023 · 1 comment
Open

Zod schema array of objects #233

pavkout opened this issue Nov 30, 2023 · 1 comment

Comments

@pavkout
Copy link

pavkout commented Nov 30, 2023

Hi everyone,

I'm trying to create a form with the following schema:

const schema = z.object({
	title: z.string().min(1),
	contacts: z
		.array(
			z.object({
				name: z.string().min(1),
				email: z.string().email(),
				type: z
					.enum(['text', 'textarea', 'select', 'radio', 'checkbox'])
					.default('text'),
			}),
		)
		.min(1),
})

and this is the code for the form implementation

import { type ActionFunction } from '@remix-run/node'
import { makeDomainFunction } from 'domain-functions'
import { useRef } from 'react'
import { z } from 'zod'
import { Form } from '~/components/remix-form.tsx'
import { formAction } from '~/services/form-action.server.ts'

const schema = z.object({
	title: z.string().min(1),
	contacts: z
		.array(
			z.object({
				name: z.string().min(1),
				email: z.string().email(),
				type: z
					.enum(['text', 'textarea', 'select', 'radio', 'checkbox'])
					.default('text'),
			}),
		)
		.min(1),
})

const mutation = makeDomainFunction(schema)(async values => values)

export const action: ActionFunction = async ({ request }) =>
	formAction({ request, schema, mutation })

export default () => {
	const nameRef = useRef<HTMLInputElement>(null)
	const emailRef = useRef<HTMLInputElement>(null)

	return (
		<Form schema={schema} values={{ contacts: [] }}>
			{({ Field, Errors, Button, watch, setValue }) => {
				const contacts = watch('contacts')

				return (
					<>
						<Field name="title" />
						<Field name="contacts">
							{({ Label, Errors }) => (
								<>
									<Label />
									<fieldset className="flex gap-2">
										<input
											type="text"
											className="block w-full rounded-md border-gray-300 text-gray-800
                      shadow-sm focus:border-pink-500 focus:ring-pink-500 sm:text-sm"
											placeholder="Name"
											ref={nameRef}
										/>
										<input
											type="text"
											className="block w-full rounded-md border-gray-300 text-gray-800
                      shadow-sm focus:border-pink-500 focus:ring-pink-500 sm:text-sm"
											placeholder="E-mail"
											ref={emailRef}
										/>
										<button
											className="rounded-md bg-pink-500 px-4"
											onClick={event => {
												event.preventDefault()

												const name = nameRef.current?.value
												const email = emailRef.current?.value

												if (name && email) {
													setValue(
														'contacts',
														[...contacts, { name, email, type: 'radio' }],
														{
															shouldValidate: true,
														},
													)
													nameRef.current.value = ''
													emailRef.current.value = ''
												}
											}}
										>
											+
										</button>
									</fieldset>
									{contacts && (
										<section className="-ml-1 flex flex-wrap pt-1">
											{contacts.map((contact, index) => (
												<span key={contact.email} className="flex gap-3">
													<span className="m-1 flex items-center rounded-md bg-pink-500 px-2 py-1 text-white">
														<span className="flex-1">
															{contact.name} ({contact.email})
														</span>
														<button
															className="ml-2 text-pink-700"
															onClick={() => {
																setValue(
																	'contacts',
																	contacts.filter(
																		({ email }) => email !== contact.email,
																	),
																	{ shouldValidate: true },
																)
															}}
														>
															X
														</button>
													</span>
													<Field
														label="Type"
														name={`contacts[${index}][type]`}
													/>
													<input
														type="hidden"
														name={`contacts[${index}][name]`}
														value={contact.name}
													/>
													<input
														type="hidden"
														name={`contacts[${index}][email]`}
														value={contact.email}
													/>
												</span>
											))}
										</section>
									)}

									<Errors />
								</>
							)}
						</Field>
						<Errors />
						<Button />
					</>
				)
			}}
		</Form>
	)
}

this is basically this example example and I just added an extra option type which is an enum with some options.

Αs a result I would expect the type field to be a dropdown field but the result is the following

Screenshot 2023-11-30 at 11 12 42 PM

Is it something I am doing wrong?

@jacekmalczyk
Copy link

You should treat type as an "input" not a separate field in your form. So in your case it should be given a ref and as an input, similar to name and e-mail inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants