Skip to content

feat(FormField): wrap error with Presence component to be animated #4147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: v3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { FormError } from '@nuxt/ui'

const state = reactive({
email: undefined,
password: undefined
})

const form = useTemplateRef('form')

const validate = (state: any): FormError[] => {
const errors = []
if (!state.email) errors.push({ name: 'email', message: 'Required' })
if (!state.password) errors.push({ name: 'password', message: 'Required' })
return errors
}
</script>

<template>
<UForm ref="form" :validate="validate" :state="state" class="space-y-4">
<UFormField
label="Email"
name="email"
:ui="{
error: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
}"
>
<UInput v-model="state.email" />
</UFormField>

<UFormField
label="Password"
name="password"
:ui="{
error: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
}"
>
<UInput v-model="state.password" type="password" />
</UFormField>

<div class="flex gap-2">
<UButton label="Submit" type="submit" color="neutral" />

<UButton label="Clear" color="neutral" variant="outline" @click="form?.clear()" />
</div>
</UForm>
</template>
30 changes: 30 additions & 0 deletions docs/content/3.components/form-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,36 @@ slots:
:u-input{placeholder="Enter your email" class="w-full"}
::

## Examples

### With error animation

You can animate the `error` slot by using the `ui` prop.

::component-example
---
collapse: true
name: 'form-field-error-animation-example'
---
::

::tip
You can also configure this globally in your `app.config.ts`:

```ts
export default defineAppConfig({
ui: {
formField: {
slots: {
error: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
}
}
}
})
```

::

## API

### Props
Expand Down
19 changes: 12 additions & 7 deletions src/runtime/components/FormField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface FormFieldSlots {

<script setup lang="ts">
import { computed, ref, inject, provide, type Ref, useId } from 'vue'
import { Primitive, Label } from 'reka-ui'
import { Primitive, Presence, Label } from 'reka-ui'
import { useAppConfig } from '#imports'
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'
import { tv } from '../utils/tv'
Expand All @@ -68,6 +68,8 @@ const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)

const error = computed(() => props.error || formErrors?.value?.find(error => error.name && (error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern))))?.message)

const hasError = computed(() => !!(typeof error.value === 'string' && error.value) || !!slots.error)

const id = ref(useId())
// Copies id's initial value to bind aria-attributes such as aria-describedby.
// This is required for the RadioGroup component which unsets the id value.
Expand Down Expand Up @@ -115,12 +117,15 @@ provide(formFieldInjectionKey, computed(() => ({
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
<slot :error="error" />

<div v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
<slot name="error" :error="error">
{{ error }}
</slot>
</div>
<div v-else-if="help || !!slots.help" :class="ui.help({ class: props.ui?.help })">
<Presence v-slot="{ present }" :present="hasError">
<div :id="`${ariaId}-error`" :data-state="present ? 'open' : 'closed'" :class="ui.error({ class: props.ui?.error })">
<slot name="error" :error="error">
{{ error }}
</slot>
</div>
</Presence>

<div v-if="!hasError && (help || !!slots.help)" :class="ui.help({ class: props.ui?.help })">
<slot name="help" :help="help">
{{ help }}
</slot>
Expand Down
36 changes: 30 additions & 6 deletions test/components/__snapshots__/Form-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`Form > custom validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -25,7 +26,8 @@ exports[`Form > custom validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<div id="v-1-error" class="mt-2 text-error">Must be at least 8 characters</div>
<div id="v-1-error" data-state="open" class="mt-2 text-error">Must be at least 8 characters</div>
<!--v-if-->
</div>
</div>
</form>"
Expand All @@ -43,6 +45,7 @@ exports[`Form > custom validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -56,6 +59,7 @@ exports[`Form > custom validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -74,6 +78,7 @@ exports[`Form > joi validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -87,7 +92,8 @@ exports[`Form > joi validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<div id="v-1-error" class="mt-2 text-error">Must be at least 8 characters</div>
<div id="v-1-error" data-state="open" class="mt-2 text-error">Must be at least 8 characters</div>
<!--v-if-->
</div>
</div>
</form>"
Expand All @@ -105,6 +111,7 @@ exports[`Form > joi validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -118,6 +125,7 @@ exports[`Form > joi validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -140,6 +148,7 @@ exports[`Form > superstruct validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -153,7 +162,8 @@ exports[`Form > superstruct validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<div id="v-1-error" class="mt-2 text-error">Must be at least 8 characters</div>
<div id="v-1-error" data-state="open" class="mt-2 text-error">Must be at least 8 characters</div>
<!--v-if-->
</div>
</div>
</form>"
Expand All @@ -171,6 +181,7 @@ exports[`Form > superstruct validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -184,6 +195,7 @@ exports[`Form > superstruct validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -202,6 +214,7 @@ exports[`Form > valibot validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -215,7 +228,8 @@ exports[`Form > valibot validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<div id="v-1-error" class="mt-2 text-error">Must be at least 8 characters</div>
<div id="v-1-error" data-state="open" class="mt-2 text-error">Must be at least 8 characters</div>
<!--v-if-->
</div>
</div>
</form>"
Expand All @@ -233,6 +247,7 @@ exports[`Form > valibot validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -246,6 +261,7 @@ exports[`Form > valibot validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -264,6 +280,7 @@ exports[`Form > yup validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -277,7 +294,8 @@ exports[`Form > yup validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<div id="v-1-error" class="mt-2 text-error">Must be at least 8 characters</div>
<div id="v-1-error" data-state="open" class="mt-2 text-error">Must be at least 8 characters</div>
<!--v-if-->
</div>
</div>
</form>"
Expand All @@ -295,6 +313,7 @@ exports[`Form > yup validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -308,6 +327,7 @@ exports[`Form > yup validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -326,6 +346,7 @@ exports[`Form > zod validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -339,7 +360,8 @@ exports[`Form > zod validation works > with error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<div id="v-1-error" class="mt-2 text-error">Must be at least 8 characters</div>
<div id="v-1-error" data-state="open" class="mt-2 text-error">Must be at least 8 characters</div>
<!--v-if-->
</div>
</div>
</form>"
Expand All @@ -357,6 +379,7 @@ exports[`Form > zod validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand All @@ -370,6 +393,7 @@ exports[`Form > zod validation works > without error 1`] = `
<!--v-if-->
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
Expand Down
Loading