mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
fix(Form): expose reactive fields (#4386)
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { DeepReadonly } from 'vue'
|
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import theme from '#build/ui/form'
|
import theme from '#build/ui/form'
|
||||||
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/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>
|
||||||
|
|
||||||
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
|
<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 { useEventBus } from '@vueuse/core'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
|
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 } }>({})
|
const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({})
|
||||||
provide(formInputsInjectionKey, inputs as any)
|
provide(formInputsInjectionKey, inputs as any)
|
||||||
|
|
||||||
const dirtyFields = new Set<keyof I>()
|
const dirtyFields: Set<keyof I> = reactive(new Set<keyof I>())
|
||||||
const touchedFields = new Set<keyof I>()
|
const touchedFields: Set<keyof I> = reactive(new Set<keyof I>())
|
||||||
const blurredFields = new Set<keyof I>()
|
const blurredFields: Set<keyof I> = reactive(new Set<keyof I>())
|
||||||
|
|
||||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||||
return errs.map(err => ({
|
return errs.map(err => ({
|
||||||
@@ -302,9 +301,9 @@ defineExpose<Form<S>>({
|
|||||||
loading,
|
loading,
|
||||||
dirty: computed(() => !!dirtyFields.size),
|
dirty: computed(() => !!dirtyFields.size),
|
||||||
|
|
||||||
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>,
|
dirtyFields: readonly(dirtyFields),
|
||||||
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>,
|
blurredFields: readonly(blurredFields),
|
||||||
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>>
|
touchedFields: readonly(touchedFields)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export interface Form<S extends FormSchema> {
|
|||||||
dirty: ComputedRef<boolean>
|
dirty: ComputedRef<boolean>
|
||||||
loading: Ref<boolean>
|
loading: Ref<boolean>
|
||||||
|
|
||||||
dirtyFields: DeepReadonly<Set<keyof FormData<S, false>>>
|
dirtyFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
|
||||||
touchedFields: DeepReadonly<Set<keyof FormData<S, false>>>
|
touchedFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
|
||||||
blurredFields: DeepReadonly<Set<keyof FormData<S, false>>>
|
blurredFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormSchema<I extends object = object, O extends object = I> =
|
export type FormSchema<I extends object = object, O extends object = I> =
|
||||||
|
|||||||
@@ -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 { describe, it, expect, test, beforeEach, vi } from 'vitest'
|
||||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
@@ -304,6 +304,67 @@ describe('Form', () => {
|
|||||||
expect(form.value.blurredFields.has('email')).toBe(true)
|
expect(form.value.blurredFields.has('email')).toBe(true)
|
||||||
expect(form.value.blurredFields.has('password')).toBe(false)
|
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 () => {
|
describe('nested', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user