@graphcommerce/react-hook-form 2.102.1
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 +99 -0
- package/README.md +157 -0
- package/__mocks__/TestShippingAddressForm.gql.ts +14 -0
- package/__mocks__/TestShippingAddressForm.graphql +16 -0
- package/__tests__/useFormGqlMutation.tsx +41 -0
- package/__tests__/useGqlDocumentHandler.tsx +50 -0
- package/index.ts +11 -0
- package/package.json +34 -0
- package/src/ComposedForm/ComposedForm.tsx +32 -0
- package/src/ComposedForm/ComposedSubmit.tsx +84 -0
- package/src/ComposedForm/context.ts +6 -0
- package/src/ComposedForm/index.ts +8 -0
- package/src/ComposedForm/reducer.ts +78 -0
- package/src/ComposedForm/types.ts +85 -0
- package/src/ComposedForm/useFormCompose.ts +36 -0
- package/src/diff.ts +39 -0
- package/src/useFormAutoSubmit.tsx +82 -0
- package/src/useFormGql.tsx +73 -0
- package/src/useFormGqlMutation.tsx +39 -0
- package/src/useFormGqlQuery.tsx +23 -0
- package/src/useFormMuiRegister.tsx +22 -0
- package/src/useFormPersist.tsx +71 -0
- package/src/useFormValidFields.tsx +23 -0
- package/src/useGqlDocumentHandler.tsx +151 -0
- package/src/useLazyQueryPromise.tsx +41 -0
- package/src/validationPatterns.tsx +4 -0
- package/tsconfig.json +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [2.102.1](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/react-hook-form@2.102.0...@graphcommerce/react-hook-form@2.102.1) (2021-09-27)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @graphcommerce/react-hook-form
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 2.102.0 (2021-09-27)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* disable isValid form checkout on submit ([b20110d](https://github.com/ho-nl/m2-pwa/commit/b20110d93327ff2678296acc47b1075b4fb3a85a))
|
|
20
|
+
* ignore md files from triggering version updates ([4f98392](https://github.com/ho-nl/m2-pwa/commit/4f9839250b3a32d3070da5290e5efcc5e2243fba))
|
|
21
|
+
* input checkmarks ([279c1c1](https://github.com/ho-nl/m2-pwa/commit/279c1c112ada46fdea102024298e8293d1a23293))
|
|
22
|
+
* make sure ComposedForm actually submits correctly ([c6499d9](https://github.com/ho-nl/m2-pwa/commit/c6499d9d36f874cd65b310cbf7f63f5a88fa86cd))
|
|
23
|
+
* make sure the checkout address fields are working as expected ([e88aae9](https://github.com/ho-nl/m2-pwa/commit/e88aae9afa3c60457b8e8c87ba52e8ae2dec4a3e))
|
|
24
|
+
* make sure useFormGqlQuery uses the new useLazyQueryPromise ([f0cf831](https://github.com/ho-nl/m2-pwa/commit/f0cf83191dbbc2da222682b053db8b8c374add69))
|
|
25
|
+
* **react-hook-form:** assertFormGqlOperation ([ce09fa5](https://github.com/ho-nl/m2-pwa/commit/ce09fa50f73f6d06b2caa15b1223ba7470a7ea96))
|
|
26
|
+
* **react-hook-form:** form autosubmit is stuck in a loop if the request fails ([c74e3e0](https://github.com/ho-nl/m2-pwa/commit/c74e3e0cbf146887c3ef5447dcbba46746971e2a))
|
|
27
|
+
* **react-hook-form:** handle ComposedForm network errors ([e028ae0](https://github.com/ho-nl/m2-pwa/commit/e028ae06f49fea5d4e4dbdf58f803b365c902404))
|
|
28
|
+
* **react-hook-form:** make sure we don’t overuse useFormPersist ([b28efde](https://github.com/ho-nl/m2-pwa/commit/b28efde79dad1fb9bf9e8d7f9cc5cf32648acdf1))
|
|
29
|
+
* **react-hook-form:** not not always submit ComposedForm ([642833f](https://github.com/ho-nl/m2-pwa/commit/642833fe8b311b20db2ccdd57f0492b8429c0e81))
|
|
30
|
+
* **react-hook-form:** Object(…) is not a function ([ebc32e4](https://github.com/ho-nl/m2-pwa/commit/ebc32e402c1db185785f1e29c21a705d38a6743d))
|
|
31
|
+
* **react-hook-form:** solve issue where form would oversubmit/not-submit ([89f0619](https://github.com/ho-nl/m2-pwa/commit/89f0619051228c08a61aafeeddb67b95e99727ff))
|
|
32
|
+
* **react-hook-form:** the wrong form would submit if the component didn’t rerender ([32d4cf1](https://github.com/ho-nl/m2-pwa/commit/32d4cf13ffc2967cc41e50a14ad872d289d4eb43))
|
|
33
|
+
* **react-hook-form:** validate if the previous request succeeds before moving on to the next request ([1985d09](https://github.com/ho-nl/m2-pwa/commit/1985d0938cd509532fa3b6bc801a3399c2baae09))
|
|
34
|
+
* remove cyclic dependencies ([8a59389](https://github.com/ho-nl/m2-pwa/commit/8a5938943a97634cce57c68bb369c6e77e7a0288))
|
|
35
|
+
* show form checkmarks when field is valid ([7df8cad](https://github.com/ho-nl/m2-pwa/commit/7df8cadd5292c7d8a1d1e4c981d51adf7b5b8119))
|
|
36
|
+
* use form auto submit should submit with prefilled data ([9957ef6](https://github.com/ho-nl/m2-pwa/commit/9957ef67ee39e30873f528ffae4da7c0dcfbb6fa))
|
|
37
|
+
* useformautosubmit initial submit ([a06cb60](https://github.com/ho-nl/m2-pwa/commit/a06cb60996f83788a95bcd3995407539b2acfd46))
|
|
38
|
+
* useFormAutoSubmit modes ([9180bf2](https://github.com/ho-nl/m2-pwa/commit/9180bf21a140f5741078007c42972ded433c277c))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
|
|
43
|
+
* created stacked-pages package ([d86008e](https://github.com/ho-nl/m2-pwa/commit/d86008ee659ccb25b194a41d624b394a1ddbd088))
|
|
44
|
+
* implemented checkmo payment method ([18525b2](https://github.com/ho-nl/m2-pwa/commit/18525b2f4efe9bd0eea12a7a992d284f341e0c68))
|
|
45
|
+
* implemented purchase order ([3a40033](https://github.com/ho-nl/m2-pwa/commit/3a40033cd4d6712a17bb9c41a8841ebf7aa2f025))
|
|
46
|
+
* introduced SheetShell as a shared layout component ([eb64f28](https://github.com/ho-nl/m2-pwa/commit/eb64f28fd05b69efbf14fa850c70b0f1da5c4237))
|
|
47
|
+
* next.js 11 ([7d61407](https://github.com/ho-nl/m2-pwa/commit/7d614075a778f488045034f74be4f75b93f63c43))
|
|
48
|
+
* **playwright:** added new playwright package to enable browser testing ([6f49ec7](https://github.com/ho-nl/m2-pwa/commit/6f49ec7595563775b96ebf21c27e39da1282e8d9))
|
|
49
|
+
* **react-hook-form:** added buttonState to ComposedSubmit ([57e77c2](https://github.com/ho-nl/m2-pwa/commit/57e77c29f17720f7f3ee3b63be82779c0e5d8714))
|
|
50
|
+
* **react-hook-form:** added ComposedForm component to handle the submission of multiple forms ([1172ec5](https://github.com/ho-nl/m2-pwa/commit/1172ec5abcb0e1b72bb362b977bf0c22997bac9a))
|
|
51
|
+
* **react-hook-form:** pass useFormGql operation options ([ddb6f75](https://github.com/ho-nl/m2-pwa/commit/ddb6f750432c0a6ed8468ae08e522a774c261f8f))
|
|
52
|
+
* **react-hook-form:** updated readme ([aede77a](https://github.com/ho-nl/m2-pwa/commit/aede77ab6d30fe5ca47b9d08bbdadca9b371713c))
|
|
53
|
+
* renamed all packages to use [@graphcommerce](https://github.com/graphcommerce) instead of [@reachdigital](https://github.com/reachdigital) ([491e4ce](https://github.com/ho-nl/m2-pwa/commit/491e4cec9a2686472dac36b79f999257c0811ffe))
|
|
54
|
+
* search result page wip ([4ecaf34](https://github.com/ho-nl/m2-pwa/commit/4ecaf34deaa0ff6d24e03d72e74fd045bb7ee269))
|
|
55
|
+
* solve issue where the order couldn’t be submitted ([ec0d357](https://github.com/ho-nl/m2-pwa/commit/ec0d3579a1277976e2dc515f420996cf716f83a6))
|
|
56
|
+
* submit composed form sequentially ([890d839](https://github.com/ho-nl/m2-pwa/commit/890d8393d635c3777aa17cfa8d4dafc13c2e6cdc))
|
|
57
|
+
* upgrade to node 14 ([d079a75](https://github.com/ho-nl/m2-pwa/commit/d079a751e9bfd8dc7f5009d2c9f31c336a0c96ab))
|
|
58
|
+
* useFormMutationCart and simpler imports ([012f090](https://github.com/ho-nl/m2-pwa/commit/012f090e8f54d09f35d393c61ad1e2319f5a90ff))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Reverts
|
|
62
|
+
|
|
63
|
+
* Revert "chore: upgrade @apollo/client" ([55ff24e](https://github.com/ho-nl/m2-pwa/commit/55ff24ede0e56c85b8095edadadd1ec5e0b1b8d2))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Change Log
|
|
70
|
+
|
|
71
|
+
All notable changes to this project will be documented in this file. See
|
|
72
|
+
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
73
|
+
|
|
74
|
+
## [2.101.4](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/react-hook-form@2.101.3...@graphcommerce/react-hook-form@2.101.4) (2021-08-09)
|
|
75
|
+
|
|
76
|
+
### Reverts
|
|
77
|
+
|
|
78
|
+
- Revert "chore: upgrade @apollo/client"
|
|
79
|
+
([55ff24e](https://github.com/ho-nl/m2-pwa/commit/55ff24ede0e56c85b8095edadadd1ec5e0b1b8d2))
|
|
80
|
+
|
|
81
|
+
## [2.101.3](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/react-hook-form@2.101.2...@graphcommerce/react-hook-form@2.101.3) (2021-07-29)
|
|
82
|
+
|
|
83
|
+
### Bug Fixes
|
|
84
|
+
|
|
85
|
+
- **react-hook-form:** validate if the previous request succeeds before moving
|
|
86
|
+
on to the next request
|
|
87
|
+
([1985d09](https://github.com/ho-nl/m2-pwa/commit/1985d0938cd509532fa3b6bc801a3399c2baae09))
|
|
88
|
+
|
|
89
|
+
# [2.101.0](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/react-hook-form@2.100.10...@graphcommerce/react-hook-form@2.101.0) (2021-07-26)
|
|
90
|
+
|
|
91
|
+
### Bug Fixes
|
|
92
|
+
|
|
93
|
+
- ignore md files from triggering version updates
|
|
94
|
+
([4f98392](https://github.com/ho-nl/m2-pwa/commit/4f9839250b3a32d3070da5290e5efcc5e2243fba))
|
|
95
|
+
|
|
96
|
+
### Features
|
|
97
|
+
|
|
98
|
+
- **playwright:** added new playwright package to enable browser testing
|
|
99
|
+
([6f49ec7](https://github.com/ho-nl/m2-pwa/commit/6f49ec7595563775b96ebf21c27e39da1282e8d9))
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Form
|
|
2
|
+
|
|
3
|
+
The Form component is an extension of the (React Hook
|
|
4
|
+
Form)(https://react-hook-form.com/) package which adds new hooks.
|
|
5
|
+
|
|
6
|
+
## `useFormGqlMutation`
|
|
7
|
+
|
|
8
|
+
Simple example:
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { useFormGqlMutation } from '@graphcommerce/react-hook-form'
|
|
12
|
+
|
|
13
|
+
const mutation = gql`
|
|
14
|
+
mutation ApplyCouponToCart($cartId: String!, $couponCode: String!) {
|
|
15
|
+
applyCouponToCart(input: { cart_id: $cartId, coupon_code: $couponCode }) {
|
|
16
|
+
cart {
|
|
17
|
+
id
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
export default function MyComponent() {
|
|
24
|
+
const form = useFormGqlMutation(mutation, {
|
|
25
|
+
defaultValues: { cartId: cartQuery?.cart?.id },
|
|
26
|
+
})
|
|
27
|
+
const { errors, handleSubmit, register, formState, required, error } = form
|
|
28
|
+
|
|
29
|
+
// We don't need to provide an actual handler as useFormGqlMutation already adds that.
|
|
30
|
+
const submit = handleSubmit(() => {})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<form onSubmit={submit} noValidate>
|
|
34
|
+
<input
|
|
35
|
+
type='text'
|
|
36
|
+
{...register('couponCode', { required: required.couponCode })}
|
|
37
|
+
disabled={formState.isSubmitting}
|
|
38
|
+
/>
|
|
39
|
+
{errors.couponCode?.message || error?.message}
|
|
40
|
+
<button type='submit'>submit</button>
|
|
41
|
+
</form>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `useFormGqlQuery`
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useFormGqlQuery } from '@graphcommerce/react-hook-form'
|
|
50
|
+
|
|
51
|
+
const query = gql`
|
|
52
|
+
query IsEmailAvailable($email: String!) {
|
|
53
|
+
isEmailAvailable(email: $email) {
|
|
54
|
+
is_email_available
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
export default function MyComponent() {
|
|
60
|
+
const form = useFormGqlQuery(query, {})
|
|
61
|
+
const { errors, handleSubmit, register, formState, required, error } = form
|
|
62
|
+
|
|
63
|
+
// We don't need to provide an actual handler as useFormGqlQuery already adds that.
|
|
64
|
+
const submit = handleSubmit(() => {})
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<form onSubmit={submit} noValidate>
|
|
68
|
+
<input
|
|
69
|
+
type='text'
|
|
70
|
+
{...register('couponCode', { required: required.couponCode })}
|
|
71
|
+
disabled={formState.isSubmitting}
|
|
72
|
+
/>
|
|
73
|
+
{errors.couponCode?.message || error?.message}
|
|
74
|
+
<button type='submit'>submit</button>
|
|
75
|
+
</form>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## `useFormAutoSubmit`
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { useFormAutoSubmit } from '@graphcommerce/react-hook-form'
|
|
84
|
+
|
|
85
|
+
export default function MyAutoSubmitForm() {
|
|
86
|
+
// Regular useForm hook, but you can also use useFormGqlMutation
|
|
87
|
+
const form = useForm()
|
|
88
|
+
const { errors, handleSubmit, register, formState, required } = form
|
|
89
|
+
|
|
90
|
+
const submit = handleSubmit(() => {
|
|
91
|
+
console.log('submitted')
|
|
92
|
+
})
|
|
93
|
+
const autoSubmitting = useFormAutoSubmit({
|
|
94
|
+
form,
|
|
95
|
+
submit,
|
|
96
|
+
fields: ['couponCode'], //optional, default: all fields
|
|
97
|
+
wait: 1200, // optional, default: 500ms
|
|
98
|
+
})
|
|
99
|
+
const disableFields = formState.isSubmitting && !autoSubmitting
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<form onSubmit={submit} noValidate>
|
|
103
|
+
<input
|
|
104
|
+
type='text'
|
|
105
|
+
{...register('couponCode', { required: required.couponCode })}
|
|
106
|
+
disabled={formState.isSubmitting}
|
|
107
|
+
/>
|
|
108
|
+
{errors.couponCode?.message}
|
|
109
|
+
</form>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `useFormPersist`
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { useFormAutoSubmit } from '@graphcommerce/react-hook-form'
|
|
118
|
+
|
|
119
|
+
export default function MyAutoSubmitForm() {
|
|
120
|
+
// Regular useForm hook, but you can also use useFormGqlMutation
|
|
121
|
+
const form = useForm()
|
|
122
|
+
const { errors, handleSubmit, register, formState, required } = form
|
|
123
|
+
|
|
124
|
+
const submit = handleSubmit(() => {
|
|
125
|
+
console.log('submitted')
|
|
126
|
+
})
|
|
127
|
+
const autoSubmitting = useFormPersist({ form, name: 'MyForm' })
|
|
128
|
+
const disableFields = formState.isSubmitting && !autoSubmitting
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<form onSubmit={submit} noValidate>
|
|
132
|
+
<input
|
|
133
|
+
type='text'
|
|
134
|
+
{...register('couponCode', { required: required.couponCode })}
|
|
135
|
+
disabled={disableFields}
|
|
136
|
+
/>
|
|
137
|
+
{errors.couponCode?.message}
|
|
138
|
+
</form>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## FAQ
|
|
144
|
+
|
|
145
|
+
### `Why is my useForm hook not submitting anything?`
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const form = useForm() // INCORRECT
|
|
149
|
+
|
|
150
|
+
const form useForm({ // CORRECT
|
|
151
|
+
mode: 'onSubmit',
|
|
152
|
+
defaultValues: {
|
|
153
|
+
yourFieldName: 'default value',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import * as Types from '@graphcommerce/graphql';
|
|
3
|
+
|
|
4
|
+
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
|
5
|
+
|
|
6
|
+
export const TestShippingAddressFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestShippingAddressForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cartId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"address"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CartAddressInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"customerNote"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"StringValue","value":"joi","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setShippingAddressesOnCart"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"cart_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cartId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"shipping_addresses"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"address"},"value":{"kind":"Variable","name":{"kind":"Name","value":"address"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"customer_notes"},"value":{"kind":"Variable","name":{"kind":"Name","value":"customerNote"}}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cart"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<TestShippingAddressFormMutation, TestShippingAddressFormMutationVariables>;
|
|
7
|
+
export type TestShippingAddressFormMutationVariables = Types.Exact<{
|
|
8
|
+
cartId: Types.Scalars['String'];
|
|
9
|
+
address: Types.CartAddressInput;
|
|
10
|
+
customerNote?: Types.Maybe<Types.Scalars['String']>;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export type TestShippingAddressFormMutation = { setShippingAddressesOnCart?: Types.Maybe<{ cart: { id: string } }> };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
mutation TestShippingAddressForm(
|
|
2
|
+
$cartId: String!
|
|
3
|
+
$address: CartAddressInput!
|
|
4
|
+
$customerNote: String = "joi"
|
|
5
|
+
) {
|
|
6
|
+
setShippingAddressesOnCart(
|
|
7
|
+
input: {
|
|
8
|
+
cart_id: $cartId
|
|
9
|
+
shipping_addresses: [{ address: $address, customer_notes: $customerNote }]
|
|
10
|
+
}
|
|
11
|
+
) {
|
|
12
|
+
cart {
|
|
13
|
+
id
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
+
import { TestShippingAddressFormDocument } from '../__mocks__/TestShippingAddressForm.gql'
|
|
3
|
+
import { useFormGqlMutation } from '../src/useFormGqlMutation'
|
|
4
|
+
|
|
5
|
+
describe('useFormGqlMutation', () => {
|
|
6
|
+
const { register, required, defaultVariables } = useFormGqlMutation(
|
|
7
|
+
TestShippingAddressFormDocument,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
it('can register stuff', () => {
|
|
11
|
+
register('address.telephone')
|
|
12
|
+
register('customerNote')
|
|
13
|
+
register('address.street.0')
|
|
14
|
+
|
|
15
|
+
// @ts-expect-error should not be posssible
|
|
16
|
+
register('address.street.hoi')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const address = {
|
|
20
|
+
cartId: '12',
|
|
21
|
+
customerNote: 'hoi',
|
|
22
|
+
address: {
|
|
23
|
+
firstname: 'Paul',
|
|
24
|
+
lastname: 'Hachmang',
|
|
25
|
+
company: 'Reach Digital',
|
|
26
|
+
country_code: 'NL',
|
|
27
|
+
street: ['Noordplein 85', '3e etage'],
|
|
28
|
+
city: 'Roelofarendsveen',
|
|
29
|
+
telephone: '0654716972',
|
|
30
|
+
postcode: '2371DJ',
|
|
31
|
+
save_in_address_book: 'true',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
it('extracts required fields correctly', () => {
|
|
36
|
+
expect(required).toEqual({ address: true, cartId: true, customerNote: false })
|
|
37
|
+
})
|
|
38
|
+
it('extracts defaults correctly', () => {
|
|
39
|
+
expect(defaultVariables).toEqual({ customerNote: 'joi' })
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { TestShippingAddressFormDocument } from '../__mocks__/TestShippingAddressForm.gql'
|
|
2
|
+
import { handlerFactory } from '../src/useGqlDocumentHandler'
|
|
3
|
+
|
|
4
|
+
describe('useGqlDocumentHandler', () => {
|
|
5
|
+
const { required, defaultVariables: defaults, encode } = handlerFactory(
|
|
6
|
+
TestShippingAddressFormDocument,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const address = {
|
|
10
|
+
cartId: '12',
|
|
11
|
+
customerNote: 'hoi',
|
|
12
|
+
address: {
|
|
13
|
+
firstname: 'Paul',
|
|
14
|
+
lastname: 'Hachmang',
|
|
15
|
+
company: 'Reach Digital',
|
|
16
|
+
country_code: 'NL',
|
|
17
|
+
street: ['Noordplein 85', '3e etage'],
|
|
18
|
+
city: 'Roelofarendsveen',
|
|
19
|
+
telephone: '0654716972',
|
|
20
|
+
postcode: '2371DJ',
|
|
21
|
+
save_in_address_book: 'true',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
it('extracts required fields correctly', () => {
|
|
26
|
+
expect(required).toEqual({ address: true, cartId: true, customerNote: false })
|
|
27
|
+
})
|
|
28
|
+
it('extracts defaults correctly', () => {
|
|
29
|
+
expect(defaults).toEqual({ customerNote: 'joi' })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('encodes objects correctly', () => {
|
|
33
|
+
const result = encode(address)
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
cartId: '12',
|
|
36
|
+
customerNote: 'hoi',
|
|
37
|
+
address: {
|
|
38
|
+
firstname: 'Paul',
|
|
39
|
+
lastname: 'Hachmang',
|
|
40
|
+
company: 'Reach Digital',
|
|
41
|
+
country_code: 'NL',
|
|
42
|
+
street: ['Noordplein 85', '3e etage'],
|
|
43
|
+
city: 'Roelofarendsveen',
|
|
44
|
+
telephone: '0654716972',
|
|
45
|
+
postcode: '2371DJ',
|
|
46
|
+
save_in_address_book: true,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from 'react-hook-form'
|
|
2
|
+
export * from './src/useFormGqlMutation'
|
|
3
|
+
export * from './src/useFormGqlQuery'
|
|
4
|
+
export * from './src/useFormAutoSubmit'
|
|
5
|
+
export * from './src/useFormPersist'
|
|
6
|
+
export * from './src/useFormMuiRegister'
|
|
7
|
+
export * from './src/useFormValidFields'
|
|
8
|
+
export * from './src/useLazyQueryPromise'
|
|
9
|
+
export * from './src/useFormGql'
|
|
10
|
+
export * from './src/validationPatterns'
|
|
11
|
+
export * from './src/ComposedForm'
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphcommerce/react-hook-form",
|
|
3
|
+
"version": "2.102.1",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "14.x"
|
|
7
|
+
},
|
|
8
|
+
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
9
|
+
"browserslist": [
|
|
10
|
+
"extends @graphcommerce/browserslist-config-pwa"
|
|
11
|
+
],
|
|
12
|
+
"eslintConfig": {
|
|
13
|
+
"extends": "@graphcommerce/eslint-config-pwa",
|
|
14
|
+
"parserOptions": {
|
|
15
|
+
"project": "./tsconfig.json"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@graphcommerce/browserslist-config-pwa": "^3.0.1",
|
|
20
|
+
"@graphcommerce/eslint-config-pwa": "^3.0.1",
|
|
21
|
+
"@graphcommerce/prettier-config-pwa": "^3.0.1",
|
|
22
|
+
"@graphcommerce/typescript-config-pwa": "^3.0.1",
|
|
23
|
+
"@playwright/test": "^1.14.1"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@apollo/client": "^3.3.21",
|
|
27
|
+
"@graphql-typed-document-node/core": "^3.1.0",
|
|
28
|
+
"graphql": "^15.5.2",
|
|
29
|
+
"react": "^17.0.2",
|
|
30
|
+
"react-dom": "^17.0.2",
|
|
31
|
+
"react-hook-form": "^7.14.2",
|
|
32
|
+
"type-fest": "^2.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { useReducer } from 'react'
|
|
2
|
+
import { composedFormContext } from './context'
|
|
3
|
+
import { composedFormReducer } from './reducer'
|
|
4
|
+
|
|
5
|
+
export type ComposedFormProps = { children?: React.ReactNode }
|
|
6
|
+
|
|
7
|
+
export default function ComposedForm(props: ComposedFormProps) {
|
|
8
|
+
const { children } = props
|
|
9
|
+
|
|
10
|
+
const [state, dispatch] = useReducer(composedFormReducer, {
|
|
11
|
+
forms: {},
|
|
12
|
+
isCompleting: false,
|
|
13
|
+
buttonState: {
|
|
14
|
+
isSubmitting: false,
|
|
15
|
+
isSubmitted: false,
|
|
16
|
+
isSubmitSuccessful: false,
|
|
17
|
+
},
|
|
18
|
+
formState: {
|
|
19
|
+
isSubmitting: false,
|
|
20
|
+
isSubmitSuccessful: false,
|
|
21
|
+
isSubmitted: false,
|
|
22
|
+
isValid: false,
|
|
23
|
+
},
|
|
24
|
+
submitted: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<composedFormContext.Provider value={[state, dispatch]}>
|
|
29
|
+
{children}
|
|
30
|
+
</composedFormContext.Provider>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ApolloError } from '@apollo/client'
|
|
2
|
+
import React, { useContext, useEffect } from 'react'
|
|
3
|
+
import { isFormGqlOperation } from '../useFormGqlMutation'
|
|
4
|
+
import { composedFormContext } from './context'
|
|
5
|
+
import { ComposedSubmitRenderComponentProps } from './types'
|
|
6
|
+
|
|
7
|
+
export type ComposedSubmitProps = {
|
|
8
|
+
onSubmitSuccessful?: () => void
|
|
9
|
+
render: React.FC<ComposedSubmitRenderComponentProps>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mergeErrors(errors: ApolloError[]): ApolloError | undefined {
|
|
13
|
+
return new ApolloError({
|
|
14
|
+
errorMessage: 'Composed submit error',
|
|
15
|
+
networkError: errors.find((error) => error.networkError)?.networkError,
|
|
16
|
+
graphQLErrors: errors.map((error) => error.graphQLErrors ?? []).flat(1),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function ComposedSubmit(props: ComposedSubmitProps) {
|
|
21
|
+
const { render: Render, onSubmitSuccessful } = props
|
|
22
|
+
const [formContext, dispatch] = useContext(composedFormContext)
|
|
23
|
+
const { formState, buttonState, isCompleting, forms } = formContext
|
|
24
|
+
|
|
25
|
+
const formEntries = Object.entries(forms).sort((a, b) => a[1].step - b[1].step)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (isCompleting && !formState.isSubmitting) {
|
|
29
|
+
/**
|
|
30
|
+
* If we have forms that are invalid, we don't need to submit anything yet. We can trigger the
|
|
31
|
+
* submission of the invalid forms and highlight those forms.
|
|
32
|
+
*/
|
|
33
|
+
const isSubmitSuccessful = !formEntries.some(
|
|
34
|
+
([, f]) => Object.keys(f.form?.formState.errors ?? {}).length > 0,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
dispatch({ type: 'SUBMITTED', isSubmitSuccessful })
|
|
38
|
+
if (isSubmitSuccessful) onSubmitSuccessful?.()
|
|
39
|
+
}
|
|
40
|
+
}, [isCompleting, dispatch, formEntries, formState.isSubmitting, onSubmitSuccessful])
|
|
41
|
+
|
|
42
|
+
/** Callback to submit all forms */
|
|
43
|
+
const submitAll = async () => {
|
|
44
|
+
/**
|
|
45
|
+
* If we have forms that are have errors, we don't need to submit anything yet. We can trigger
|
|
46
|
+
* the submission of the invalid forms and highlight those forms.
|
|
47
|
+
*/
|
|
48
|
+
let formsToSubmit = formEntries.filter(
|
|
49
|
+
([, f]) => Object.keys(f.form?.formState.errors ?? {}).length > 0,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// We have no errors or invalid forms
|
|
53
|
+
if (!formsToSubmit.length) formsToSubmit = formEntries
|
|
54
|
+
|
|
55
|
+
dispatch({ type: 'SUBMIT' })
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
/**
|
|
59
|
+
* We're executing these steps all in sequence, since certain forms can depend on other forms
|
|
60
|
+
* in the backend.
|
|
61
|
+
*
|
|
62
|
+
* Todo: There might be a performance optimization by submitting multiple forms in parallel.
|
|
63
|
+
*/
|
|
64
|
+
let canSubmit = true
|
|
65
|
+
for (const [, { submit, form }] of formsToSubmit) {
|
|
66
|
+
// eslint-disable-next-line no-await-in-loop
|
|
67
|
+
if (canSubmit) await submit?.()
|
|
68
|
+
// eslint-disable-next-line no-await-in-loop
|
|
69
|
+
if (!canSubmit) await form?.trigger()
|
|
70
|
+
if (!form?.formState.isValid || (isFormGqlOperation(form) && form.error)) canSubmit = false
|
|
71
|
+
}
|
|
72
|
+
dispatch({ type: 'SUBMITTING' })
|
|
73
|
+
} catch (error) {
|
|
74
|
+
dispatch({ type: 'SUBMITTED', isSubmitSuccessful: false })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const errors: ApolloError[] = []
|
|
79
|
+
formEntries.forEach(([, { form }]) => {
|
|
80
|
+
if (form && isFormGqlOperation(form) && form.error) errors.push(form.error)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return <Render buttonState={buttonState} submit={submitAll} error={mergeErrors(errors)} />
|
|
84
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isFormGqlOperation } from '../useFormGqlMutation'
|
|
2
|
+
import { ComposedFormReducer, ComposedFormState } from './types'
|
|
3
|
+
|
|
4
|
+
function updateFormStateIfNecessary(state: ComposedFormState): ComposedFormState {
|
|
5
|
+
const formEntries = Object.entries(state.forms)
|
|
6
|
+
const formState = Object.entries(state.forms).map(
|
|
7
|
+
([, { form }]) =>
|
|
8
|
+
form?.formState ?? {
|
|
9
|
+
isSubmitting: false,
|
|
10
|
+
isSubmitSuccessful: false,
|
|
11
|
+
isSubmitted: false,
|
|
12
|
+
isValid: false,
|
|
13
|
+
},
|
|
14
|
+
)
|
|
15
|
+
const hasState = formState.length > 0
|
|
16
|
+
|
|
17
|
+
const isSubmitting = hasState && formState.some((fs) => fs.isSubmitting)
|
|
18
|
+
const isSubmitSuccessful =
|
|
19
|
+
hasState &&
|
|
20
|
+
formState.every((f) => f.isSubmitSuccessful) &&
|
|
21
|
+
formEntries.every(([, f]) => (f.form && isFormGqlOperation(f.form) ? !f.form.error : true))
|
|
22
|
+
const isSubmitted = hasState && formState.every((fs) => fs.isSubmitted)
|
|
23
|
+
const isValid = hasState ? formState.every((fs) => fs.isValid) : false
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
state.formState.isSubmitSuccessful !== isSubmitSuccessful ||
|
|
27
|
+
state.formState.isSubmitted !== isSubmitted ||
|
|
28
|
+
state.formState.isSubmitting !== isSubmitting ||
|
|
29
|
+
state.formState.isValid !== isValid
|
|
30
|
+
) {
|
|
31
|
+
return {
|
|
32
|
+
...state,
|
|
33
|
+
formState: { isSubmitting, isSubmitSuccessful, isSubmitted, isValid },
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return state
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const composedFormReducer: ComposedFormReducer = (state, action) => {
|
|
41
|
+
switch (action.type) {
|
|
42
|
+
case 'REGISTER':
|
|
43
|
+
return { ...state, forms: { ...state.forms, [action.key]: undefined } }
|
|
44
|
+
break
|
|
45
|
+
case 'UNREGISTER':
|
|
46
|
+
delete state.forms[action.key]
|
|
47
|
+
return { ...state }
|
|
48
|
+
break
|
|
49
|
+
case 'ASSIGN':
|
|
50
|
+
state.forms[action.key] = { ...state.forms[action.key], ...action }
|
|
51
|
+
break
|
|
52
|
+
case 'SUBMIT':
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
buttonState: { ...state.buttonState, isSubmitting: true },
|
|
56
|
+
}
|
|
57
|
+
case 'SUBMITTING':
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
isCompleting: true,
|
|
61
|
+
buttonState: { ...state.buttonState },
|
|
62
|
+
}
|
|
63
|
+
case 'SUBMITTED':
|
|
64
|
+
return {
|
|
65
|
+
...state,
|
|
66
|
+
isCompleting: false,
|
|
67
|
+
buttonState: {
|
|
68
|
+
isSubmitting: false,
|
|
69
|
+
isSubmitted: true,
|
|
70
|
+
isSubmitSuccessful: action.isSubmitSuccessful,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
case 'FORMSTATE':
|
|
74
|
+
return updateFormStateIfNecessary(state)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return state
|
|
78
|
+
}
|