Skip to content

Commit 1a8feb7

Browse files
authored
fix(Form): expose reactive fields (#4386)
1 parent 1d281e9 commit 1a8feb7

File tree

3 files changed

+72
-12
lines changed

3 files changed

+72
-12
lines changed

src/runtime/components/Form.vue

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script lang="ts">
2-
import type { DeepReadonly } from 'vue'
32
import type { AppConfig } from '@nuxt/schema'
43
import theme from '#build/ui/form'
54
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
@@ -64,7 +63,7 @@ export interface FormSlots {
6463
</script>
6564

6665
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
67-
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
66+
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly, reactive } from 'vue'
6867
import { useEventBus } from '@vueuse/core'
6968
import { useAppConfig } from '#imports'
7069
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
@@ -155,9 +154,9 @@ provide('form-errors', errors)
155154
const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({})
156155
provide(formInputsInjectionKey, inputs as any)
157156
158-
const dirtyFields = new Set<keyof I>()
159-
const touchedFields = new Set<keyof I>()
160-
const blurredFields = new Set<keyof I>()
157+
const dirtyFields: Set<keyof I> = reactive(new Set<keyof I>())
158+
const touchedFields: Set<keyof I> = reactive(new Set<keyof I>())
159+
const blurredFields: Set<keyof I> = reactive(new Set<keyof I>())
161160
162161
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
163162
return errs.map(err => ({
@@ -302,9 +301,9 @@ defineExpose<Form<S>>({
302301
loading,
303302
dirty: computed(() => !!dirtyFields.size),
304303
305-
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>,
306-
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>,
307-
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>>
304+
dirtyFields: readonly(dirtyFields),
305+
blurredFields: readonly(blurredFields),
306+
touchedFields: readonly(touchedFields)
308307
})
309308
</script>
310309

src/runtime/types/form.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ export interface Form<S extends FormSchema> {
1616
dirty: ComputedRef<boolean>
1717
loading: Ref<boolean>
1818

19-
dirtyFields: DeepReadonly<Set<keyof FormData<S, false>>>
20-
touchedFields: DeepReadonly<Set<keyof FormData<S, false>>>
21-
blurredFields: DeepReadonly<Set<keyof FormData<S, false>>>
19+
dirtyFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
20+
touchedFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
21+
blurredFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
2222
}
2323

2424
export type FormSchema<I extends object = object, O extends object = I> =

test/components/Form.spec.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { reactive, ref, nextTick } from 'vue'
1+
import { reactive, ref, nextTick, watch } from 'vue'
22
import { describe, it, expect, test, beforeEach, vi } from 'vitest'
33
import { mountSuspended } from '@nuxt/test-utils/runtime'
44
import * as z from 'zod'
@@ -304,6 +304,67 @@ describe('Form', () => {
304304
expect(form.value.blurredFields.has('email')).toBe(true)
305305
expect(form.value.blurredFields.has('password')).toBe(false)
306306
})
307+
308+
test('reactivity: touchedFields works on focus', async () => {
309+
const emailInput = wrapper.find('#emailInput')
310+
311+
const mockWatchCallback = vi.fn()
312+
watch(() => form.value.touchedFields, mockWatchCallback, { deep: true })
313+
314+
emailInput.trigger('focus')
315+
await flushPromises()
316+
expect(mockWatchCallback).toHaveBeenCalledTimes(1)
317+
expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
318+
expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
319+
})
320+
321+
test('reactivity: touchedFields works on change', async () => {
322+
const emailInput = wrapper.find('#emailInput')
323+
324+
const mockWatchCallback = vi.fn()
325+
watch(() => form.value.touchedFields, mockWatchCallback, { deep: true })
326+
327+
emailInput.trigger('change')
328+
await flushPromises()
329+
expect(mockWatchCallback).toHaveBeenCalledTimes(1)
330+
expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
331+
expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
332+
})
333+
334+
test('reactivity: blurredFields works', async () => {
335+
const emailInput = wrapper.find('#emailInput')
336+
337+
const mockWatchCallback = vi.fn()
338+
watch(() => form.value.blurredFields, mockWatchCallback, { deep: true })
339+
340+
emailInput.trigger('blur')
341+
await flushPromises()
342+
expect(mockWatchCallback).toHaveBeenCalledTimes(1)
343+
expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
344+
expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
345+
})
346+
347+
test('reactivity: dirtyFields works', async () => {
348+
const emailInput = wrapper.find('#emailInput')
349+
const mockWatchCallback = vi.fn()
350+
watch(() => form.value.dirtyFields, mockWatchCallback, { deep: true })
351+
352+
emailInput.trigger('change')
353+
await flushPromises()
354+
expect(mockWatchCallback).toHaveBeenCalledTimes(1)
355+
expect(mockWatchCallback.mock.calls[0][0].has('email')).toBe(true)
356+
expect(mockWatchCallback.mock.calls[0][0].has('password')).toBe(false)
357+
})
358+
359+
test('reactivity: dirty works', async () => {
360+
const emailInput = wrapper.find('#emailInput')
361+
expect(form.value.dirty).toBe(false)
362+
363+
emailInput.trigger('change')
364+
await flushPromises()
365+
366+
expect(form.value.dirty).toBe(true)
367+
})
307368
})
308369

309370
describe('nested', async () => {

0 commit comments

Comments
 (0)