124 lines
3.4 KiB
Vue
124 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
import type { FormErrors } from '~/types/error'
|
|
import { Input } from '~/components/pub/ui/input'
|
|
import { cn } from '~/lib/utils'
|
|
import { computed } from 'vue'
|
|
import { useFieldError } from 'vee-validate'
|
|
|
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
|
|
|
type InputType = 'text' | 'number' | 'password' | 'email' | 'date' | 'time' | 'datetime-local' | 'search' | 'tel'
|
|
|
|
const props = defineProps<{
|
|
fieldName: string
|
|
placeholder: string
|
|
label?: string
|
|
errors?: FormErrors
|
|
class?: string
|
|
colSpan?: number
|
|
numericOnly?: boolean
|
|
maxLength?: number
|
|
isRequired?: boolean
|
|
isDisabled?: boolean
|
|
rightLabel?: string
|
|
bottomLabel?: string
|
|
suffixMsg?: string
|
|
iconName?: string
|
|
inputType?: InputType
|
|
}>()
|
|
|
|
const { class: containerClass } = props
|
|
|
|
function handleInput(event: Event) {
|
|
const target = event.target as HTMLInputElement
|
|
let value = target.value
|
|
|
|
// Filter numeric only jika diperlukan
|
|
if (props.numericOnly) {
|
|
value = value.replace(/\D/g, '')
|
|
}
|
|
|
|
// Batasi panjang maksimal jika diperlukan
|
|
if (props.maxLength && value.length > props.maxLength) {
|
|
value = value.slice(0, props.maxLength)
|
|
}
|
|
|
|
// Update value jika ada perubahan
|
|
if (target.value !== value) {
|
|
target.value = value
|
|
// Trigger input event untuk update form
|
|
target.dispatchEvent(new Event('input', { bubbles: true }))
|
|
}
|
|
}
|
|
|
|
// Get error state from vee-validate
|
|
const fieldError = useFieldError(() => props.fieldName)
|
|
const hasError = computed(() => !!fieldError.value)
|
|
</script>
|
|
|
|
<template>
|
|
<DE.Cell :col-span="colSpan || 1">
|
|
<DE.Label
|
|
v-if="label"
|
|
:label-for="fieldName"
|
|
:is-required="isRequired && !isDisabled"
|
|
>
|
|
{{ label }}
|
|
</DE.Label>
|
|
<DE.Field
|
|
:id="fieldName"
|
|
:errors="errors"
|
|
>
|
|
<FormField
|
|
v-slot="{ componentField }"
|
|
:name="fieldName"
|
|
>
|
|
<FormItem>
|
|
<FormControl :class="cn('relative', containerClass)">
|
|
<div class="relative w-full max-w-sm items-center">
|
|
<Input
|
|
:disabled="isDisabled"
|
|
v-bind="componentField"
|
|
:placeholder="placeholder"
|
|
:maxlength="maxLength"
|
|
:class="cn(hasError && 'border-red-500 focus-visible:border-red-500 focus-visible:ring-red-500')"
|
|
autocomplete="off"
|
|
aria-autocomplete="none"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
@input="handleInput"
|
|
:type="inputType"
|
|
/>
|
|
<span
|
|
v-if="suffixMsg"
|
|
class="absolute inset-y-0 end-0 flex items-center justify-center px-2 text-muted-foreground"
|
|
>
|
|
{{ suffixMsg }}
|
|
</span>
|
|
<Icon
|
|
v-if="iconName"
|
|
:name="iconName"
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400"
|
|
/>
|
|
<p
|
|
v-show="rightLabel"
|
|
class="absolute right-3 top-0 text-gray-400"
|
|
>
|
|
{{ rightLabel }}
|
|
</p>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</DE.Field>
|
|
<p
|
|
v-show="bottomLabel"
|
|
class="text-gray-400"
|
|
>
|
|
{{ bottomLabel }}
|
|
</p>
|
|
</DE.Cell>
|
|
</template>
|