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 () => {