diff --git a/packages/ui/src/components/va-form/VaForm.stories.ts b/packages/ui/src/components/va-form/VaForm.stories.ts
index 695db8fa44..2c97a8dc48 100644
--- a/packages/ui/src/components/va-form/VaForm.stories.ts
+++ b/packages/ui/src/components/va-form/VaForm.stories.ts
@@ -240,7 +240,7 @@ export const ValidateAndResetValidation: StoryFn = () => ({
components: { VaForm, VaInput, VaButton },
template: `
-
+
Validate
@@ -539,3 +539,60 @@ export const FormDataInitialValue = () => ({
`,
})
+
+export const DirtyForm: StoryFn = () => ({
+ components: { VaForm, VaInput, VaButton },
+
+ setup () {
+ const { isDirty } = useForm('form')
+
+ return {
+ isDirty,
+ }
+ },
+
+ template: `
+
[form-dirty]: {{ isDirty }}
+ [input-dirty]: {{ $refs.input?.isDirty }}
+
+
+
+
+ Validate
+
+
+ Reset validation
+
+ `,
+})
+
+DirtyForm.play = async ({ canvasElement, step }) => {
+ const canvas = within(canvasElement)
+ const input = canvas.getByTestId('input')
+ const validateButton = canvas.getByRole('button', { name: 'Validate' }) as HTMLElement
+ const resetButton = canvas.getByRole('button', { name: 'Reset validation' }) as HTMLElement
+
+ await step('Validates input with error', async () => {
+ await userEvent.click(validateButton)
+ expect(input.getAttribute('aria-invalid')).toEqual('true')
+ expect(canvasElement.querySelector('#form-dirty')?.innerHTML.includes('true')).toBeTruthy()
+ expect(canvasElement.querySelector('#input-dirty')?.innerHTML.includes('false')).toBeTruthy()
+ })
+
+ await step('Reset inputs validation', async () => {
+ await userEvent.click(resetButton)
+ expect(input.getAttribute('aria-invalid')).toEqual('false')
+ })
+
+ await step('Validates input on input error', async () => {
+ await userEvent.type(input, 'Hello')
+ expect(input.getAttribute('aria-invalid')).toEqual('true')
+ expect(canvasElement.querySelector('#form-dirty')?.innerHTML.includes('false')).toBeTruthy()
+ expect(canvasElement.querySelector('#input-dirty')?.innerHTML.includes('true')).toBeTruthy()
+ })
+
+ await step('Reset inputs validation', async () => {
+ await userEvent.click(resetButton)
+ expect(input.getAttribute('aria-invalid')).toEqual('false')
+ })
+}
diff --git a/packages/ui/src/components/va-input/VaInput.vue b/packages/ui/src/components/va-input/VaInput.vue
index 03cfe22b31..24715e31b4 100644
--- a/packages/ui/src/components/va-input/VaInput.vue
+++ b/packages/ui/src/components/va-input/VaInput.vue
@@ -148,6 +148,7 @@ export default defineComponent({
})
const {
+ isDirty,
computedError,
computedErrorMessages,
listeners: { onBlur, onFocus },
@@ -240,6 +241,7 @@ export default defineComponent({
fieldListeners: createFieldListeners(emit),
filterSlots,
+ isDirty,
reset,
focus,
blur,
diff --git a/packages/ui/src/composables/useForm/useFormChild.ts b/packages/ui/src/composables/useForm/useFormChild.ts
index 8857368bf5..93f0e26ce9 100644
--- a/packages/ui/src/composables/useForm/useFormChild.ts
+++ b/packages/ui/src/composables/useForm/useFormChild.ts
@@ -8,6 +8,7 @@ export const useFormChild = (context: FormFiled) => {
if (!formContext) {
return {
+ isFormDirty: ref(false),
isFormImmediate: ref(false),
doShowError: ref(true),
doShowErrorMessages: ref(true),
@@ -26,6 +27,7 @@ export const useFormChild = (context: FormFiled) => {
})
return {
+ isFormDirty: formContext.isFormDirty,
isFormImmediate: formContext.immediate,
doShowError: formContext.doShowError,
doShowErrorMessages: formContext.doShowErrorMessages,
diff --git a/packages/ui/src/composables/useForm/useFormParent.ts b/packages/ui/src/composables/useForm/useFormParent.ts
index 7f8c487521..081abc6509 100644
--- a/packages/ui/src/composables/useForm/useFormParent.ts
+++ b/packages/ui/src/composables/useForm/useFormParent.ts
@@ -20,6 +20,7 @@ export const createFormContext = (options: FormParentOptio
doShowError: computed(() => !options.hideErrors),
doShowErrorMessages: computed(() => !options.hideErrorMessages),
doShowLoading: computed(() => !options.hideLoading),
+ isFormDirty: ref(false),
registerField: (uid: number, field: FormFiled) => {
fields.value.set(uid, field as FormFiled)
},
@@ -34,7 +35,7 @@ export const useFormParent = (options: FormParent
provide(FormServiceKey, formContext)
- const { fields } = formContext
+ const { fields, isFormDirty } = formContext
const fieldNames = computed(() => fields.value.map((field) => unref(field.name)).filter(Boolean) as Names[])
const fieldsNamed = computed(() => fields.value.reduce((acc, field) => {
@@ -47,7 +48,10 @@ export const useFormParent = (options: FormParent
}, {} as Record))
const isValid = computed(() => fields.value.every((field) => unref(field.isValid)))
const isLoading = computed(() => fields.value.some((field) => unref(field.isLoading)))
- const isDirty = computed(() => fields.value.some((field) => unref(field.isLoading)))
+ const isDirty = computed({
+ get () { return fields.value.some((field) => unref(field.isLoading)) || isFormDirty.value },
+ set (v) { isFormDirty.value = v },
+ })
const errorMessages = computed(() => fields.value.map((field) => unref(field.errorMessages)).flat())
const errorMessagesNamed = computed(() => fields.value.reduce((acc, field) => {
if (unref(field.name)) { acc[unref(field.name) as Names] = unref(field.errorMessages) }
@@ -55,6 +59,7 @@ export const useFormParent = (options: FormParent
}, {} as Record))
const validate = () => {
+ isDirty.value = true
// Validate each filed to get the error messages
return fields.value.reduce((acc, field) => {
return field.validate() && acc
@@ -62,16 +67,19 @@ export const useFormParent = (options: FormParent
}
const validateAsync = () => {
+ isDirty.value = true
return Promise.all(fields.value.map((field) => field.validateAsync())).then((results) => {
return results.every(Boolean)
})
}
const reset = () => {
+ isDirty.value = false
fields.value.forEach((field) => field.reset())
}
const resetValidation = () => {
+ isDirty.value = false
fields.value.forEach((field) => field.resetValidation())
}
diff --git a/packages/ui/src/composables/useValidation.ts b/packages/ui/src/composables/useValidation.ts
index 8f04c1cd5b..9f92000de1 100644
--- a/packages/ui/src/composables/useValidation.ts
+++ b/packages/ui/src/composables/useValidation.ts
@@ -100,6 +100,7 @@ export const useValidation = {
computedError.value = false
computedErrorMessages.value = []
+ isDirty.value = false
}
const processResults = (results: any[]) => {
@@ -193,6 +194,7 @@ export const useValidation = !computedError.value),
@@ -202,7 +204,11 @@ export const useValidation = {
+ reset()
+ resetValidation()
+ isDirty.value = false
+ },
value: computed(() => options.value || props.modelValue),
name: toRef(props, 'name'),
})
@@ -212,7 +218,9 @@ export const useValidation = {
// Hide error if component haven't been interacted yet
// Ignore dirty state if immediateValidation is true
- if (!immediateValidation.value && !isDirty.value) { return false }
+ if (!isFormDirty.value) {
+ if (!immediateValidation.value && !isDirty.value) { return false }
+ }
return doShowError.value ? computedError.value : false
}),