Skip to content

Commit

Permalink
Merge pull request #180 from seasonedcc/array-of-objects
Browse files Browse the repository at this point in the history
Add array of objects example
  • Loading branch information
danielweinmann authored Apr 3, 2023
2 parents 938bd61 + 8858cd7 commit 0c912b4
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 2 deletions.
3 changes: 3 additions & 0 deletions apps/web/app/routes/examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export default function Component() {
<SidebarLayout.NavLink to={'/examples/schemas/array-of-strings'}>
Array of strings
</SidebarLayout.NavLink>
<SidebarLayout.NavLink to={'/examples/schemas/array-of-objects'}>
Array of objects
</SidebarLayout.NavLink>
<SidebarLayout.NavTitle>Forms</SidebarLayout.NavTitle>
<SidebarLayout.NavLink to={'/examples/forms/auto-generated'}>
Auto-generated
Expand Down
254 changes: 254 additions & 0 deletions apps/web/app/routes/examples/schemas/array-of-objects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import hljs from 'highlight.js/lib/common'
import type {
ActionFunction,
LoaderFunction,
MetaFunction,
} from '@remix-run/node'
import { formAction } from '~/formAction'
import { z } from 'zod'
import Form from '~/ui/form'
import { metaTags } from '~/helpers'
import { makeDomainFunction } from 'domain-functions'
import Example from '~/ui/example'
import { useRef } from 'react'
import { uniq } from 'lodash'

const title = 'Array of objects'
const description =
'In this example, we use custom inputs to manage an array of objects.'

export const meta: MetaFunction = () => metaTags({ title, description })

const code = `const schema = z.object({
title: z.string().min(1),
contacts: z
.array(z.object({ name: z.string().min(1), email: z.string().email() }))
.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',
uniq([...contacts, { name, email }]),
{ 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}>
<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>
<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>
)
}`

const schema = z.object({
title: z.string().min(1),
contacts: z
.array(z.object({ name: z.string().min(1), email: z.string().email() }))
.min(1),
})

export const loader: LoaderFunction = () => ({
code: hljs.highlight(code, { language: 'ts' }).value,
})

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 (
<Example title={title} description={description}>
<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',
uniq([...contacts, { name, email }]),
{ 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}>
<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>
<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>
</Example>
)
}
14 changes: 12 additions & 2 deletions apps/web/app/routes/examples/schemas/array-of-strings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@ const description =

export const meta: MetaFunction = () => metaTags({ title, description })

const code = `export default () => {
const code = `const schema = z.object({
title: z.string().min(1),
tags: z.array(z.string()).min(1),
})
const mutation = makeDomainFunction(schema)(async (values) => values)
export const action: ActionFunction = async ({ request }) =>
formAction({ request, schema, mutation })
export default () => {
const tagRef = useRef<HTMLInputElement>(null)
return (
Expand Down Expand Up @@ -139,7 +149,7 @@ export default () => {
if (value) {
setValue(
'tags',
uniq([...(tags || []), value.toLowerCase()]),
uniq([...tags, value.toLowerCase()]),
{ shouldValidate: true },
)
}
Expand Down

0 comments on commit 0c912b4

Please sign in to comment.