homemanager-fe/src/components/form/FormField.vue

151 lines
3.9 KiB
Vue

<template>
<div class="flex flex-col space-y-1">
<label
:for="forId"
:class="[
'block text-sm font-medium transition-colors duration-200',
errors.length ? 'text-red-500' : 'text-gray-700',
]"
>{{ label }}
<span v-if="required" class="font-bold text-red-600">*</span></label
>
<slot
name="input"
:for-id="forId"
:type="type"
:id="forId"
:fieldName="name"
:fieldFQN="fieldName"
:value="value"
:placeholder="placeholder"
:invalid="!!errors?.length"
:onInput="onInput"
:onChange="onChange"
:onFocus="onFocus"
:onBlur="onBlur"
:setValue="setValue"
>
<input
:type="type"
:id="forId"
:name="name"
:value="value"
:class="inputClass"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:step="step"
:min="min"
:max="max"
@input="onInput"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
/>
</slot>
<slot />
<span
class="text-sm text-red-500"
v-for="error of errors"
aria-live="assertive"
>{{ error }}</span
>
</div>
</template>
<script setup lang="ts">
import set from 'lodash.set';
import get from 'lodash.get';
import { computed, onBeforeUnmount, onMounted, ref, Ref } from 'vue';
import { inject } from 'vue';
import { FormData, FormErrors, InputType } from './form.types';
const props = withDefaults(
defineProps<{
type?: InputType;
forIdPrefix?: string;
label: string;
name: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
placeholder?: string;
step?: string;
min?: string;
max?: string;
}>(),
{
type: 'text',
required: false,
disabled: false,
readonly: false,
}
);
const emit = defineEmits<{
(e: 'change', value: unknown): void;
(e: 'focus'): void;
(e: 'blur'): void;
}>();
const formData = inject<Ref<FormData>>('formData');
const formErrors = inject<Ref<FormErrors>>('formErrors');
const formGroup = inject<Ref<string>>('formGroup', ref(''));
const fieldChange = inject<(field: string) => void>('fieldChange');
const registerField = inject<(field: string) => void>('registerField');
const unregisterField = inject<(field: string) => void>('unregisterField');
const fieldName = computed(() =>
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
);
const forId = computed(
() => `${props.forIdPrefix || 'form'}-${fieldName.value}`
);
const value = computed(() => get(formData?.value, fieldName.value));
const errors = computed(() => formErrors?.value[fieldName.value] || []);
const onInput = (ev: Event) => {
if (!formData) return;
const value = (ev.target as HTMLInputElement).value;
const cleanValue = props.type === 'number' ? parseFloat(value) : value;
setValue(cleanValue);
};
const onChange = (ev: Event) => {
if (!formData) return;
if (props.type === 'checkbox' || props.type === 'radio') {
const cleanValue = (ev.target as HTMLInputElement).checked;
setValue(cleanValue);
}
};
const setValue = (value: unknown) => {
if (!formData) return;
set(formData.value, fieldName.value, value);
emit('change', value);
fieldChange?.(fieldName.value);
};
const onFocus = () => emit('focus');
const onBlur = () => {
emit('blur');
fieldChange?.(fieldName.value);
};
const inputClass = computed(() => {
return [
errors.value.length
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
props.type === 'checkbox' || props.type === 'radio' ? '' : 'w-full',
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
`mt-1 block rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
];
});
onMounted(() => registerField?.(fieldName.value));
onBeforeUnmount(() => unregisterField?.(fieldName.value));
</script>