From 1a8feb751e6827c414ef82fe9fb259ba7dcc7e08 Mon Sep 17 00:00:00 2001
From: Jack Bobakanoosh <10647192+Bobakanoosh@users.noreply.github.com>
Date: Tue, 24 Jun 2025 10:56:12 -0500
Subject: [PATCH] fix(Form): expose reactive fields (#4386)
---
src/runtime/components/Form.vue | 15 ++++----
src/runtime/types/form.ts | 6 ++--
test/components/Form.spec.ts | 63 ++++++++++++++++++++++++++++++++-
3 files changed, 72 insertions(+), 12 deletions(-)
diff --git a/src/runtime/components/Form.vue b/src/runtime/components/Form.vue
index 3213c5f2..a38d35be 100644
--- a/src/runtime/components/Form.vue
+++ b/src/runtime/components/Form.vue
@@ -1,5 +1,4 @@
diff --git a/src/runtime/types/form.ts b/src/runtime/types/form.ts
index 3749c370..96fe5f17 100644
--- a/src/runtime/types/form.ts
+++ b/src/runtime/types/form.ts
@@ -16,9 +16,9 @@ export interface Form {
dirty: ComputedRef
loading: Ref
- dirtyFields: DeepReadonly>>
- touchedFields: DeepReadonly>>
- blurredFields: DeepReadonly>>
+ dirtyFields: ReadonlySet>>
+ touchedFields: ReadonlySet>>
+ blurredFields: ReadonlySet>>
}
export type FormSchema =
diff --git a/test/components/Form.spec.ts b/test/components/Form.spec.ts
index 9ca9c6c4..e2bc3c3e 100644
--- a/test/components/Form.spec.ts
+++ b/test/components/Form.spec.ts
@@ -1,4 +1,4 @@
-import { reactive, ref, nextTick } from 'vue'
+import { reactive, ref, nextTick, watch } from 'vue'
import { describe, it, expect, test, beforeEach, vi } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import * as z from 'zod'
@@ -304,6 +304,67 @@ describe('Form', () => {
expect(form.value.blurredFields.has('email')).toBe(true)
expect(form.value.blurredFields.has('password')).toBe(false)
})
+
+ test('reactivity: touchedFields works on focus', async () => {
+ const emailInput = wrapper.find('#emailInput')
+
+ const mockWatchCallback = vi.fn()
+ watch(() => form.value.touchedFields, mockWatchCallback, { deep: true })
+
+ emailInput.trigger('focus')
+ await flushPromises()
+ expect(mockWatchCallback).toHaveBeenCalledTimes(1)
+ expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
+ expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
+ })
+
+ test('reactivity: touchedFields works on change', async () => {
+ const emailInput = wrapper.find('#emailInput')
+
+ const mockWatchCallback = vi.fn()
+ watch(() => form.value.touchedFields, mockWatchCallback, { deep: true })
+
+ emailInput.trigger('change')
+ await flushPromises()
+ expect(mockWatchCallback).toHaveBeenCalledTimes(1)
+ expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
+ expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
+ })
+
+ test('reactivity: blurredFields works', async () => {
+ const emailInput = wrapper.find('#emailInput')
+
+ const mockWatchCallback = vi.fn()
+ watch(() => form.value.blurredFields, mockWatchCallback, { deep: true })
+
+ emailInput.trigger('blur')
+ await flushPromises()
+ expect(mockWatchCallback).toHaveBeenCalledTimes(1)
+ expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
+ expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
+ })
+
+ test('reactivity: dirtyFields works', async () => {
+ const emailInput = wrapper.find('#emailInput')
+ const mockWatchCallback = vi.fn()
+ watch(() => form.value.dirtyFields, mockWatchCallback, { deep: true })
+
+ emailInput.trigger('change')
+ await flushPromises()
+ expect(mockWatchCallback).toHaveBeenCalledTimes(1)
+ expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
+ expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
+ })
+
+ test('reactivity: dirty works', async () => {
+ const emailInput = wrapper.find('#emailInput')
+ expect(form.value.dirty).toBe(false)
+
+ emailInput.trigger('change')
+ await flushPromises()
+
+ expect(form.value.dirty).toBe(true)
+ })
})
describe('nested', async () => {