mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-18 14:08:06 +01:00
feat(Form): improve form control and input validation trigger (#487)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
committed by
Benjamin Canac
parent
60bb74675c
commit
6d7973f6e1
@@ -34,7 +34,7 @@ import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -100,7 +100,8 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormChange, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
|
||||
const toggle = computed({
|
||||
get () {
|
||||
@@ -113,7 +114,7 @@ export default defineComponent({
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', event)
|
||||
emitFormBlur()
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
const inputClass = computed(() => {
|
||||
@@ -122,8 +123,8 @@ export default defineComponent({
|
||||
ui.value.rounded,
|
||||
ui.value.background,
|
||||
ui.value.border,
|
||||
ui.value.ring.replaceAll('{color}', props.color),
|
||||
ui.value.color.replaceAll('{color}', props.color)
|
||||
ui.value.ring.replaceAll('{color}', color.value),
|
||||
ui.value.color.replaceAll('{color}', color.value)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { provide, ref, type PropType, h, defineComponent } from 'vue'
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { provide, ref, type PropType, defineComponent } from 'vue'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
import type {
|
||||
ObjectSchema as YupObjectSchema,
|
||||
ValidationError as YupError
|
||||
} from 'yup'
|
||||
import type { FormError, FormEvent } from '../../types'
|
||||
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
|
||||
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, Form } from '../../types'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -26,21 +30,20 @@ export default defineComponent({
|
||||
| PropType<(state: any) => Promise<FormError[]>>
|
||||
| PropType<(state: any) => FormError[]>,
|
||||
default: () => []
|
||||
},
|
||||
validateOn: {
|
||||
type: Array as PropType<FormEventType[]>,
|
||||
default: () => ['blur', 'input', 'change', 'submit']
|
||||
}
|
||||
},
|
||||
setup (props, { slots, expose }) {
|
||||
emits: ['submit'],
|
||||
setup (props, { expose, emit }) {
|
||||
const seed = Math.random().toString(36).substring(7)
|
||||
const bus = useEventBus<FormEvent>(`form-${seed}`)
|
||||
|
||||
bus.on(async (event) => {
|
||||
if (event.type === 'blur') {
|
||||
const otherErrors = errors.value.filter(
|
||||
(error) => error.path !== event.path
|
||||
)
|
||||
const pathErrors = (await getErrors()).filter(
|
||||
(error) => error.path === event.path
|
||||
)
|
||||
errors.value = otherErrors.concat(pathErrors)
|
||||
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
|
||||
await validate(event.path, { silent: true })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,22 +69,67 @@ export default defineComponent({
|
||||
return errs
|
||||
}
|
||||
|
||||
async function validate () {
|
||||
errors.value = await getErrors()
|
||||
if (errors.value.length > 0) {
|
||||
async function validate (path?: string, opts: { silent?: boolean } = { silent: false }) {
|
||||
if (path) {
|
||||
const otherErrors = errors.value.filter(
|
||||
(error) => error.path !== path
|
||||
)
|
||||
const pathErrors = (await getErrors()).filter(
|
||||
(error) => error.path === path
|
||||
)
|
||||
errors.value = otherErrors.concat(pathErrors)
|
||||
} else {
|
||||
errors.value = await getErrors()
|
||||
}
|
||||
|
||||
if (!opts.silent && errors.value.length > 0) {
|
||||
throw new Error(
|
||||
`Form validation failed: ${JSON.stringify(errors.value, null, 2)}`
|
||||
)
|
||||
}
|
||||
|
||||
return props.state
|
||||
}
|
||||
|
||||
expose({
|
||||
validate
|
||||
})
|
||||
async function onSubmit (event: SubmitEvent) {
|
||||
if (props.validateOn?.includes('submit')) {
|
||||
await validate()
|
||||
}
|
||||
const submitEvent = event as FormSubmitEvent<any>
|
||||
submitEvent.data = props.state
|
||||
emit('submit', event)
|
||||
}
|
||||
|
||||
return () => h('form', slots.default?.())
|
||||
expose({
|
||||
validate,
|
||||
errors,
|
||||
setErrors (errs: FormError[], path?: string) {
|
||||
errors.value = errs
|
||||
if (path) {
|
||||
errors.value = errors.value.filter(
|
||||
(error) => error.path !== path
|
||||
).concat(errs)
|
||||
} else {
|
||||
errors.value = errs
|
||||
}
|
||||
},
|
||||
getErrors (path?: string) {
|
||||
if (path) {
|
||||
return errors.value.filter((err) => err.path === path)
|
||||
}
|
||||
return errors.value
|
||||
},
|
||||
clear (path?: string) {
|
||||
if (path) {
|
||||
errors.value = errors.value.filter((err) => err.path === path)
|
||||
} else {
|
||||
errors.value = []
|
||||
}
|
||||
}
|
||||
} as Form<any>)
|
||||
|
||||
return {
|
||||
onSubmit
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -156,3 +204,4 @@ async function getJoiErrors (
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,108 +0,0 @@
|
||||
import { h, cloneVNode, computed, defineComponent, provide, inject } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { getSlotsChildren } from '../../utils'
|
||||
import type { FormError } from '../../types'
|
||||
import { useAppConfig } from '#imports'
|
||||
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.formGroup.size).includes(value)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
error: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
|
||||
default: () => appConfig.ui.formGroup
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
|
||||
|
||||
provide('form-path', props.name)
|
||||
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
||||
const errorMessage = computed(() => {
|
||||
return props.error && typeof props.error === 'string'
|
||||
? props.error
|
||||
: formErrors?.value?.find((error) => error.path === props.name)?.message
|
||||
})
|
||||
|
||||
const children = computed(() => getSlotsChildren(slots))
|
||||
|
||||
const clones = computed(() => children.value.map((node) => {
|
||||
const vProps: any = {}
|
||||
|
||||
if (errorMessage.value) {
|
||||
vProps.oldColor = node.props?.color
|
||||
vProps.color = 'red'
|
||||
} else if (vProps.oldColor) {
|
||||
vProps.color = vProps.oldColor
|
||||
}
|
||||
|
||||
if (props.name) {
|
||||
vProps.name = props.name
|
||||
}
|
||||
|
||||
if (props.size) {
|
||||
vProps.size = props.size
|
||||
}
|
||||
|
||||
return cloneVNode(node, vProps)
|
||||
}))
|
||||
|
||||
const size = computed(() => ui.value.size[props.size ?? appConfig.ui.input.default.size])
|
||||
|
||||
return () => h('div', { class: [ui.value.wrapper] }, [
|
||||
props.label && h('div', { class: [ui.value.label.wrapper, size.value] }, [
|
||||
h('label', { for: props.name, class: [ui.value.label.base, props.required && ui.value.label.required] }, props.label),
|
||||
props.hint && h('span', { class: [ui.value.hint] }, props.hint)
|
||||
]),
|
||||
props.description && h('p', { class: [ui.value.description, size.value
|
||||
] }, props.description),
|
||||
h('div', { class: [!!props.label && ui.value.container] }, [
|
||||
...clones.value,
|
||||
errorMessage.value ? h('p', { class: [ui.value.error, size.value] }, errorMessage.value) : props.help ? h('p', { class: [ui.value.help, size.value] }, props.help) : null
|
||||
])
|
||||
])
|
||||
}
|
||||
})
|
||||
107
src/runtime/components/forms/FormGroup.vue
Normal file
107
src/runtime/components/forms/FormGroup.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<label>
|
||||
<div v-if="label" :class="[ui.label.wrapper, size]">
|
||||
<p :class="[ui.label.base, required ? ui.label.required : '']">{{ label }}</p>
|
||||
<span v-if="hint" :class="[ui.hint]">{{ hint }}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="description" :class="[ui.description, size]">{{ description }}</p>
|
||||
|
||||
<div :class="[label ? ui.container : '']">
|
||||
<slot v-bind="{ error }" />
|
||||
|
||||
<p v-if="error" :class="[ui.error, size]">{{ error }}</p>
|
||||
<p v-else-if="help" :class="[ui.help, size]">{{ help }}</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, provide, inject } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import type { FormError } from '../../types'
|
||||
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.formGroup.size).includes(value)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
error: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
|
||||
default: () => appConfig.ui.formGroup
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
|
||||
|
||||
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
||||
|
||||
const error = computed(() => {
|
||||
return (props.error && typeof props.error === 'string') || typeof props.error === 'boolean'
|
||||
? props.error
|
||||
: formErrors?.value?.find((error) => error.path === props.name)?.message
|
||||
})
|
||||
|
||||
const size = computed(() => ui.value.size[props.size ?? appConfig.ui.input.default.size])
|
||||
|
||||
provide('form-group', {
|
||||
error,
|
||||
name: computed(() => props.name),
|
||||
size: computed(() => props.size)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
size,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
error
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<input
|
||||
:id="name"
|
||||
ref="input"
|
||||
:name="name"
|
||||
:value="modelValue"
|
||||
@@ -36,7 +35,7 @@ import { ref, computed, onMounted, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
@@ -147,7 +146,9 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defu({}, props.ui, appConfig.ui.input))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
@@ -159,6 +160,7 @@ export default defineComponent({
|
||||
|
||||
const onInput = (event: InputEvent) => {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
emitFormInput()
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
@@ -173,17 +175,17 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
ui.value.placeholder,
|
||||
ui.value.size[props.size],
|
||||
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size]
|
||||
ui.value.size[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -215,15 +217,15 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.pointer,
|
||||
ui.value.icon.leading.padding[props.size]
|
||||
ui.value.icon.leading.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
@@ -232,15 +234,15 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.pointer,
|
||||
ui.value.icon.trailing.padding[props.size]
|
||||
ui.value.icon.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
:id="`${name}-${value}`"
|
||||
v-model="pick"
|
||||
:name="name"
|
||||
:required="required"
|
||||
@@ -12,7 +11,6 @@
|
||||
class="form-radio"
|
||||
:class="inputClass"
|
||||
v-bind="$attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ms-3 text-sm">
|
||||
@@ -32,7 +30,7 @@ import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -83,14 +81,15 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.radio
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
emits: ['update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormChange, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
|
||||
const pick = computed({
|
||||
get () {
|
||||
@@ -99,7 +98,7 @@ export default defineComponent({
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
if (value) {
|
||||
emitFormBlur()
|
||||
emitFormChange()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -109,8 +108,8 @@ export default defineComponent({
|
||||
ui.value.base,
|
||||
ui.value.background,
|
||||
ui.value.border,
|
||||
ui.value.ring.replaceAll('{color}', props.color),
|
||||
ui.value.color.replaceAll('{color}', props.color)
|
||||
ui.value.ring.replaceAll('{color}', color.value),
|
||||
ui.value.color.replaceAll('{color}', color.value)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<input
|
||||
:id="name"
|
||||
ref="input"
|
||||
v-model.number="value"
|
||||
:name="name"
|
||||
@@ -24,7 +23,7 @@ import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -83,7 +82,9 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defu({}, props.ui, appConfig.ui.range))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormChange, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||
|
||||
const value = computed({
|
||||
get () {
|
||||
@@ -96,13 +97,13 @@ export default defineComponent({
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', event)
|
||||
emitFormBlur()
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.wrapper,
|
||||
ui.value.size[props.size]
|
||||
ui.value.size[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -111,8 +112,8 @@ export default defineComponent({
|
||||
ui.value.base,
|
||||
ui.value.background,
|
||||
ui.value.rounded,
|
||||
ui.value.ring.replaceAll('{color}', props.color),
|
||||
ui.value.size[props.size]
|
||||
ui.value.ring.replaceAll('{color}', color.value),
|
||||
ui.value.size[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -120,10 +121,10 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.thumb.base,
|
||||
// Intermediate class to allow thumb ring or background color (set to `current`) as it's impossible to safelist with arbitrary values
|
||||
ui.value.thumb.color.replaceAll('{color}', props.color),
|
||||
ui.value.thumb.color.replaceAll('{color}', color.value),
|
||||
ui.value.thumb.ring,
|
||||
ui.value.thumb.background,
|
||||
ui.value.thumb.size[props.size]
|
||||
ui.value.thumb.size[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -132,7 +133,7 @@ export default defineComponent({
|
||||
ui.value.track.base,
|
||||
ui.value.track.background,
|
||||
ui.value.track.rounded,
|
||||
ui.value.track.size[props.size]
|
||||
ui.value.track.size[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -140,8 +141,8 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.progress.base,
|
||||
ui.value.progress.rounded,
|
||||
ui.value.progress.background.replaceAll('{color}', props.color),
|
||||
ui.value.progress.size[props.size]
|
||||
ui.value.progress.background.replaceAll('{color}', color.value),
|
||||
ui.value.progress.size[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<select
|
||||
:id="name"
|
||||
:name="name"
|
||||
:value="modelValue"
|
||||
:required="required"
|
||||
@@ -60,7 +59,7 @@ import { get } from 'lodash-es'
|
||||
import { defu } from 'defu'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -174,14 +173,17 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormChange, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||
|
||||
|
||||
const onInput = (event: InputEvent) => {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emitFormBlur()
|
||||
emitFormChange()
|
||||
emit('change', event)
|
||||
}
|
||||
|
||||
@@ -238,16 +240,16 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size]
|
||||
ui.value.size[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -279,15 +281,15 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.pointer,
|
||||
ui.value.icon.leading.padding[props.size]
|
||||
ui.value.icon.leading.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
@@ -296,15 +298,15 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.pointer,
|
||||
ui.value.icon.trailing.padding[props.size]
|
||||
ui.value.icon.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -135,7 +135,7 @@ import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
@@ -304,24 +304,26 @@ export default defineComponent({
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormBlur, emitFormChange, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
'text-left cursor-default',
|
||||
ui.value.size[props.size],
|
||||
ui.value.gap[props.size],
|
||||
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size],
|
||||
ui.value.size[size.value],
|
||||
ui.value.gap[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value],
|
||||
'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
@@ -354,15 +356,15 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.pointer,
|
||||
ui.value.icon.leading.padding[props.size]
|
||||
ui.value.icon.leading.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
@@ -371,15 +373,15 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.pointer,
|
||||
ui.value.icon.trailing.padding[props.size]
|
||||
ui.value.icon.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
@@ -423,7 +425,7 @@ export default defineComponent({
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
emitFormBlur()
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<textarea
|
||||
:id="name"
|
||||
ref="textarea"
|
||||
:value="modelValue"
|
||||
:name="name"
|
||||
@@ -23,7 +22,7 @@ import { ref, computed, watch, onMounted, nextTick, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -112,7 +111,9 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defu({}, props.ui, appConfig.ui.textarea))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
@@ -146,11 +147,12 @@ export default defineComponent({
|
||||
autoResize()
|
||||
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
emitFormInput()
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
emitFormBlur()
|
||||
emit('blur', event)
|
||||
emitFormBlur()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -171,15 +173,15 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const textareaClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
ui.value.placeholder,
|
||||
ui.value.size[props.size],
|
||||
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
ui.value.size[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
!props.resize && 'resize-none'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import { defu } from 'defu'
|
||||
import { Switch as HSwitch } from '@headlessui/vue'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -76,7 +76,8 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defu({}, props.ui, appConfig.ui.toggle))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
const { emitFormChange, formGroup } = useFormGroup()
|
||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||
|
||||
const active = computed({
|
||||
get () {
|
||||
@@ -84,7 +85,7 @@ export default defineComponent({
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
emitFormBlur()
|
||||
emitFormChange()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -92,20 +93,20 @@ export default defineComponent({
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
ui.value.ring.replaceAll('{color}', props.color),
|
||||
(active.value ? ui.value.active : ui.value.inactive).replaceAll('{color}', props.color)
|
||||
ui.value.ring.replaceAll('{color}', color.value),
|
||||
(active.value ? ui.value.active : ui.value.inactive).replaceAll('{color}', color.value)
|
||||
)
|
||||
})
|
||||
|
||||
const onIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.on.replaceAll('{color}', props.color)
|
||||
ui.value.icon.on.replaceAll('{color}', color.value)
|
||||
)
|
||||
})
|
||||
|
||||
const offIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.off.replaceAll('{color}', props.color)
|
||||
ui.value.icon.off.replaceAll('{color}', color.value)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user