@chris-c-brine/form-dialog 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,91 +42,55 @@ This package has the following peer dependencies that need to be installed in yo
42
42
 
43
43
  ```tsx
44
44
  // LoginPage.tsx
45
- import { useCallback, type FC, type PropsWithChildren } from "react";
45
+ import { type FC } from "react";
46
46
  import { Container, IconButton } from "@mui/material";
47
- import { globalErrorAtom, useUser } from "@features";
48
47
  import { useDialog } from "@chris-c-brine/form-dialog";
49
- import { useForm, type SubmitHandler } from "react-hook-form-mui";
48
+ import LoginForm from "./components/forms/LoginForm";
49
+ import { Lock as LockIcon } from "@mui/icons-material";
50
50
  import { LoginDialog } from "./LoginDialog";
51
- import LoginForm from ".//LoginForm";
52
- import { Lock as LockIcon, Person as PersonIcon } from "@mui/icons-material";
53
- import { useSetAtom } from "jotai";
54
51
 
55
- const defaultLoginFormValues = { username: "", password: "" };
56
- type SubmitLogin = SubmitHandler<typeof defaultLoginFormValues>;
57
52
  const LoginPage: FC = () => {
58
- const { dialogProps, closeDialog, openDialog } = useDialog();
59
- const { setUser } = useUser();
60
- const setError = useSetAtom(globalErrorAtom);
61
-
62
- const formContext = useForm({ defaultValues: defaultLoginFormValues });
63
-
64
- const onSuccess: SubmitLogin = useCallback(
65
- (data, event) => {
66
- event?.preventDefault(); // Stop default html form submit
67
- formContext.reset(); // Reset form
68
- setUser({ name: data.username, isActive: true }); // Update User (and/or other business logic)
69
- closeDialog(); // Close Dialog
70
- setError({
71
- message: <>Hello {data.username}!</>,
72
- title: 'Successful Login!',
73
- severity: "success",
74
- icon: <PersonIcon sx={{fontSize: 35}} />
75
- });
76
- },
77
- [formContext, setUser, closeDialog, setError],
78
- );
53
+ const { dialogProps, openDialog } = useDialog();
79
54
 
80
55
  return (
81
- <LoginFormContainer>
82
- <LoginForm onSuccess={onSuccess}>
56
+ <Container
57
+ component="main"
58
+ maxWidth={"xs"}
59
+ sx={{
60
+ display: "flex",
61
+ alignItems: "center",
62
+ justifyContent: "center",
63
+ height: "85vh",
64
+ overflow: "hidden",
65
+ animation: "fadeIn 1s ease-in-out",
66
+ "@keyframes fadeIn": {
67
+ from: { opacity: 0 },
68
+ to: { opacity: 1 },
69
+ },
70
+ }}
71
+ >
72
+ <LoginForm>
83
73
  <IconButton color="primary" onClick={openDialog} sx={{ py: 1 }}>
84
74
  <LockIcon sx={{ fontSize: 50 }} />
85
75
  </IconButton>
86
76
  </LoginForm>
87
- <LoginDialog
88
- dialogProps={{ ...dialogProps }}
89
- handleSubmit={onSuccess}
90
- onClose={closeDialog}
91
- />
92
- </LoginFormContainer>
77
+ <LoginDialog dialogProps={dialogProps} />
78
+ </Container>
93
79
  );
94
80
  };
95
81
 
96
82
  export default LoginPage;
97
-
98
- const LoginFormContainer: FC<PropsWithChildren> = ({ children }) => (
99
- <Container
100
- component="main"
101
- maxWidth={"xs"}
102
- sx={{
103
- display: "flex",
104
- alignItems: "center",
105
- justifyContent: "center",
106
- height: "85vh",
107
- overflow: "hidden",
108
- animation: "fadeIn 1s ease-in-out",
109
- "@keyframes fadeIn": {
110
- from: { opacity: 0 },
111
- to: { opacity: 1 },
112
- },
113
- }}
114
- >
115
- {children}
116
- </Container>
117
- );
118
83
  ```
119
84
  ```tsx
120
85
  // LoginPageConstants.ts
121
86
  import type { SubmitHandler } from "react-hook-form";
122
87
 
123
88
  export const defaultLoginFormValues = { username: "", password: "" };
124
- export type LoginForm = typeof defaultLoginFormValues;
125
- export type SubmitLogin = SubmitHandler<LoginForm>;
89
+ export type LoginFormValues = typeof defaultLoginFormValues;
126
90
  ```
127
91
  ```tsx
128
92
  // LoginFormBase.tsx
129
- import {TextFieldElement, PasswordElement, type PasswordElementProps, useFormContext} from "react-hook-form-mui";
93
+ import { TextFieldElement, PasswordElement, type PasswordElementProps, useFormContext} from "react-hook-form-mui";
130
94
  import { memo, useEffect } from "react";
131
95
  import { useFormDialog, AutoGrid, type AutoGridProps } from "@chris-c-brine/form-dialog";
132
96
 
@@ -177,6 +141,7 @@ const UserName = ({ disabled }: Pick<PasswordElementProps, "disabled">) => (
177
141
  }}
178
142
  />
179
143
  );
144
+ UserName.displayName = "UserName";
180
145
 
181
146
  const Password = ({ disabled }: Pick<PasswordElementProps, "disabled">) => {
182
147
  const { setValue, getValues } = useFormContext();
@@ -184,7 +149,7 @@ const Password = ({ disabled }: Pick<PasswordElementProps, "disabled">) => {
184
149
 
185
150
  useEffect(() => {
186
151
  if (disabled && password !== "") {
187
- setValue("passwordDisabled", "");
152
+ setValue("disabledPassword", "");
188
153
  }
189
154
  }, [disabled, setValue, password]);
190
155
 
@@ -205,26 +170,29 @@ const Password = ({ disabled }: Pick<PasswordElementProps, "disabled">) => {
205
170
  />
206
171
  );
207
172
  };
173
+
174
+ Password.displayName = "Password";
208
175
  ```
209
176
  ```tsx
210
177
  // LoginForm.tsx
211
178
  import { Lock as LockIcon } from "@mui/icons-material";
212
179
  import { Box, Typography, type TypographyProps } from "@mui/material";
213
- import { type SubmitLogin, defaultLoginFormValues } from "./LoginPageConstants";
214
- import LoginFormBase from "./LoginFormBase";
215
180
  import { merge } from "lodash";
216
- import type { FC, PropsWithChildren } from "react";
181
+ import { FC, PropsWithChildren, useCallback, useMemo } from "react";
217
182
  import { FormDialogActions, FormDialogProvider, PaperForm } from "@chris-c-brine/form-dialog";
183
+ import { SubmitHandler, useForm } from "react-hook-form-mui";
184
+ import { defaultLoginFormValues, LoginFormValues } from "./LoginPageConstants";
185
+ import LoginFormBase from "./LoginFormBase";
186
+ import { globalErrorAtom, useUser } from "@features";
187
+ import { useSetAtom } from "jotai/index";
188
+ import { Person as PersonIcon } from "@mui/icons-material";
218
189
 
219
190
  const AltIcon = () => <LockIcon sx={{ mr: 1, fontSize: 20 }} />;
220
191
 
221
- interface LoginFormProps extends PropsWithChildren {
222
- onSuccess: SubmitLogin;
223
- }
192
+ export const LoginForm: FC<PropsWithChildren> = ({ children }) => {
224
193
 
225
- const LoginForm: FC<LoginFormProps> = ({ onSuccess, children }) => {
226
194
  return (
227
- <LoginPaperForm onSuccess={onSuccess}>
195
+ <LoginPaperForm>
228
196
  <Box px={2}>
229
197
  {children}
230
198
  <SecureLoginText />
@@ -243,8 +211,6 @@ const LoginForm: FC<LoginFormProps> = ({ onSuccess, children }) => {
243
211
  );
244
212
  };
245
213
 
246
- export default LoginForm;
247
-
248
214
  const SecureLoginText: FC<TypographyProps> = (props) => {
249
215
  const typographyProps = merge(
250
216
  {
@@ -257,18 +223,38 @@ const SecureLoginText: FC<TypographyProps> = (props) => {
257
223
  return <Typography {...typographyProps}>Secure Login</Typography>;
258
224
  };
259
225
 
260
- type LoginPaperFormProps = PropsWithChildren & {
261
- onSuccess?: SubmitLogin;
262
- };
226
+ const LoginPaperForm: FC<PropsWithChildren> = ({ children }) => {
227
+ const formContext = useForm({defaultValues: defaultLoginFormValues});
228
+ const { setUser } = useUser();
229
+ const setError = useSetAtom(globalErrorAtom);
230
+ const reset = useMemo(() => formContext.reset, [formContext?.reset]);
231
+
232
+ const onSuccess: SubmitHandler<LoginFormValues> = useCallback(
233
+ (data, event) => {
234
+ event?.preventDefault(); // Stop default html form submit
235
+ event?.stopPropagation(); // STOP!!!!!
236
+
237
+ setUser({ name: data.username, isActive: true }); // Update User (and/or other business logic)
238
+ reset(); // reset form
239
+ setError({ // Signal Success!
240
+ message: <>Hello {data.username}!</>,
241
+ title: "Successful Login!",
242
+ severity: "success",
243
+ icon: <PersonIcon sx={{ fontSize: 35 }} />,
244
+ });
245
+ }, [setUser, setError, reset]);
263
246
 
264
- const LoginPaperForm: FC<LoginPaperFormProps> = ({ children, onSuccess }) => {
265
247
  return (
266
248
  <FormDialogProvider>
267
249
  <PaperForm
250
+ persistKey={'login-page-form'}
268
251
  formProps={{
269
- defaultValues: defaultLoginFormValues,
252
+ formContext,
270
253
  onSuccess,
271
- onError: (e) => console.log(e),
254
+ onError: (errors, event) => {
255
+ event?.preventDefault();
256
+ console.log(errors)
257
+ },
272
258
  }}
273
259
  elevation={3}
274
260
  sx={{
@@ -285,39 +271,63 @@ const LoginPaperForm: FC<LoginPaperFormProps> = ({ children, onSuccess }) => {
285
271
  ```
286
272
  ```tsx
287
273
  // LoginDialog.tsx
288
- import {useDialog, FormDialog, FormDialogActions, type FormDialogProps, PersistForm } from "@chris-c-brine/form-dialog";
289
- import { type FC } from "react";
290
- import { useForm } from "react-hook-form-mui";
274
+ import { useDialog, FormDialog, FormDialogActions } from "@chris-c-brine/form-dialog";
275
+ import { memo, useCallback, useMemo } from "react";
291
276
  import LoginFormBase from "./LoginFormBase";
292
- import {
293
- defaultLoginFormValues,
294
- type LoginForm,
295
- type SubmitLogin,
296
- } from "./LoginPageConstants";
277
+ import { defaultLoginFormValues, LoginFormValues } from "./LoginPageConstants";
278
+ import { SubmitHandler, useForm } from "react-hook-form-mui";
279
+ import { globalErrorAtom, useUser } from "@features";
280
+ import { useSetAtom } from "jotai/index";
281
+ import { Person as PersonIcon } from "@mui/icons-material";
297
282
 
298
283
  const formKey = "dialog-login-form";
299
284
 
300
- /* Basic Form Dialog Test */
301
- type LoginDialogProps = Pick<ReturnType<typeof useDialog>, "dialogProps"> &
302
- Pick<FormDialogProps<LoginForm>, "onClose"> & {
303
- handleSubmit: SubmitLogin;
304
- };
285
+ export type LoginDialogProps = {
286
+ dialogProps: ReturnType<typeof useDialog>["dialogProps"];
287
+ };
305
288
 
306
- export const LoginDialog: FC<LoginDialogProps> = ({ dialogProps, handleSubmit }) => {
289
+ export const LoginDialog = memo(function ({ dialogProps }: LoginDialogProps) {
290
+ const setError = useSetAtom(globalErrorAtom);
291
+ const { setUser } = useUser();
307
292
  const formContext = useForm({ defaultValues: defaultLoginFormValues });
308
293
 
294
+ const reset = useMemo(() => formContext.reset, [formContext?.reset]);
295
+
296
+ const onSuccess: SubmitHandler<LoginFormValues> = useCallback(
297
+ (data, event) => {
298
+ event?.preventDefault();
299
+ event?.stopPropagation();
300
+
301
+ console.log(event);
302
+ setUser({ name: data.username, isActive: true }); // Update User (and/or other business logic)
303
+ reset(); // reset form
304
+ setError({
305
+ // Signal Success!
306
+ message: <>Hello {data.username}!</>,
307
+ title: "Successful Login!",
308
+ severity: "success",
309
+ icon: <PersonIcon sx={{ fontSize: 35 }} />,
310
+ });
311
+ dialogProps?.onClose();
312
+ },
313
+ [setUser, setError, dialogProps, reset],
314
+ );
315
+
309
316
  return (
310
317
  <FormDialog
311
318
  {...dialogProps}
312
- formProps={{ onSuccess: handleSubmit, formContext, onError: (e) => console.log(e) }}
319
+ persistKey={formKey}
320
+ formProps={{ onSuccess, formContext }}
313
321
  title={"Basic Persist Form Dialog Test"}
314
322
  titleProps={{ variant: "h5", textAlign: "center" }}
315
323
  actions={<FormDialogActions resetProps={{ formKey }} submitProps={{ maxAttempts: 3 }} />}
316
324
  >
317
- <PersistForm formName={formKey}>
318
- <LoginFormBase name={formKey} columnCount={2} />
319
- </PersistForm>
325
+ <LoginFormBase columnCount={2} />
320
326
  </FormDialog>
321
327
  );
322
- };
328
+ });
329
+
330
+ LoginDialog.displayName = "LoginDialog";
331
+
332
+
323
333
  ```
@@ -14,7 +14,7 @@ export type FormSubmitButtonProps = Omit<LoadingButtonProps, "onClick"> & {
14
14
  maxAttempts?: number;
15
15
  };
16
16
  /**
17
- * A submit button for forms with loading state, attempt tracking and form context awareness
17
+ * A "submit button" for forms with loading state, attempt tracking and form context awareness
18
18
  *
19
19
  * This component extends the LoadingButton with form-specific features:
20
20
  * - Automatically displays loading state during form submission
@@ -9,12 +9,12 @@ export type BaseDialogProps = BlackoutDialogProps & {
9
9
  title?: ReactNode;
10
10
  /**
11
11
  * Props passed to the DialogTitle component
12
- * Only applied when title is provided
12
+ * Only applied when the title is provided
13
13
  */
14
14
  titleProps?: DialogTitleProps;
15
15
  /**
16
16
  * Props passed to the DialogContent component
17
- * Only applied when children is provided
17
+ * Only applied when children are provided
18
18
  */
19
19
  contentProps?: DialogContentProps;
20
20
  /**
@@ -24,7 +24,7 @@ export type BaseDialogProps = BlackoutDialogProps & {
24
24
  actions?: ReactNode;
25
25
  /**
26
26
  * Props passed to the DialogActions component
27
- * Only applied when actions is provided
27
+ * Only applied when actions are provided
28
28
  */
29
29
  actionsProps?: DialogActionsProps;
30
30
  /**
@@ -1,15 +1,10 @@
1
1
  import { type BaseDialogProps } from "./BaseDialog";
2
- import { type FieldValues, type FormContainerProps } from "react-hook-form-mui";
2
+ import { CommonFormProps } from "../forms/PaperForm";
3
+ import { type FieldValues } from "react-hook-form-mui";
3
4
  /**
4
5
  * Props for the FormDialog component
5
6
  */
6
- export type FormDialogProps<T extends FieldValues> = Omit<BaseDialogProps, "PaperComponent"> & {
7
- /**
8
- * Form methods from useForm hook
9
- * Establish the form context for child components
10
- */
11
- formProps: FormContainerProps<T>;
12
- };
7
+ export type FormDialogProps<T extends FieldValues> = Omit<BaseDialogProps, "PaperComponent"> & CommonFormProps<T>;
13
8
  /**
14
9
  * A dialog component specialized for forms with integrated context providers
15
10
  *
@@ -31,4 +26,7 @@ export type FormDialogProps<T extends FieldValues> = Omit<BaseDialogProps, "Pape
31
26
  * - useFormDialog() - Access dialog controls and state
32
27
  *
33
28
  */
34
- export declare const FormDialog: <T extends FieldValues>({ formProps, children, open, onClose, ...dialogProps }: FormDialogProps<T>) => import("react/jsx-runtime").JSX.Element;
29
+ export declare const FormDialog: {
30
+ <T extends FieldValues>({ formProps, children, open, onClose, persistKey, ...dialogProps }: FormDialogProps<T>): import("react/jsx-runtime").JSX.Element;
31
+ displayName: string;
32
+ };
@@ -16,7 +16,7 @@ export type FormDialogActionsProps = Partial<PropsWithChildren> & {
16
16
  */
17
17
  resetProps?: FormResetButtonProps;
18
18
  /**
19
- * Props to customize the submit button
19
+ * Props to customize the "submit button"
20
20
  */
21
21
  submitProps?: FormSubmitButtonProps;
22
22
  /**
@@ -1,15 +1,23 @@
1
1
  import { type PaperProps } from "@mui/material";
2
2
  import { type FieldValues, type FormContainerProps } from "react-hook-form-mui";
3
- /**
4
- * Props for the PaperForm component
5
- */
6
- export type PaperFormProps<T extends FieldValues> = PaperProps & {
3
+ export type CommonFormProps<T extends FieldValues> = {
7
4
  /**
8
5
  * Props to configure the form container
9
- * Includes settings for form state, validation, and submission handling
6
+ * This includes settings for form state, validation, and submission handling
10
7
  */
11
8
  formProps: FormContainerProps<T>;
9
+ /**
10
+ * Optional key to use for form state persistence
11
+ * When provided, the form state will be persisted in
12
+ * session storage with a fallback to local storage (TODO: make configurable)
13
+ * and restored on form reload/mount.
14
+ */
15
+ persistKey?: string;
12
16
  };
17
+ /**
18
+ * Props for the PaperForm component
19
+ */
20
+ export type PaperFormProps<T extends FieldValues> = PaperProps & CommonFormProps<T>;
13
21
  /**
14
22
  * A component that combines a Material UI Paper with a form container
15
23
  *
@@ -56,4 +64,4 @@ export type PaperFormProps<T extends FieldValues> = PaperProps & {
56
64
  *
57
65
  * @template T - The type of form values being handled
58
66
  */
59
- export declare const PaperForm: <T extends FieldValues>({ children, formProps, ...paperProps }: PaperFormProps<T>) => import("react/jsx-runtime").JSX.Element;
67
+ export declare const PaperForm: <T extends FieldValues>({ children, persistKey, formProps, ...paperProps }: PaperFormProps<T>) => import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,4 @@
1
- import type { FC, PropsWithChildren } from "react";
1
+ import { type PropsWithChildren } from "react";
2
2
  /**
3
3
  * Props for the PersistForm component
4
4
  */
@@ -50,4 +50,4 @@ export interface PersistFormProps extends PropsWithChildren {
50
50
  * </PersistForm>
51
51
  * </FormDialog>
52
52
  */
53
- export declare const PersistForm: FC<PersistFormProps>;
53
+ export declare const PersistForm: import("react").NamedExoticComponent<PersistFormProps>;
@@ -1,5 +1,5 @@
1
1
  export * from "./useFormDialog";
2
2
  export * from "./useMaxAttempts";
3
- export * from "./usePersistedForm";
3
+ export * from "./usePersistForm";
4
4
  export * from "./useOnMount";
5
5
  export * from "./useDialog";
@@ -1,19 +1,20 @@
1
- import { DialogProps } from "@mui/material";
1
+ import { MouseEvent } from "react";
2
2
  export interface UseDialogProps {
3
3
  /** Initial open state of the dialog */
4
4
  open?: boolean;
5
- /** Additional props to spread onto the dialog component */
6
- dialogProps?: Omit<DialogProps, "open" | "onClose">;
5
+ /** Always keep the children in the DOM. */
6
+ keepMounted?: boolean;
7
7
  }
8
- export interface UseDialogReturn extends Omit<DialogProps, "open" | "onClose"> {
8
+ export interface UseDialogReturn {
9
9
  /** Function to close the dialog */
10
10
  closeDialog: () => void;
11
11
  /** Function to open the dialog */
12
- openDialog: () => void;
12
+ openDialog: (e?: MouseEvent) => void;
13
13
  /** Props to spread onto the dialog component */
14
- dialogProps: DialogProps & {
14
+ dialogProps: {
15
15
  open: boolean;
16
16
  onClose: () => void;
17
+ keepMounted: boolean;
17
18
  };
18
19
  }
19
20
  /**
@@ -23,6 +24,8 @@ export interface UseDialogReturn extends Omit<DialogProps, "open" | "onClose"> {
23
24
  * functions to open and close the dialog, along with props that can be
24
25
  * spread onto a Material-UI Dialog component.
25
26
  *
27
+ * Restores focus to the opening element, but only after the dialog has fully closed.
28
+ *
26
29
  * @example
27
30
  * // Basic usage
28
31
  * const { openDialog, closeDialog, dialogProps } = useDialog();
@@ -35,7 +38,6 @@ export interface UseDialogReturn extends Omit<DialogProps, "open" | "onClose"> {
35
38
  * // With initial configuration
36
39
  * const { dialogProps } = useDialog({ keepMounted: true});
37
40
  *
38
- *
39
41
  * @param props - Optional configuration options
40
42
  */
41
43
  export declare const useDialog: (props?: UseDialogProps) => UseDialogReturn;
@@ -2,7 +2,7 @@
2
2
  * Hook for accessing the FormDialog context values and functions
3
3
  *
4
4
  * This hook provides access to the form dialog state and controls managed by
5
- * the FormDialogProvider. It allows components to interact with dialog state,
5
+ * the FormDialogProvider. It allows components to interact with the dialog state,
6
6
  * including open/close status and form-wide disabled state.
7
7
  *
8
8
  * The context provides:
@@ -0,0 +1,29 @@
1
+ import { FieldValues, UseFormReturn } from "react-hook-form-mui";
2
+ export type PersistedFormProviderProps<T extends FieldValues> = {
3
+ /**
4
+ * A unique key for the form
5
+ */
6
+ formName: string | undefined;
7
+ /**
8
+ * Represents the context of a form, which is used to manage the state and actions of the form.
9
+ * The context is typically provided by a form management library and used to handle form inputs,
10
+ * validation, and submission.
11
+ */
12
+ formContext: UseFormReturn<T, any, T> | UseFormReturn<T> | undefined;
13
+ };
14
+ /**
15
+ * Hook that enables form state persistence across sessions
16
+ *
17
+ * This hook connects a form to persistent storage (e.g., sessionStorage)
18
+ * allowing form values to be preserved when navigating away and returning.
19
+ * It works by watching form changes and syncing with a zustand store.
20
+ *
21
+ * Key features:
22
+ * - Persists form values during navigation or page reloads
23
+ * - Automatically restores saved values when form is rendered
24
+ * - Debounced updates to avoid excessive storage operations
25
+ * - Only saves changed fields, not the entire form state
26
+ * - Automatically clears storage when form values match defaults
27
+ *
28
+ */
29
+ export declare const usePersistForm: <T extends FieldValues>({ formName, formContext }: PersistedFormProviderProps<T>) => void;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from "./components";
2
- export { usePersistedForm, useFormDialog, useDialog } from "./hooks";
2
+ export { usePersistForm, useFormDialog, useDialog } from "./hooks";
3
3
  export { FormDialogProvider } from "./state/FormDialogProvider";