Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save lauslim12/61618bdb1c10b8f46bedea475c343f1e to your computer and use it in GitHub Desktop.

Select an option

Save lauslim12/61618bdb1c10b8f46bedea475c343f1e to your computer and use it in GitHub Desktop.

Revisions

  1. lauslim12 created this gist Apr 14, 2023.
    214 changes: 214 additions & 0 deletions chakra-ui-react-hook-form-dynamic-form.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,214 @@
    // Codesandbox: https://codesandbox.io/s/silly-wu-0fz2t1

    import React, { useState, useRef } from "react";
    import ReactDOM from "react-dom/client";
    import { ChakraProvider } from "@chakra-ui/react";
    import {
    Controller,
    useForm,
    useFormState,
    useFieldArray
    } from "react-hook-form";
    import { zodResolver } from "@hookform/resolvers/zod";
    import {
    Button,
    Flex,
    HStack,
    Heading,
    Text,
    Input,
    FormControl,
    FormLabel,
    VStack,
    FormErrorMessage,
    FormHelperText,
    Textarea
    } from "@chakra-ui/react";
    import { z } from "zod";
    import type { Control } from "react-hook-form";

    /**
    * Schema
    */

    const identitySchema = z.object({
    firstName: z.string().min(1),
    lastName: z.string().min(1)
    });

    const identityFormSchema = z.object({
    identities: z.array(identitySchema)
    });

    type IdentityFormSchema = z.infer<typeof identityFormSchema>;

    /**
    * Isolate submission to prevent re-renders
    */

    interface SubmitButtonProps {
    control: Control<IdentityFormSchema>;
    }

    function SubmitButton({ control }: SubmitButtonProps) {
    const { isValid, isSubmitting } = useFormState({ control });

    return (
    <Button
    size="sm"
    colorScheme="green"
    type="submit"
    isDisabled={!isValid || isSubmitting}
    >
    Submit
    </Button>
    );
    }

    /**
    * App
    */

    function App() {
    // Calculate re-render, first time we have to start from
    // -1 because initially it will only render one time.
    const renderCount = useRef(-1);
    renderCount.current = renderCount.current + 1;

    const [submittedData, setSubmittedData] = useState("");
    const { control, handleSubmit } = useForm<IdentityFormSchema>({
    resolver: zodResolver(identityFormSchema),
    mode: "all",
    defaultValues: {
    identities: [
    { firstName: "Naruto", lastName: "Uzumaki" },
    { firstName: "Sasuke", lastName: "Uchiha" },
    { firstName: "Boruto", lastName: "Uzumaki" },
    { firstName: "Kawaki", lastName: "Uzumaki" }
    ]
    }
    });
    const { append, fields, remove } = useFieldArray({
    control,
    name: "identities"
    });

    const submitChanges = handleSubmit((changes) => {
    setSubmittedData(JSON.stringify(changes, null, 4));
    });

    return (
    <Flex as="main" p={2} direction="column" textAlign="center">
    <Heading>
    Hello CodeSandbox{" "}
    <span role="img" aria-label="Zap icon" aria-labelledby="zap icon">
    </span>
    </Heading>
    <Text>Field Array Dynamic Form</Text>
    <Text>
    Rerender Count (Strict Mode, so twice, starting from 0):{" "}
    {renderCount.current}
    </Text>

    <form onSubmit={submitChanges}>
    {fields.map((item, index) => (
    <VStack key={item.id} px={4} py={2} spacing={4}>
    <HStack key={item.id} py={2} width="full">
    <FormControl isRequired>
    <FormLabel htmlFor={`${index}.firstName`}>First Name</FormLabel>
    <Controller
    control={control}
    name={`identities.${index}.firstName`}
    defaultValue=""
    render={({ fieldState, field }) => (
    <FormControl
    isInvalid={fieldState.invalid}
    textAlign="left"
    >
    <Input id={`${index}.firstName`} type="text" {...field} />

    {fieldState.error ? (
    <FormErrorMessage>
    {fieldState.error.message}
    </FormErrorMessage>
    ) : (
    <FormHelperText>Input first name.</FormHelperText>
    )}
    </FormControl>
    )}
    />
    </FormControl>

    <FormControl isRequired>
    <FormLabel htmlFor={`${index}.lastName`}>Last Name</FormLabel>
    <Controller
    control={control}
    name={`identities.${index}.lastName`}
    defaultValue=""
    render={({ fieldState, field }) => (
    <FormControl
    isInvalid={fieldState.invalid}
    textAlign="left"
    >
    <Input id={`${index}.lastName`} type="text" {...field} />
    {fieldState.error ? (
    <FormErrorMessage>
    {fieldState.error.message}
    </FormErrorMessage>
    ) : (
    <FormHelperText>Input last name.</FormHelperText>
    )}
    </FormControl>
    )}
    />
    </FormControl>
    </HStack>

    <Button
    colorScheme="red"
    size="sm"
    onClick={() => remove(index)}
    width="full"
    >
    Remove
    </Button>
    </VStack>
    ))}

    <HStack as="section" justify="center" mt={4}>
    <Button
    colorScheme="yellow"
    size="sm"
    onClick={() =>
    append({ firstName: "", lastName: "" }, { shouldFocus: false })
    }
    >
    Add New Identity
    </Button>

    <SubmitButton control={control} />
    </HStack>
    </form>

    {submittedData && (
    <Textarea height="500px" mt={4} value={submittedData} isReadOnly />
    )}
    </Flex>
    );
    }

    /**
    * React 18 render
    */

    const rootElement = document.getElementById("root")!;
    const root = ReactDOM.createRoot(rootElement);

    root.render(
    <React.StrictMode>
    <ChakraProvider>
    <App />
    </ChakraProvider>
    </React.StrictMode>
    );