From abb042a474b5967d4b03dbfc65ce00293ae42b51 Mon Sep 17 00:00:00 2001
From: bloodyowl <bloodyowl@icloud.com>
Date: Tue, 9 Jul 2024 18:50:35 +0200
Subject: [PATCH] Support dynamic config

---
 src/useForm.ts                |  14 +++-
 website/src/App.tsx           |   3 +
 website/src/forms/Dynamic.tsx | 131 ++++++++++++++++++++++++++++++++++
 website/src/utils/router.ts   |   1 +
 website/yarn.lock             |   8 +--
 5 files changed, 150 insertions(+), 7 deletions(-)
 create mode 100644 website/src/forms/Dynamic.tsx

diff --git a/src/useForm.ts b/src/useForm.ts
index f69d927..d485fe0 100644
--- a/src/useForm.ts
+++ b/src/useForm.ts
@@ -233,7 +233,11 @@ export const useForm = <Values extends Required<Values>, ErrorMessage = string>(
       names.forEach((name) => fields.current[name].callbacks.add(callback));
 
       return () => {
-        names.forEach((name) => fields.current[name].callbacks.delete(callback));
+        names.forEach((name) => {
+          if (fields.current[name] != null) {
+            fields.current[name].callbacks.delete(callback);
+          }
+        });
       };
     };
 
@@ -399,7 +403,9 @@ export const useForm = <Values extends Required<Values>, ErrorMessage = string>(
             fields.current[name].callbacks.add(callback);
 
             return () => {
-              fields.current[name].callbacks.delete(callback);
+              if (fields.current[name] != null) {
+                fields.current[name].callbacks.delete(callback);
+              }
             };
           },
         }),
@@ -423,7 +429,9 @@ export const useForm = <Values extends Required<Values>, ErrorMessage = string>(
 
         return () => {
           if (isFirstMounting) {
-            fields.current[name].mounted = false;
+            if (fields.current[name] != null) {
+              fields.current[name].mounted = false;
+            }
           }
         };
       }, [name]);
diff --git a/website/src/App.tsx b/website/src/App.tsx
index 720b7d1..877e9ca 100644
--- a/website/src/App.tsx
+++ b/website/src/App.tsx
@@ -11,6 +11,7 @@ import { AsyncSubmissionForm } from "./forms/AsyncSubmissionForm";
 import { BasicForm } from "./forms/BasicForm";
 import { CheckboxesForm } from "./forms/CheckboxesForm";
 import { CreditCardForm } from "./forms/CreditCardForm";
+import { Dynamic } from "./forms/Dynamic";
 import { FieldsListenerForm } from "./forms/FieldsListenerForm";
 import { IBANForm } from "./forms/IBANForm";
 import { InputMaskingForm } from "./forms/InputMaskingForm";
@@ -76,6 +77,7 @@ export const App = () => {
             <Link to={Router.IBAN()}>IBAN</Link>
             <Link to={Router.CreditCard()}>Credit card</Link>
             <Link to={Router.InputMasking()}>Input masking</Link>
+            <Link to={Router.Dynamic()}>Dynamic fields</Link>
           </VStack>
         </Flex>
       )}
@@ -89,6 +91,7 @@ export const App = () => {
         .with({ name: "IBAN" }, () => <IBANForm />)
         .with({ name: "CreditCard" }, () => <CreditCardForm />)
         .with({ name: "InputMasking" }, () => <InputMaskingForm />)
+        .with({ name: "Dynamic" }, () => <Dynamic />)
         .with(P.nullish, () => <Page title="Not found" />)
         .exhaustive()}
     </Flex>
diff --git a/website/src/forms/Dynamic.tsx b/website/src/forms/Dynamic.tsx
new file mode 100644
index 0000000..7542f92
--- /dev/null
+++ b/website/src/forms/Dynamic.tsx
@@ -0,0 +1,131 @@
+import { Button } from "@chakra-ui/button";
+import { HStack, Spacer } from "@chakra-ui/layout";
+import { useToast } from "@chakra-ui/toast";
+import { useForm } from "@swan-io/use-form";
+import * as React from "react";
+import { Input } from "../components/Input";
+import { Page } from "../components/Page";
+
+export const Dynamic = () => {
+  const [fields, setFields] = React.useState<{ key: string; name: string }[]>([]);
+
+  const config = React.useMemo(
+    () =>
+      Object.fromEntries(
+        fields.map((item) => [
+          item.key,
+          {
+            strategy: "onBlur" as const,
+            initialValue: "",
+            sanitize: (value: string) => value.trim(),
+            validate: (value: string) => {
+              if (value === "") {
+                return "First name is required";
+              }
+            },
+          },
+        ]),
+      ),
+    [fields],
+  );
+
+  const { Field, resetForm, submitForm } = useForm(config);
+
+  const toast = useToast();
+
+  const onSubmit = (event: React.FormEvent) => {
+    event.preventDefault();
+
+    submitForm({
+      onSuccess: (values) => {
+        console.log("values", values);
+
+        toast({
+          title: "Submission succeeded",
+          status: "success",
+          duration: 5000,
+          isClosable: true,
+        });
+      },
+      onFailure: (errors) => {
+        console.log("errors", errors);
+
+        toast({
+          title: "Submission failed",
+          status: "error",
+          duration: 5000,
+          isClosable: true,
+        });
+      },
+    });
+  };
+
+  return (
+    <Page
+      title="Basic"
+      description={
+        <>
+          A common form example which play with at least two different strategies.
+          <br />
+          Note that all values are sanitized using trimming.
+        </>
+      }
+    >
+      <HStack justifyContent="flex-start" spacing={3}>
+        <Button
+          width={100}
+          onClick={() =>
+            setFields((fields) => [
+              ...fields,
+              { key: crypto.randomUUID(), name: `Field ${fields.length}` },
+            ])
+          }
+        >
+          Add field
+        </Button>
+        <Button width={140} onClick={() => setFields((fields) => fields.slice(0, -1))}>
+          Remove field
+        </Button>
+      </HStack>
+
+      <Spacer height={8} />
+
+      <form
+        onSubmit={onSubmit}
+        onReset={() => {
+          resetForm();
+        }}
+      >
+        {fields.map((field) => (
+          <Field name={field.key} key={field.key}>
+            {({ error, onBlur, onChange, ref, valid, value }) => (
+              <Input
+                label={field.name}
+                validation="Required"
+                strategy="onBlur"
+                error={error}
+                onBlur={onBlur}
+                onChangeText={onChange}
+                ref={ref}
+                valid={valid}
+                value={value}
+              />
+            )}
+          </Field>
+        ))}
+
+        <Spacer height={4} />
+
+        <HStack justifyContent="flex-end" spacing={3}>
+          <Button type="reset" width={100}>
+            Reset
+          </Button>
+
+          <Button colorScheme="green" type="submit" width={100}>
+            Submit
+          </Button>
+        </HStack>
+      </form>
+    </Page>
+  );
+};
diff --git a/website/src/utils/router.ts b/website/src/utils/router.ts
index 8fac7e0..ee437e2 100644
--- a/website/src/utils/router.ts
+++ b/website/src/utils/router.ts
@@ -10,6 +10,7 @@ const routesObject = {
   IBAN: "/iban",
   CreditCard: "/credit-card",
   InputMasking: "/input-masking",
+  Dynamic: "/dynamic",
 } as const;
 
 export const routes = Dict.keys(routesObject);
diff --git a/website/yarn.lock b/website/yarn.lock
index a76be25..4e124ac 100644
--- a/website/yarn.lock
+++ b/website/yarn.lock
@@ -1168,10 +1168,10 @@
   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz#0bb7ac3cd1c3292db1f39afdabfd03ccea3a3d34"
   integrity sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==
 
-"@swan-io/boxed@^2.1.1":
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/@swan-io/boxed/-/boxed-2.1.1.tgz#bdf17d725270c8560d2371972427b6f7e80da5d8"
-  integrity sha512-NzmIVAnJuzuPwm8ZR4I9tUpuo9qjtVNA7aTSZYdSvcJ+y7NTyt9+tCi4y87T3m8hACN2ATMqvTmRtHOSDIIz7w==
+"@swan-io/boxed@^2.3.0":
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/@swan-io/boxed/-/boxed-2.3.0.tgz#aafa19432a3fa2c00d472be7832632619c4ed5ff"
+  integrity sha512-ubonCw2GLblwsA9rodcDnhLvCY+0d9g0KyMFyzKZa+vPumWQ8IZ6HFjdQT3Nm5yC+/RnSmJ20RMPwOn/06M8Vw==
 
 "@swan-io/chicane@^2.0.0":
   version "2.0.0"