@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 +12 -0
- package/index.ts +6 -5
- package/package.json +4 -4
- package/src/ComposedForm/ComposedSubmit.tsx +12 -1
- package/src/ComposedForm/types.ts +2 -1
- package/src/useFormGql.tsx +66 -19
- package/src/utils/tryTuple.ts +25 -0
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/
|
|
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.
|
|
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.
|
|
20
|
-
"@graphcommerce/prettier-config-pwa": "^9.0.0-canary.
|
|
21
|
-
"@graphcommerce/typescript-config-pwa": "^9.0.0-canary.
|
|
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
|
|
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 = {
|
package/src/useFormGql.tsx
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
158
|
+
let variables = !deprecated_useV1 ? formValues : encode({ ...defaultValues, ...formValues })
|
|
118
159
|
|
|
119
160
|
// Wait for the onBeforeSubmit to complete
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 (
|
|
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
|
-
|
|
177
|
+
const [, onCompleteError] = await complete(result, variables)
|
|
178
|
+
if (onCompleteError) {
|
|
179
|
+
form.setError('root', { message: onCompleteError.message })
|
|
180
|
+
return
|
|
181
|
+
}
|
|
135
182
|
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|