feat(Input/Textarea): add default-value prop (#4404)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Romain Hamel
2025-06-30 15:04:52 +02:00
committed by GitHub
parent 69a7b957d5
commit fb9e7bb856
2 changed files with 28 additions and 23 deletions

View File

@@ -8,7 +8,7 @@ import type { AcceptableValue, ComponentConfig } from '../types/utils'
type Input = ComponentConfig<typeof theme, AppConfig, 'input'> type Input = ComponentConfig<typeof theme, AppConfig, 'input'>
export interface InputProps extends UseComponentIconsProps { export interface InputProps<T extends AcceptableValue = AcceptableValue> extends UseComponentIconsProps {
/** /**
* The element or component this component should render as. * The element or component this component should render as.
* @defaultValue 'div' * @defaultValue 'div'
@@ -38,6 +38,8 @@ export interface InputProps extends UseComponentIconsProps {
disabled?: boolean disabled?: boolean
/** Highlight the ring color like a focus state. */ /** Highlight the ring color like a focus state. */
highlight?: boolean highlight?: boolean
modelValue?: T
defaultValue?: T
modelModifiers?: { modelModifiers?: {
string?: boolean string?: boolean
number?: boolean number?: boolean
@@ -65,6 +67,7 @@ export interface InputSlots {
<script setup lang="ts" generic="T extends AcceptableValue"> <script setup lang="ts" generic="T extends AcceptableValue">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { Primitive } from 'reka-ui' import { Primitive } from 'reka-ui'
import { useVModel } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup' import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons' import { useComponentIcons } from '../composables/useComponentIcons'
@@ -76,7 +79,7 @@ import UAvatar from './Avatar.vue'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<InputProps>(), { const props = withDefaults(defineProps<InputProps<T>>(), {
type: 'text', type: 'text',
autocomplete: 'off', autocomplete: 'off',
autofocusDelay: 0 autofocusDelay: 0
@@ -84,13 +87,12 @@ const props = withDefaults(defineProps<InputProps>(), {
const emits = defineEmits<InputEmits<T>>() const emits = defineEmits<InputEmits<T>>()
const slots = defineSlots<InputSlots>() const slots = defineSlots<InputSlots>()
// eslint-disable-next-line vue/no-dupe-keys const modelValue = useVModel<InputProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue })
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Input['AppConfig'] const appConfig = useAppConfig() as Input['AppConfig']
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true }) const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps<T>>(props, { deferInputValidation: true })
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props) const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps<T>>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
@@ -111,15 +113,15 @@ const inputRef = ref<HTMLInputElement | null>(null)
// Custom function to handle the v-model properties // Custom function to handle the v-model properties
function updateInput(value: string | null) { function updateInput(value: string | null) {
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
value = value?.trim() ?? null value = value?.trim() ?? null
} }
if (modelModifiers.number || props.type === 'number') { if (props.modelModifiers?.number || props.type === 'number') {
value = looseToNumber(value) value = looseToNumber(value)
} }
if (modelModifiers.nullify) { if (props.modelModifiers?.nullify) {
value ||= null value ||= null
} }
@@ -128,7 +130,7 @@ function updateInput(value: string | null) {
} }
function onInput(event: Event) { function onInput(event: Event) {
if (!modelModifiers.lazy) { if (!props.modelModifiers?.lazy) {
updateInput((event.target as HTMLInputElement).value) updateInput((event.target as HTMLInputElement).value)
} }
} }
@@ -136,12 +138,12 @@ function onInput(event: Event) {
function onChange(event: Event) { function onChange(event: Event) {
const value = (event.target as HTMLInputElement).value const value = (event.target as HTMLInputElement).value
if (modelModifiers.lazy) { if (props.modelModifiers?.lazy) {
updateInput(value) updateInput(value)
} }
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63 // Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
(event.target as HTMLInputElement).value = value.trim() (event.target as HTMLInputElement).value = value.trim()
} }

View File

@@ -9,7 +9,7 @@ type Textarea = ComponentConfig<typeof theme, AppConfig, 'textarea'>
type TextareaValue = string | number | null type TextareaValue = string | number | null
export interface TextareaProps extends UseComponentIconsProps { export interface TextareaProps<T extends TextareaValue = TextareaValue> extends UseComponentIconsProps {
/** /**
* The element or component this component should render as. * The element or component this component should render as.
* @defaultValue 'div' * @defaultValue 'div'
@@ -41,8 +41,11 @@ export interface TextareaProps extends UseComponentIconsProps {
maxrows?: number maxrows?: number
/** Highlight the ring color like a focus state. */ /** Highlight the ring color like a focus state. */
highlight?: boolean highlight?: boolean
modelValue?: T
defaultValue?: T
modelModifiers?: { modelModifiers?: {
string?: boolean string?: boolean
number?: boolean
trim?: boolean trim?: boolean
lazy?: boolean lazy?: boolean
nullify?: boolean nullify?: boolean
@@ -67,6 +70,7 @@ export interface TextareaSlots {
<script setup lang="ts" generic="T extends TextareaValue"> <script setup lang="ts" generic="T extends TextareaValue">
import { ref, computed, onMounted, nextTick, watch } from 'vue' import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { Primitive } from 'reka-ui' import { Primitive } from 'reka-ui'
import { useVModel } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { useComponentIcons } from '../composables/useComponentIcons' import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField' import { useFormField } from '../composables/useFormField'
@@ -77,7 +81,7 @@ import UAvatar from './Avatar.vue'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<TextareaProps>(), { const props = withDefaults(defineProps<TextareaProps<T>>(), {
rows: 3, rows: 3,
maxrows: 0, maxrows: 0,
autofocusDelay: 0, autofocusDelay: 0,
@@ -86,12 +90,11 @@ const props = withDefaults(defineProps<TextareaProps>(), {
const emits = defineEmits<TextareaEmits<T>>() const emits = defineEmits<TextareaEmits<T>>()
const slots = defineSlots<TextareaSlots>() const slots = defineSlots<TextareaSlots>()
// eslint-disable-next-line vue/no-dupe-keys const modelValue = useVModel<TextareaProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue })
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Textarea['AppConfig'] const appConfig = useAppConfig() as Textarea['AppConfig']
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true }) const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps<T>>(props, { deferInputValidation: true })
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({
@@ -109,15 +112,15 @@ const textareaRef = ref<HTMLTextAreaElement | null>(null)
// Custom function to handle the v-model properties // Custom function to handle the v-model properties
function updateInput(value: string | null) { function updateInput(value: string | null) {
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
value = value?.trim() ?? null value = value?.trim() ?? null
} }
if (modelModifiers.number) { if (props.modelModifiers?.number) {
value = looseToNumber(value) value = looseToNumber(value)
} }
if (modelModifiers.nullify) { if (props.modelModifiers?.nullify) {
value ||= null value ||= null
} }
@@ -128,7 +131,7 @@ function updateInput(value: string | null) {
function onInput(event: Event) { function onInput(event: Event) {
autoResize() autoResize()
if (!modelModifiers.lazy) { if (!props.modelModifiers?.lazy) {
updateInput((event.target as HTMLInputElement).value) updateInput((event.target as HTMLInputElement).value)
} }
} }
@@ -136,12 +139,12 @@ function onInput(event: Event) {
function onChange(event: Event) { function onChange(event: Event) {
const value = (event.target as HTMLInputElement).value const value = (event.target as HTMLInputElement).value
if (modelModifiers.lazy) { if (props.modelModifiers?.lazy) {
updateInput(value) updateInput(value)
} }
// Update trimmed textarea so that it has same behavior as native textarea https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63 // Update trimmed textarea so that it has same behavior as native textarea https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
(event.target as HTMLInputElement).value = value.trim() (event.target as HTMLInputElement).value = value.trim()
} }