Skip to content
This repository was archived by the owner on Apr 9, 2025. It is now read-only.

Commit 47a263a

Browse files
authored
Merge pull request #62 from dbssman/feature/55-add-composableutil-for-prop-binding-generation
🧱 Add build function and documentation
2 parents 67892dd + bf83d79 commit 47a263a

File tree

8 files changed

+156
-27
lines changed

8 files changed

+156
-27
lines changed

docs/.vitepress/config.cts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default defineConfig({
9292
link: '/api/use-form-handler/modified-values',
9393
},
9494
{ text: 'register', link: '/api/use-form-handler/register' },
95+
{ text: 'build', link: '/api/use-form-handler/build' },
9596
{ text: 'resetField', link: '/api/use-form-handler/reset-field' },
9697
{ text: 'resetForm', link: '/api/use-form-handler/reset-form' },
9798
{ text: 'setError', link: '/api/use-form-handler/set-error' },

docs/api/use-form-handler/build.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# build
2+
3+
Builds a form based on the configuration passed, to leave a cleaner template and provide better readability over the whole form setup.
4+
5+
## Demo
6+
7+
Coming soon...
8+
9+
## Usage
10+
11+
### Build complex form fields and improve readability
12+
13+
```vue
14+
<template>
15+
<form @submit.prevent="() => handleSubmit(successFn, errorFn)">
16+
<input type="text" v-bind="form.name" />
17+
<input type="text" v-bind="form.email" />
18+
<input type="text" v-bind="form.password" />
19+
<input type="text" v-bind="form.passwordConfirmation" />
20+
<button type="submit">Submit</button>
21+
</form>
22+
</template>
23+
<script setup lang="ts">
24+
import { useFormHandler } from 'vue-form-handler'
25+
26+
const successFn = (result: Record<string, any>) => { console.log(result) }
27+
const errorFn = (result: Record<string, string | undefined>) => { console.warn(result) }
28+
const { build, handleSubmit, values, formState } = useFormHandler()
29+
30+
const form = build({
31+
name: {
32+
required: true,
33+
pattern: {
34+
value: /^[a-zA-Z]+$/,
35+
message: 'Only letters are allowed'
36+
}
37+
},
38+
email: {
39+
required: true,
40+
pattern: {
41+
value: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/,
42+
message: 'Please enter a valid email'
43+
}
44+
},
45+
password: {
46+
required: true,
47+
pattern: {
48+
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/,
49+
message: 'Password must contain at least 8 characters, one uppercase, one lowercase and one number'
50+
}
51+
},
52+
passwordConfirmation: {
53+
required: true,
54+
pattern: {
55+
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/,
56+
message: 'Password must contain at least 8 characters, one uppercase, one lowercase and one number'
57+
},
58+
validate: {
59+
match: (value: string) => value === values.password || 'Passwords do not match'
60+
}
61+
}
62+
})
63+
</script>
64+
```
65+
66+
Notice how the template looks much cleaner with this approach, and this helps us to achieve better readability and is less confusing since we bind directly pieces of a form to each component/field on the template.
67+
68+
## Type Declarations
69+
70+
```ts
71+
export interface Build<T = Record<string, RegisterOptions>> {
72+
(configuration: T | Ref<T> | ComputedRef<T>): ComputedRef<
73+
Record<keyof T, RegisterReturn>
74+
>
75+
}
76+
```

docs/api/use-form-handler/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Using the `always` validationMode will have a more significant impact on perform
153153
- [handleSubmit](/api/use-form-handler/handle-submit)
154154
- [modifiedValues](/api/use-form-handler/modified-values)
155155
- [register](/api/use-form-handler/register)
156+
- [build](/api/use-form-handler/build)
156157
- [resetField](/api/use-form-handler/reset-field)
157158
- [resetForm](/api/use-form-handler/reset-form)
158159
- [setError](/api/use-form-handler/set-error)
@@ -187,6 +188,7 @@ export interface FormHandlerReturn {
187188
handleSubmit: HandleSubmit
188189
modifiedValues: ModifiedValues
189190
register: Register
191+
register: Build
190192
resetField: ResetField
191193
resetForm: ResetForm
192194
setError: SetError

src/test/component.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { FormHandler } from '../FormHandler'
22
import { mount } from '@vue/test-utils'
33
import { expect, it, describe } from 'vitest'
44

5-
describe('FormHandler component testing', () => {
6-
it('Form handler gets mounted', () => {
5+
describe('FormHandler', () => {
6+
it('should mount', () => {
77
expect(FormHandler).toBeTruthy()
88

99
const wrapper = mount(FormHandler)
1010
expect(wrapper.exists()).toBeTruthy()
1111
})
12-
it('Form handler scoped slot provides values and form state', () => {
12+
it('should provide values and formState', () => {
1313
expect(FormHandler).toBeTruthy()
1414

1515
const wrapper = mount(FormHandler, {
@@ -23,7 +23,7 @@ describe('FormHandler component testing', () => {
2323
expect(wrapper.html()).toContain('values')
2424
expect(wrapper.html()).toContain('formState')
2525
})
26-
it('Form handler is correctly initialized', () => {
26+
it('should be properly initialized', () => {
2727
const initialValues = {
2828
field1: 'something',
2929
field2: 'some other thing',

src/test/handler.test.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import { initialState, useFormHandler } from '../useFormHandler'
22
import { expect, it, describe } from 'vitest'
33

4-
describe('Form handler testing', () => {
5-
it('Initial form state and values', () => {
4+
describe('useFormHandler()', () => {
5+
it('should have correct initial state and values', () => {
66
const { values, formState } = useFormHandler()
77
expect(values).toStrictEqual({})
88
expect(formState).toStrictEqual({ ...initialState() })
99
})
10-
it('Initial values should be applied without', () => {
10+
it('should apply initialValues when specified', () => {
1111
const initialValues = {
1212
field: 'test',
1313
}
1414
const { values } = useFormHandler({ initialValues })
1515
expect(values).toStrictEqual(initialValues)
1616
})
17-
it('Setting a value programmatically', async () => {
17+
it('should set a value programmatically', async () => {
1818
const { values, setValue, formState } = useFormHandler()
1919
await setValue('field', 'oneTwoThree')
2020
expect(values.field).toBe('oneTwoThree')
2121
expect(formState.isDirty).toBeTruthy()
2222
})
23-
it('Clearing a field programmatically', async () => {
23+
it('should clear a field', async () => {
2424
const { register, values, setValue, formState, clearField } =
2525
useFormHandler()
2626
register('field')
@@ -31,7 +31,7 @@ describe('Form handler testing', () => {
3131
expect(values.field).toBe(null)
3232
expect(formState.isDirty).toBeFalsy()
3333
})
34-
it('Clearing an initialized field leaves it dirty', async () => {
34+
it('should leave an initialized field dirty when clearing', async () => {
3535
const { register, values, formState, clearField } = useFormHandler({
3636
initialValues: { field: 'value' },
3737
})
@@ -42,13 +42,13 @@ describe('Form handler testing', () => {
4242
expect(values.field).toBe(null)
4343
expect(formState.isDirty).toBeTruthy()
4444
})
45-
it('Setting an error programmatically', async () => {
45+
it('should set an error programmatically', async () => {
4646
const { formState, setError } = useFormHandler()
4747
setError('field', 'some error')
4848
expect(formState.errors).toStrictEqual({ field: 'some error' })
4949
expect(formState.isValid).toBeFalsy()
5050
})
51-
it('Clearing an error programmatically', async () => {
51+
it('should clear an error programmatically', async () => {
5252
const { formState, setError, clearError } = useFormHandler()
5353
const error = 'This field has an error'
5454
setError('field', error)
@@ -57,7 +57,7 @@ describe('Form handler testing', () => {
5757
expect(formState.errors.field).toBeUndefined()
5858
expect(formState.isValid).toBeTruthy()
5959
})
60-
it('Clearing all errors of the form programmatically', async () => {
60+
it('should clear all form errors programmatically', async () => {
6161
const { formState, setError, clearError } = useFormHandler()
6262
const error = 'some error'
6363
setError('field1', error)
@@ -70,7 +70,7 @@ describe('Form handler testing', () => {
7070
expect(formState.errors.field2).toBeUndefined()
7171
expect(formState.isValid).toBeTruthy()
7272
})
73-
it('Resetting a field it back to its initial values and state', async () => {
73+
it('should correctly reset a field', async () => {
7474
const { values, formState, resetField, setValue } = useFormHandler({
7575
initialValues: { field: 'value' },
7676
})
@@ -83,7 +83,7 @@ describe('Form handler testing', () => {
8383
expect(values.field).toBe('value')
8484
expect(formState.isDirty).toBeFalsy()
8585
})
86-
it('Expecting modifiedValues to work', async () => {
86+
it('should return correct modifiedValues', async () => {
8787
const { modifiedValues, setValue } = useFormHandler({
8888
initialValues: {
8989
field: 'something',
@@ -92,4 +92,21 @@ describe('Form handler testing', () => {
9292
await setValue('field2', 'another thing')
9393
expect(modifiedValues()).toStrictEqual({ field2: 'another thing' })
9494
})
95+
it('should register field properly via build', () => {
96+
const { build, values } = useFormHandler()
97+
const form = build({
98+
field: {},
99+
})
100+
101+
expect(form.value.field.name).toBe('field')
102+
expect(form.value.field.error).toBeUndefined()
103+
expect(form.value.field.onBlur).toBeDefined()
104+
expect(form.value.field.isDirty).toBeUndefined()
105+
expect(form.value.field.isTouched).toBeUndefined()
106+
expect(form.value.field.onClear).toBeDefined()
107+
expect(form.value.field.onChange).toBeDefined()
108+
expect(form.value.field.modelValue).toBe(null)
109+
expect(form.value.field['onUpdate:modelValue']).toBeDefined()
110+
expect(values.field).toBe(null)
111+
})
95112
})

src/test/register.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, it, expect } from 'vitest'
22
import { useFormHandler } from '../useFormHandler'
33

4-
describe('Register function testing', () => {
5-
it('Registering a field', () => {
4+
describe('register()', () => {
5+
it('should register a field', () => {
66
const { values, register } = useFormHandler()
77
const field = register('field')
88
expect(field.name).toBe('field')
@@ -16,31 +16,37 @@ describe('Register function testing', () => {
1616
expect(field['onUpdate:modelValue']).toBeDefined()
1717
expect(values.field).toBe(null)
1818
})
19-
it('Specified native field should have native handlers', () => {
19+
it('should apply native handlers by default', () => {
20+
const { register } = useFormHandler()
21+
const field = register('field')
22+
expect(field.ref).toBeDefined()
23+
expect(field.onChange).toBeDefined()
24+
})
25+
it('should apply native handlers when native is specified', () => {
2026
const { register } = useFormHandler()
2127
const field = register('field', { native: true })
2228
expect(field.ref).toBeDefined()
2329
expect(field.onChange).toBeDefined()
2430
})
25-
it("Specified custom field shouldn't have native handlers", () => {
31+
it('should not apply native handlers when native is set false', () => {
2632
const { register } = useFormHandler()
2733
const field = register('field', { native: false })
2834
expect(field.onChange).toBeUndefined()
2935
expect(field.modelValue).toBeDefined()
3036
expect(field['onUpdate:modelValue']).toBeDefined()
3137
})
32-
it('Input registered with details receives dirty and touched states', () => {
38+
it('should apply dirty and touched states when withDetails is specified', () => {
3339
const { register } = useFormHandler()
3440
const field = register('field', { withDetails: true })
3541
expect(field.isDirty).toBeDefined()
3642
expect(field.isTouched).toBeDefined()
3743
})
38-
it('Registering a field with default value', () => {
44+
it('should apply default value', () => {
3945
const { values, register } = useFormHandler()
4046
register('field', { defaultValue: 'something' })
4147
expect(values.field).toBe('something')
4248
})
43-
it('Registered validations work on update via handler', async () => {
49+
it('should trigger validation via handler', async () => {
4450
const { values, register, formState } = useFormHandler()
4551
const field = register('field', {
4652
validate: {
@@ -53,7 +59,7 @@ describe('Register function testing', () => {
5359
expect(formState.isValid).toBeFalsy()
5460
}
5561
})
56-
it('Registered validations work on update via setter', async () => {
62+
it('should trigger validation via setter', async () => {
5763
const { values, register, formState, setValue, triggerValidation } =
5864
useFormHandler()
5965
register('field', {

src/types/formHandler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComputedRef, Ref } from 'vue'
2-
import { RegisterOptions, Register } from './register'
2+
import { RegisterOptions, Register, RegisterReturn } from './register'
33

44
export interface FormState {
55
/** Boolean holding the dirty state of the form */
@@ -77,6 +77,12 @@ export type HandleSubmit = (
7777
errorFn?: HandleSubmitErrorFn
7878
) => void
7979

80+
export interface Build<T = Record<string, RegisterOptions>> {
81+
(configuration: T | Ref<T> | ComputedRef<T>): ComputedRef<
82+
Record<keyof T, RegisterReturn>
83+
>
84+
}
85+
8086
export interface InterceptorParams {
8187
/** Name of the field that is currently about to be set*/
8288
name: string
@@ -159,6 +165,9 @@ export interface FormHandlerReturn {
159165
/** Method to register a field and make it interact with the current form */
160166
register: Register
161167

168+
/** Method to build a form configuration */
169+
build: Build
170+
162171
/** Function to reset a field */
163172
resetField: ResetField
164173

src/useFormHandler.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Build } from './types/formHandler'
12
import { NativeValidations } from './types/validations'
23
import { DEFAULT_FIELD_VALUE } from './constants'
34
import {
@@ -23,8 +24,10 @@ import {
2324
ValidationsConfiguration,
2425
Unregister,
2526
FieldReference,
27+
RegisterOptions,
28+
RegisterReturn,
2629
} from './types'
27-
import { reactive, readonly, unref, watch } from '@vue/runtime-core'
30+
import { computed, reactive, readonly, unref, watch } from '@vue/runtime-core'
2831
import { isEqual } from 'lodash-es'
2932
import {
3033
getNativeFieldValue,
@@ -59,7 +62,7 @@ export const useFormHandler: UseFormHandler = ({
5962
const _getDefault = (name: string): any =>
6063
_refs[name]?._defaultValue ?? getDefaultFieldValue(_refs[name]?.ref)
6164
const _getInitial = (name: string): any =>
62-
unref(initialValues)?.[name] ?? _getDefault(name)
65+
(unref(initialValues) as Record<string, any>)?.[name] ?? _getDefault(name)
6366
const _initControl: InitControl = (name, options) => {
6467
const needsReset = options.disabled && _refs[name] && !_refs[name]._disabled
6568
_refs[name] = {
@@ -76,7 +79,11 @@ export const useFormHandler: UseFormHandler = ({
7679
unregister(name)
7780
return
7881
}
79-
if (initialValues[name] === undefined && values[name] === undefined) {
82+
if (
83+
(!initialValues ||
84+
(unref(initialValues) as Record<string, any>)?.[name] === undefined) &&
85+
values[name] === undefined
86+
) {
8087
values[name] = _getDefault(name)
8188
}
8289
}
@@ -289,6 +296,16 @@ export const useFormHandler: UseFormHandler = ({
289296
}
290297
}
291298

299+
const build: Build = (configuration) => {
300+
const staticConfig = unref(configuration) as Record<string, RegisterOptions>
301+
return computed(() =>
302+
Object.keys(staticConfig).reduce((acc, key) => {
303+
acc[key] = register(key, staticConfig[key])
304+
return acc
305+
}, {} as Record<string, RegisterReturn>)
306+
)
307+
}
308+
292309
const isValidForm: IsValidForm = async () => {
293310
if (validate) {
294311
return await validate(values)
@@ -322,6 +339,7 @@ export const useFormHandler: UseFormHandler = ({
322339
handleSubmit,
323340
modifiedValues,
324341
register,
342+
build,
325343
resetField,
326344
resetForm,
327345
setError,

0 commit comments

Comments
 (0)