@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 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,6 @@
1
+ import React from 'react'
2
+ import { ComposedFormContext } from './types'
3
+
4
+ export const composedFormContext = React.createContext(
5
+ (undefined as unknown) as ComposedFormContext,
6
+ )
@@ -0,0 +1,8 @@
1
+ export { default as ComposedForm } from './ComposedForm'
2
+ export * from './ComposedForm'
3
+
4
+ export { default as ComposedSubmit } from './ComposedSubmit'
5
+ export * from './ComposedSubmit'
6
+
7
+ export * from './types'
8
+ export * from './useFormCompose'
@@ -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
+ }