@graphcommerce/react-hook-form 9.0.0-canary.79 → 9.0.0-canary.80

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.0.0-canary.80
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2341](https://github.com/graphcommerce-org/graphcommerce/pull/2341) [`16e2980`](https://github.com/graphcommerce-org/graphcommerce/commit/16e2980da4b72330642e59e8c82d1acde387e4fc) - useFormGql and it's derived hooks now have a new `skipUnchanged` prop. The form will only be submitted when there are fields dirty in a form. This reduces the amount of queries ran in the checkout greatly. ([@Giovanni-Schroevers](https://github.com/Giovanni-Schroevers))
8
+
9
+ - [#2341](https://github.com/graphcommerce-org/graphcommerce/pull/2341) [`1d6512d`](https://github.com/graphcommerce-org/graphcommerce/commit/1d6512d4118cfb46602aa1f2432c3566fdb3261d) - Rename experimental_useV2 prop to deprecated_useV1 in useFromGql and enable it by default ([@Giovanni-Schroevers](https://github.com/Giovanni-Schroevers))
10
+
11
+ ### Patch Changes
12
+
13
+ - [#2341](https://github.com/graphcommerce-org/graphcommerce/pull/2341) [`af45239`](https://github.com/graphcommerce-org/graphcommerce/commit/af452399eaab59ee4e13484fdc9cb0a7660da531) - When a useFormGql throws an error in the onBeforeSubmit method or onComplete method it will setError('root.thrown') with the message, allowing it to be displayed somewhere. PaymentMethodButton will now render this as an ErrorSnackbar. ([@Giovanni-Schroevers](https://github.com/Giovanni-Schroevers))
14
+
3
15
  ## 9.0.0-canary.79
4
16
 
5
17
  ## 9.0.0-canary.78
package/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  export * from 'react-hook-form'
2
+ export * from './src/ComposedForm'
3
+ export * from './src/useFormAutoSubmit'
4
+ export * from './src/useFormGql'
2
5
  export * from './src/useFormGqlMutation'
3
6
  export * from './src/useFormGqlQuery'
4
- export * from './src/useFormAutoSubmit'
5
- export * from './src/useFormPersist'
6
7
  export * from './src/useFormMuiRegister'
8
+ export * from './src/useFormPersist'
7
9
  export * from './src/useFormValidFields'
8
- export * from './src/useFormGql'
9
- export * from './src/validationPatterns'
10
- export * from './src/ComposedForm'
10
+ export * from './src/utils/tryTuple'
11
11
  export * from './src/utils/useDebounce'
12
+ export * from './src/validationPatterns'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/react-hook-form",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.0.0-canary.79",
5
+ "version": "9.0.0-canary.80",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -16,9 +16,9 @@
16
16
  },
17
17
  "peerDependencies": {
18
18
  "@apollo/client": "^3",
19
- "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.79",
20
- "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.79",
21
- "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.79",
19
+ "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.80",
20
+ "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.80",
21
+ "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.80",
22
22
  "@mui/utils": "^5",
23
23
  "graphql": "^16.6.0",
24
24
  "react": "^18.2.0",
@@ -1,5 +1,6 @@
1
1
  import { ApolloError } from '@apollo/client'
2
2
  import React, { useContext, useEffect, useRef } from 'react'
3
+ import { GlobalError } from 'react-hook-form'
3
4
  import { isFormGqlOperation } from '../useFormGqlMutation'
4
5
  import { composedFormContext } from './context'
5
6
  import { ComposedSubmitRenderComponentProps } from './types'
@@ -119,9 +120,19 @@ export function ComposedSubmit(props: ComposedSubmitProps) {
119
120
  }
120
121
 
121
122
  const errors: ApolloError[] = []
123
+ let rootThrown: GlobalError | undefined
124
+
122
125
  formEntries.forEach(([, { form }]) => {
123
126
  if (form && isFormGqlOperation(form) && form.error) errors.push(form.error)
127
+ if (form && form.formState.errors.root?.thrown) rootThrown = form.formState.errors.root.thrown
124
128
  })
125
129
 
126
- return <Render buttonState={buttonState} submit={submitAll} error={mergeErrors(errors)} />
130
+ return (
131
+ <Render
132
+ buttonState={buttonState}
133
+ submit={submitAll}
134
+ error={mergeErrors(errors)}
135
+ rootThrown={rootThrown}
136
+ />
137
+ )
127
138
  }
@@ -1,5 +1,5 @@
1
1
  import { ApolloError } from '@apollo/client'
2
- import type { FieldValues, FormState, UseFormReturn } from 'react-hook-form'
2
+ import type { FieldValues, FormState, GlobalError, UseFormReturn } from 'react-hook-form'
3
3
  import type { SetOptional } from 'type-fest'
4
4
 
5
5
  export type UseFormComposeOptions<V extends FieldValues = FieldValues> = {
@@ -36,6 +36,7 @@ export type ComposedSubmitRenderComponentProps = {
36
36
  submit: () => Promise<void>
37
37
  buttonState: ButtonState
38
38
  error?: ApolloError
39
+ rootThrown?: GlobalError
39
40
  }
40
41
 
41
42
  export type ComposedFormState = {
@@ -1,27 +1,39 @@
1
1
  import {
2
- FetchResult,
3
- TypedDocumentNode,
4
- MutationTuple,
5
2
  ApolloError,
3
+ FetchResult,
6
4
  LazyQueryResultTuple,
5
+ MutationTuple,
6
+ TypedDocumentNode,
7
7
  } from '@apollo/client'
8
+ import { getOperationName } from '@apollo/client/utilities'
8
9
  import useEventCallback from '@mui/utils/useEventCallback'
9
10
  import { useEffect, useRef } from 'react'
10
11
  import { DefaultValues, FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form'
11
12
  import diff from './diff'
12
13
  import { useGqlDocumentHandler, UseGqlDocumentHandler } from './useGqlDocumentHandler'
14
+ import { tryAsync } from './utils/tryTuple'
13
15
 
14
16
  export type OnCompleteFn<Q, V> = (data: FetchResult<Q>, variables: V) => void | Promise<void>
15
17
 
16
18
  type UseFormGraphQLCallbacks<Q, V> = {
17
19
  /**
18
20
  * Allows you to modify the variablels computed by the form to make it compatible with the GraphQL
19
- * Mutation. Also allows you to send false to skip submission.
21
+ * Mutation.
22
+ *
23
+ * When returning false, it will silently stop the submission.
24
+ * When an error is thrown, it will be set as a generic error with `setError('root.thrown', { message: error.message })`
20
25
  */
21
26
  onBeforeSubmit?: (variables: V) => V | false | Promise<V | false>
27
+ /**
28
+ * Called after the mutation has been executed. Allows you to handle the result of the mutation.
29
+ *
30
+ * When an error is thrown, it will be set as a generic error with `setError('root.thrown', { message: error.message })`
31
+ */
22
32
  onComplete?: OnCompleteFn<Q, V>
23
33
 
24
34
  /**
35
+ * @deprecated Not used anymore, is now the default
36
+ *
25
37
  * Changes:
26
38
  * - Restores `defaultValues` functionality to original functionality, use `values` instead.
27
39
  * - Does not reset the form after submission, use `values` instead.
@@ -44,6 +56,19 @@ type UseFormGraphQLCallbacks<Q, V> = {
44
56
  * ```
45
57
  */
46
58
  experimental_useV2?: boolean
59
+ /**
60
+ * To restore the previous functionality of the useFormGqlMutation, set this to true.
61
+ *
62
+ * @deprecated Will be removed in the next version.
63
+ */
64
+ deprecated_useV1?: boolean
65
+
66
+ /**
67
+ * Only submit the form when there are dirty fields. If all fields are clean, we skip the submission.
68
+ *
69
+ * Form is still set to isSubmitted and isSubmitSuccessful.
70
+ */
71
+ skipUnchanged?: boolean
47
72
  }
48
73
 
49
74
  export type UseFormGraphQlOptions<Q, V extends FieldValues> = UseFormProps<V> &
@@ -73,6 +98,7 @@ export function useFormGql<Q, V extends FieldValues>(
73
98
  form: UseFormReturn<V>
74
99
  tuple: MutationTuple<Q, V> | LazyQueryResultTuple<Q, V>
75
100
  defaultValues?: UseFormProps<V>['defaultValues']
101
+ skipUnchanged?: boolean
76
102
  } & UseFormGraphQLCallbacks<Q, V>,
77
103
  ): UseFormGqlMethods<Q, V> {
78
104
  const {
@@ -81,8 +107,9 @@ export function useFormGql<Q, V extends FieldValues>(
81
107
  document,
82
108
  form,
83
109
  tuple,
110
+ skipUnchanged,
84
111
  defaultValues,
85
- experimental_useV2 = false,
112
+ deprecated_useV1 = false,
86
113
  } = options
87
114
  const { encode, type, ...gqlDocumentHandler } = useGqlDocumentHandler<Q, V>(document)
88
115
  const [execute, { data, error, loading }] = tuple
@@ -94,7 +121,7 @@ export function useFormGql<Q, V extends FieldValues>(
94
121
  const controllerRef = useRef<AbortController | undefined>()
95
122
  const valuesString = JSON.stringify(defaultValues)
96
123
  useEffect(() => {
97
- if (experimental_useV2) return
124
+ if (!deprecated_useV1) return
98
125
 
99
126
  if (initital.current) {
100
127
  initital.current = false
@@ -105,36 +132,56 @@ export function useFormGql<Q, V extends FieldValues>(
105
132
  // eslint-disable-next-line react-hooks/exhaustive-deps
106
133
  }, [valuesString, form])
107
134
 
108
- const beforeSubmit: NonNullable<typeof onBeforeSubmit> = useEventCallback(
109
- onBeforeSubmit ?? ((v) => v),
135
+ const beforeSubmit = useEventCallback(
136
+ tryAsync((onBeforeSubmit ?? ((v) => v)) satisfies NonNullable<typeof onBeforeSubmit>),
137
+ )
138
+ const complete = useEventCallback(
139
+ tryAsync((onComplete ?? (() => undefined)) satisfies NonNullable<typeof onComplete>),
110
140
  )
111
- const complete: NonNullable<typeof onComplete> = useEventCallback(onComplete ?? (() => undefined))
112
141
 
113
142
  const handleSubmit: UseFormReturn<V>['handleSubmit'] = (onValid, onInvalid) =>
114
143
  form.handleSubmit(async (formValues, event) => {
144
+ const hasDirtyFields = skipUnchanged
145
+ ? Object.values(form?.formState.dirtyFields ?? []).filter(Boolean).length > 0
146
+ : true
147
+
148
+ if (skipUnchanged && !hasDirtyFields) {
149
+ console.log(
150
+ `[useFormGql ${getOperationName(document)}] skipped submission, no dirty fields`,
151
+ )
152
+ await onValid(formValues, event)
153
+ return
154
+ }
155
+
115
156
  // Combine defaults with the formValues and encode
116
157
  submittedVariables.current = undefined
117
- let variables = experimental_useV2 ? formValues : encode({ ...defaultValues, ...formValues })
158
+ let variables = !deprecated_useV1 ? formValues : encode({ ...defaultValues, ...formValues })
118
159
 
119
160
  // Wait for the onBeforeSubmit to complete
120
- const res = await beforeSubmit(variables)
121
- if (res === false) return
122
- variables = res
123
-
124
- // if (variables === false) onInvalid?.(formValues, event)
161
+ const [onBeforeSubmitResult, onBeforeSubmitError] = await beforeSubmit(variables)
162
+ if (onBeforeSubmitError) {
163
+ form.setError('root', { message: onBeforeSubmitError.message })
164
+ return
165
+ }
166
+ if (onBeforeSubmitResult === false) return
167
+ variables = onBeforeSubmitResult
125
168
 
126
169
  submittedVariables.current = variables
127
- if (loading && experimental_useV2) controllerRef.current?.abort()
170
+ if (!deprecated_useV1 && loading) controllerRef.current?.abort()
128
171
  controllerRef.current = new window.AbortController()
129
172
  const result = await execute({
130
173
  variables,
131
174
  context: { fetchOptions: { signal: controllerRef.current.signal } },
132
175
  })
133
176
 
134
- if (result.data) await complete(result, variables)
177
+ const [, onCompleteError] = await complete(result, variables)
178
+ if (onCompleteError) {
179
+ form.setError('root', { message: onCompleteError.message })
180
+ return
181
+ }
135
182
 
136
- // Reset the state of the form if it is unmodified afterwards
137
- if (typeof diff(form.getValues(), formValues) === 'undefined' && !experimental_useV2)
183
+ if (deprecated_useV1 && typeof diff(form.getValues(), formValues) === 'undefined')
184
+ // Reset the state of the form if it is unmodified afterwards
138
185
  form.reset(formValues)
139
186
 
140
187
  await onValid(formValues, event)
@@ -0,0 +1,25 @@
1
+ export const tryAsync =
2
+ <R, Args extends unknown[]>(
3
+ fn: (...args: Args) => Promise<R> | R,
4
+ ): ((...args: Args) => Promise<[R, undefined] | [undefined, Error]>) =>
5
+ async (...args: Args) => {
6
+ try {
7
+ return [await fn(...args), undefined]
8
+ } catch (e) {
9
+ console.error(e)
10
+ return [undefined, e as Error]
11
+ }
12
+ }
13
+
14
+ export const trySync =
15
+ <R, Args extends unknown[]>(
16
+ fn: (...args: Args) => R,
17
+ ): ((...args: Args) => [R, undefined] | [undefined, Error]) =>
18
+ (...args: Args) => {
19
+ try {
20
+ return [fn(...args), undefined]
21
+ } catch (e) {
22
+ console.error(e)
23
+ return [undefined, e as Error]
24
+ }
25
+ }