Skip to content

Commit 4fb9c63

Browse files
authored
Merge pull request #63 from dbssman/feature/60-add-context-provider-as-composable
💉 Add context provider and tests
2 parents 712819e + d9e8d60 commit 4fb9c63

8 files changed

+234
-4
lines changed

docs/.vitepress/config.cts

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export default defineConfig({
105105
{ text: 'values', link: '/api/use-form-handler/values' },
106106
],
107107
},
108+
{ text: `useFormContext`, link: '/api/use-form-context' },
108109
{ text: `FormHandler`, link: '/api/form-handler' },
109110
],
110111
},

docs/api/use-form-context.md

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# useFormContext
2+
3+
`useFormContext` is a composable to access all the exposed functionalities of `useFormHandler` in any descendant of the handlers subtree is used. By this we avoid drilling the things we need down.
4+
5+
## How it works
6+
7+
`useFormContext` makes use of the [Vue 3 Provide/Inject](https://vuejs.org/guide/components/provide-inject.html) feature to directly provide the exposed features of `useFormHandler` to all the subtree.
8+
9+
As you can imagine, `useFormContext` and `useFormHandler` share the same return, nevertheless, the ancestor who consumes useFormHandler will rule things like `initialValues`, `validationMode`....
10+
11+
## Example:
12+
13+
```vue
14+
<!-- Parent.vue component -->
15+
<template>
16+
<form @submit.prevent="handleSubmit(successFn)">
17+
<input v-bind="register('firstName')" />
18+
<input v-bind="register('lastName')" />
19+
<Child></Child>
20+
<button type="submit">Submit</button>
21+
</form>
22+
</template>
23+
<script setup lang="ts">
24+
import { useFormHandler } from 'vue-form-handler'
25+
import Child from './Child.vue'
26+
27+
const { handleSubmit, register } = useFormHandler();
28+
const successFn = (form: Record<string, any>) => {
29+
console.log({ form })
30+
}
31+
</script>
32+
```
33+
34+
```vue
35+
<!-- Child.vue component -->
36+
<template>
37+
<input v-bind="register('anotherField')" />
38+
<GrandChild></GrandChild>
39+
</template>
40+
<script setup lang="ts">
41+
import { useFormContext } from 'vue-form-handler'
42+
import GrandChild from './GrandChild.vue'
43+
44+
const { register } = useFormContext()
45+
</script>
46+
```
47+
48+
```vue
49+
<!-- GrandChild.vue component -->
50+
<template>
51+
<input v-bind="register('anotherField2')" />
52+
</template>
53+
<script setup lang="ts">
54+
import { useFormContext } from 'vue-form-handler'
55+
56+
const { register } = useFormContext()
57+
</script>
58+
```
59+
60+
Feel free to play with it, you can also combine `register` and `build` approaches for the same form, within the same and in different files
61+
62+
::: warning
63+
Be aware that for a basic and usual functionality, We provide with a default key, If you have more than one `useFormHandler` usage in the same tree, the `injection keys` will collide, so you'll need to pass a specific one to `useFormHandler` and then to its consequent consumer, i.e:
64+
65+
:::details
66+
```vue
67+
<!-- Parent.vue component -->
68+
<template>
69+
<div>
70+
<form @submit.prevent="handleSubmit(successFn)">
71+
<input v-bind="register('firstName')" />
72+
<input v-bind="register('lastName')" />
73+
<Child></Child>
74+
<button type="submit">Submit</button>
75+
</form>
76+
<form @submit.prevent="handleSubmit2(successFn)">
77+
<input v-bind="register2('firstName')" />
78+
<input v-bind="register2('lastName')" />
79+
<AnotherChild></AnotherChild>
80+
<button type="submit">Submit</button>
81+
</form>
82+
</div>
83+
</template>
84+
<script setup lang="ts">
85+
import { useFormHandler } from 'vue-form-handler'
86+
import Child from './Child.vue'
87+
import AnotherChild from './AnotherChild.vue'
88+
89+
const { handleSubmit, register } = useFormHandler({ injectionKey: 'form1' });
90+
const { handleSubmit: handleSubmit2, register: register2 } = useFormHandler({ injectionKey: 'form2' });
91+
const successFn = (form: Record<string, any>) => {
92+
console.log({ form })
93+
}
94+
</script>
95+
```
96+
97+
```vue
98+
<!-- Child.vue component -->
99+
<template>
100+
<input v-bind="register('anotherField')" />
101+
</template>
102+
<script setup lang="ts">
103+
import { useFormContext } from 'vue-form-handler'
104+
105+
const { register } = useFormContext('form1')
106+
</script>
107+
```
108+
109+
```vue
110+
<!-- AnotherChild.vue component -->
111+
<template>
112+
<input v-bind="register('anotherField')" />
113+
</template>
114+
<script setup lang="ts">
115+
import { useFormContext } from 'vue-form-handler'
116+
117+
const { register } = useFormContext('form2')
118+
</script>
119+
```
120+
:::
121+
122+
::: tip
123+
Please refer to [Working with Symbol Keys](https://vuejs.org/guide/components/provide-inject.html#working-with-symbol-keys) for a quick read and understanding how provide/inject is intended to be used, and a correct way of defining your keys, as passing plain strings might not be the best approach, but rather using `Symbol`
124+
:::

src/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export const BaseInputEmits: BaseControlEmits = [
2222
'blur',
2323
'clear',
2424
]
25+
26+
export const defaultInjectionKey = Symbol('formHandler')

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './FormHandler'
22
export * from './useFormHandler'
3+
export * from './useFormContext'
34
export * from './types'
45
export * from './constants'

src/test/context.test.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { defineComponent } from '@vue/runtime-core'
2+
import { mount } from '@vue/test-utils'
3+
import { expect, it, describe } from 'vitest'
4+
import {
5+
useFormContext,
6+
useFormHandler,
7+
FormHandler,
8+
InjectionKey,
9+
} from '../index'
10+
11+
describe('useFormContext', () => {
12+
const registerComponents = (injectionKey?: InjectionKey) => {
13+
const Child = defineComponent({
14+
template: `<template>
15+
{{props}}
16+
</template>`,
17+
setup() {
18+
const props = useFormContext(injectionKey)
19+
return {
20+
props,
21+
}
22+
},
23+
})
24+
const Parent = defineComponent({
25+
template: `<template>
26+
<Child></Child>
27+
</template>`,
28+
components: { Child },
29+
setup() {
30+
useFormHandler({ injectionKey })
31+
},
32+
})
33+
34+
return Parent
35+
}
36+
it('should provide values and formState', () => {
37+
expect(FormHandler).toBeTruthy()
38+
const Parent = registerComponents()
39+
const wrapper = mount(Parent, {
40+
slots: {
41+
default: `<template #default="props">
42+
{{props}}
43+
</template>
44+
`,
45+
},
46+
})
47+
expect(wrapper.html()).toContain('values')
48+
expect(wrapper.html()).toContain('formState')
49+
})
50+
it('should work with string injection keys', () => {
51+
expect(FormHandler).toBeTruthy()
52+
const Parent = registerComponents('test')
53+
const wrapper = mount(Parent, {
54+
slots: {
55+
default: `<template #default="props">
56+
{{props}}
57+
</template>
58+
`,
59+
},
60+
})
61+
expect(wrapper.html()).toContain('values')
62+
expect(wrapper.html()).toContain('formState')
63+
})
64+
it('should work with Symbol injection keys', () => {
65+
expect(FormHandler).toBeTruthy()
66+
const Parent = registerComponents(Symbol('test'))
67+
const wrapper = mount(Parent, {
68+
slots: {
69+
default: `<template #default="props">
70+
{{props}}
71+
</template>
72+
`,
73+
},
74+
})
75+
expect(wrapper.html()).toContain('values')
76+
expect(wrapper.html()).toContain('formState')
77+
})
78+
})

src/types/formHandler.ts

+5
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export type FormValidation = (
127127
values: Record<string, any>
128128
) => Promise<boolean> | boolean
129129

130+
export type InjectionKey = string | Symbol
131+
130132
export interface FormHandlerParams {
131133
/** Values to initialize the form */
132134
initialValues?:
@@ -142,6 +144,9 @@ export interface FormHandlerParams {
142144

143145
/** Validation behavior options */
144146
validationMode?: 'onChange' | 'onBlur' | 'onSubmit' | 'always'
147+
148+
/** Injection key to override the default */
149+
injectionKey?: InjectionKey
145150
}
146151
export interface FormHandlerReturn {
147152
/** Current form state */

src/useFormContext.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { inject } from '@vue/runtime-core'
2+
import { defaultInjectionKey } from './constants'
3+
import { FormHandlerReturn, InjectionKey } from './types'
4+
5+
export const useFormContext = (
6+
key: InjectionKey = defaultInjectionKey
7+
): FormHandlerReturn => inject(key) as FormHandlerReturn

src/useFormHandler.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Build } from './types/formHandler'
1+
import { Build, FormHandlerReturn } from './types/formHandler'
22
import { NativeValidations } from './types/validations'
3-
import { DEFAULT_FIELD_VALUE } from './constants'
3+
import { DEFAULT_FIELD_VALUE, defaultInjectionKey } from './constants'
44
import {
55
ModifiedValues,
66
TriggerValidation,
@@ -27,7 +27,14 @@ import {
2727
RegisterOptions,
2828
RegisterReturn,
2929
} from './types'
30-
import { computed, reactive, readonly, unref, watch } from '@vue/runtime-core'
30+
import {
31+
computed,
32+
provide,
33+
reactive,
34+
readonly,
35+
unref,
36+
watch,
37+
} from '@vue/runtime-core'
3138
import { isEqual } from 'lodash-es'
3239
import {
3340
getNativeFieldValue,
@@ -53,6 +60,7 @@ export const useFormHandler: UseFormHandler = ({
5360
interceptor,
5461
validate,
5562
validationMode = 'onChange',
63+
injectionKey = defaultInjectionKey,
5664
} = {}) => {
5765
const values: Record<string, any> = reactive({ ...unref(initialValues) })
5866
const formState = reactive<FormState>({ ...initialState() })
@@ -332,7 +340,7 @@ export const useFormHandler: UseFormHandler = ({
332340
{ deep: true }
333341
)
334342

335-
return {
343+
const toExpose: FormHandlerReturn = {
336344
clearError,
337345
clearField,
338346
formState: readonly(formState),
@@ -348,4 +356,8 @@ export const useFormHandler: UseFormHandler = ({
348356
unregister,
349357
values: readonly(values),
350358
}
359+
360+
provide(injectionKey, toExpose)
361+
362+
return toExpose
351363
}

0 commit comments

Comments
 (0)