fix(Form): expose reactive fields (#4386)

This commit is contained in:
Jack Bobakanoosh
2025-06-24 10:56:12 -05:00
committed by GitHub
parent 1d281e915a
commit 1a8feb751e
3 changed files with 72 additions and 12 deletions

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
@@ -64,7 +63,7 @@ export interface FormSlots {
</script>
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly, reactive } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
@@ -155,9 +154,9 @@ provide('form-errors', errors)
const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({})
provide(formInputsInjectionKey, inputs as any)
const dirtyFields = new Set<keyof I>()
const touchedFields = new Set<keyof I>()
const blurredFields = new Set<keyof I>()
const dirtyFields: Set<keyof I> = reactive(new Set<keyof I>())
const touchedFields: Set<keyof I> = reactive(new Set<keyof I>())
const blurredFields: Set<keyof I> = reactive(new Set<keyof I>())
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({
@@ -302,9 +301,9 @@ defineExpose<Form<S>>({
loading,
dirty: computed(() => !!dirtyFields.size),
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>,
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>,
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>>
dirtyFields: readonly(dirtyFields),
blurredFields: readonly(blurredFields),
touchedFields: readonly(touchedFields)
})
</script>

View File

@@ -16,9 +16,9 @@ export interface Form<S extends FormSchema> {
dirty: ComputedRef<boolean>
loading: Ref<boolean>
dirtyFields: DeepReadonly<Set<keyof FormData<S, false>>>
touchedFields: DeepReadonly<Set<keyof FormData<S, false>>>
blurredFields: DeepReadonly<Set<keyof FormData<S, false>>>
dirtyFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
touchedFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
blurredFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
}
export type FormSchema<I extends object = object, O extends object = I> =

View File

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