Skip to content

Commit

Permalink
Merge pull request #122 from gutentag2012/fix/step-wizard-no-unmounte…
Browse files Browse the repository at this point in the history
…d-errors

Fix/step wizard no unmounted errors
  • Loading branch information
gutentag2012 authored Oct 26, 2024
2 parents 963454f + d8723b8 commit 739309c
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/reference/core/FieldLogic.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type FieldLogicOptions<

removeValueOnUnmount?: boolean
resetValueToDefaultOnUnmount?: boolean
keepInFormOnUnmount?: boolean

transformFromBinding?: (value: TBoundValue) => ValueAtPath<TData, TName> | [ValueAtPath<TData, TName>, ValidationError]
transformToBinding?: (value: ValueAtPath<TData, TName>, isValid: boolean, writeBuffer?: TBoundValue) => TBoundValue
Expand All @@ -116,6 +117,7 @@ type FieldLogicOptions<
| `defaultState` | The default state of the field. There you can set default errors and the touched state. |
| `removeValueOnUnmount` | If set to `true`, the value of the field will be removed when the field is unmounted. |
| `resetValueToDefaultOnUnmount` | If set to `true`, the value of the field will be reset to the default value when the field is unmounted. |
| `keepInFormOnUnmount` | If set to `true`, the field will stay in the form when unmounted. |
| `transformFromBinding` | The function to transform the value from the binding. <br/>Reference the [Basic Usage](/guide/basic-usage#add-transformation) |
| `transformToBinding` | The function to transform the value to the binding. <br/>Reference the [Basic Usage](/guide/basic-usage#add-transformation) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const AccountStep = (props: PersonalStepProps) => {
name="username"
validator={z.string().min(3)}
validatorOptions={{ validateOnChangeIfTouched: true }}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Username</Label>
Expand All @@ -58,6 +59,7 @@ export const AccountStep = (props: PersonalStepProps) => {
name="password"
validator={z.string().min(8)}
validatorOptions={{ validateOnChangeIfTouched: true }}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Password</Label>
Expand All @@ -72,6 +74,7 @@ export const AccountStep = (props: PersonalStepProps) => {
}
validateMixin={['password']}
validatorOptions={{ validateOnChangeIfTouched: true }}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Confirm Password</Label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,45 @@ export const AddressStep = (props: PersonalStepProps) => {
</CardHeader>

<CardContent>
<group.FieldProvider name="street" validator={z.string().min(1)}>
<group.FieldProvider
name="street"
validator={z.string().min(1)}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Street</Label>
<InputForm placeholder="Type here..." />
<ErrorText />
</div>
</group.FieldProvider>
<group.FieldProvider name="city" validator={z.string().min(1)}>
<group.FieldProvider
name="city"
validator={z.string().min(1)}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>City</Label>
<InputForm placeholder="Type here..." />
<ErrorText />
</div>
</group.FieldProvider>
<div className="mb-1.5 flex flex-row gap-2">
<group.FieldProvider name="zip" validator={z.string().min(1)}>
<group.FieldProvider
name="zip"
validator={z.string().min(1)}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Postal Code</Label>
<InputForm placeholder="Type here..." />
<ErrorText />
</div>
</group.FieldProvider>
<group.FieldProvider name="state" validator={z.string().min(1)}>
<group.FieldProvider
name="state"
validator={z.string().min(1)}
keepInFormOnUnmount
>
<div className="flex-[5]">
<Label>State</Label>
<InputForm placeholder="Type here..." />
Expand All @@ -84,6 +100,7 @@ export const AddressStep = (props: PersonalStepProps) => {
name="country"
validator={z.string()}
defaultValue="USA"
keepInFormOnUnmount
>
{(field) => (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,34 @@ export const PersonalStep = (props: PersonalStepProps) => {

<CardContent>
<div className="mb-1.5 flex flex-row gap-2">
<group.FieldProvider name="firstName" validator={z.string().min(1)}>
<group.FieldProvider
name="firstName"
validator={z.string().min(1)}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>First Name</Label>
<InputForm placeholder="Type here..." />
<ErrorText />
</div>
</group.FieldProvider>
<group.FieldProvider name="lastName" validator={z.string().min(1)}>
<group.FieldProvider
name="lastName"
validator={z.string().min(1)}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Last Name</Label>
<InputForm placeholder="Type here..." />
<ErrorText />
</div>
</group.FieldProvider>
</div>
<group.FieldProvider name="dateOfBirth" validator={z.date()}>
<group.FieldProvider
name="dateOfBirth"
validator={z.date()}
keepInFormOnUnmount
>
{(field) => (
<div className="flex flex-1 flex-col gap-1">
<Label htmlFor={field.name}>Date of Birth</Label>
Expand All @@ -78,14 +90,19 @@ export const PersonalStep = (props: PersonalStepProps) => {
name="email"
validator={z.string().email().min(1)}
validatorOptions={{ validateOnChangeIfTouched: true }}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Email</Label>
<InputForm placeholder="Type here..." />
<ErrorText />
</div>
</group.FieldProvider>
<group.FieldProvider name="phone" validator={z.string().optional()}>
<group.FieldProvider
name="phone"
validator={z.string().optional()}
keepInFormOnUnmount
>
<div className="flex-1">
<Label>Phone</Label>
<InputForm placeholder="Type here..." />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export const PreferencesStep = (props: PersonalStepProps) => {
<group.FieldProvider
name="contact"
validator={([value, email, phone]) => {
console.log('validate', value, email, phone)
if (value === 'email' && !email)
return 'Email is required for this contact method'
if (value === 'phone' && !phone)
Expand All @@ -64,6 +63,7 @@ export const PreferencesStep = (props: PersonalStepProps) => {
}}
defaultValue="email"
validateMixin={['email', 'phone']}
keepInFormOnUnmount
>
{(field) => (
<div>
Expand All @@ -86,6 +86,7 @@ export const PreferencesStep = (props: PersonalStepProps) => {
name="language"
validator={z.string()}
defaultValue="english"
keepInFormOnUnmount
>
{(field) => (
<div>
Expand All @@ -107,6 +108,7 @@ export const PreferencesStep = (props: PersonalStepProps) => {
name="newsletter"
validator={z.boolean()}
defaultValue={false}
keepInFormOnUnmount
>
<Label className="items-top mt-2 flex gap-2">
<CheckboxForm className="h-5 w-5 rounded" />
Expand Down
61 changes: 61 additions & 0 deletions packages/form-core/src/FieldGroupLogic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,67 @@ describe('FieldGroupLogic', () => {
'Value must be less than or equal to 7',
])
})
it('should still keep validating, even if fields are unmounted', () => {
const form = new FormLogic({
defaultValues: {
name: 'default',
age: 51,
},
})
form.mount()
const fieldName = form.getOrCreateField('name')
fieldName.mount()
const fieldAge = form.getOrCreateField('age')
fieldAge.mount()

const fieldGroup = form.getOrCreateFieldGroup(['name', 'age'], {
validator: ({ name, age }) => {
if (name === 'default') return 'name should not be default'
if (age < 50) return 'age should be over 50'
return undefined
},
validatorOptions: {
validateOnMount: true,
},
})
fieldGroup.mount()

expect(fieldGroup.errors.value).toEqual(['name should not be default'])
})
it('should still keep validating its fields, even if fields are unmounted, if configured', () => {
const form = new FormLogic({
defaultValues: {
name: 'default',
age: 51,
},
})
form.mount()
const fieldName = form.getOrCreateField('name', {
keepInFormOnUnmount: true,
validator: (name) =>
name === 'default' ? 'name should not be default' : undefined,
validatorOptions: {
validateOnMount: true,
},
})
fieldName.mount()
const fieldAge = form.getOrCreateField('age', {
keepInFormOnUnmount: true,
validator: (age) => (age < 50 ? 'age should be over 50' : undefined),
validatorOptions: {
validateOnMount: true,
},
})
fieldAge.mount()

const fieldGroup = form.getOrCreateFieldGroup(['name', 'age'])
fieldGroup.mount()

fieldName.unmount()
fieldAge.unmount()

expect(fieldGroup.isValid.value).toEqual(false)
})
})
describe('handleSubmit', () => {
it('should not handle submit if the group is invalid', () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/form-core/src/FieldLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ export type FieldLogicOptions<
* If true, the field value will set to its default value.
*/
resetValueToDefaultOnUnmount?: boolean
/**
* Whenever a field is unmounted, the field is removed from the form.
* If true, the field will stay in the form.
*/
keepInFormOnUnmount?: boolean

/**
* This takes the value provided by the binding and transforms it to the value that should be set in the form.
Expand Down Expand Up @@ -572,6 +577,7 @@ export class FieldLogic<
this.defaultValue.peek(),
this._options.peek()?.removeValueOnUnmount,
this._options.peek()?.resetValueToDefaultOnUnmount,
this._options.peek()?.keepInFormOnUnmount,
)
}

Expand Down
34 changes: 34 additions & 0 deletions packages/form-core/src/FormLogic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,40 @@ describe('FormLogic', () => {
await form.handleSubmit()
expect(field.errors.value).toEqual([])
})
it('should validate unmounted fields if they are kept, only for the submit event', async () => {
const form = new FormLogic<{ name: string }>({
defaultValues: {
name: 'default',
},
})
await form.mount()
const field = new FieldLogic(form, 'name', {
validator: () => 'error',
keepInFormOnUnmount: true,
})
await field.mount()

field.unmount()
await form.handleSubmit()
expect(field.errors.value).toEqual(['error'])
})
it("should not validate unmounted fields if they are kept, for any event other than 'onSubmit'", async () => {
const form = new FormLogic<{ name: string }>({
defaultValues: {
name: 'default',
},
})
await form.mount()
const field = new FieldLogic(form, 'name', {
validator: () => 'error',
keepInFormOnUnmount: true,
})
await field.mount()

field.unmount()
form.data.value.name.value = 'asd'
expect(field.errors.value).toEqual([])
})
})
describe('handleSubmit', () => {
it('should not handle submit if the form is invalid', () => {
Expand Down
24 changes: 17 additions & 7 deletions packages/form-core/src/FormLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,13 @@ export class FormLogic<
this.validateForEvent('onSubmit'),
...this._fieldsArray
.peek()
.map((field) => field.validateForEvent('onSubmit')),
// If the values are kept in the form but are unmounted, we want to force validation
.map((field) =>
field.validateForEvent(
'onSubmit',
field.options.peek()?.keepInFormOnUnmount,
),
),
...this._fieldGroupsArray
.peek()
.map((group) => group.validateForEvent('onSubmit')),
Expand Down Expand Up @@ -871,6 +877,7 @@ export class FormLogic<
* @param defaultValue - The default value for the field.
* @param removeValue - If true, the value will be removed in the form data.
* @param resetToDefault - If true, the value will be reset to the default value.
* @param keepInForm - If true, the field will not be removed from the form.
*
* @note
* By default, the value of an unregistered Field will be removed from the form data.
Expand All @@ -882,14 +889,17 @@ export class FormLogic<
defaultValue?: ValueAtPath<TData, TPath>,
removeValue?: boolean,
resetToDefault?: boolean,
keepInForm?: boolean,
): void {
const newMap = new Map(this._fields.peek())
newMap.delete(path)
for (const key of newMap.keys()) {
if (!(key as string).startsWith(`${path}.`)) continue
newMap.delete(key)
if (!keepInForm) {
const newMap = new Map(this._fields.peek())
newMap.delete(path)
for (const key of newMap.keys()) {
if (!(key as string).startsWith(`${path}.`)) continue
newMap.delete(key)
}
this._fields.value = newMap
}
this._fields.value = newMap

if (removeValue) {
removeSignalValueAtPath(this._data, path)
Expand Down

0 comments on commit 739309c

Please sign in to comment.