Add shared form fields for contact details
This commit is contained in:
112
app/components/form/FieldEmail.vue
Normal file
112
app/components/form/FieldEmail.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="form-control">
|
||||
<label v-if="label" class="label" :for="inputId">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
<span v-if="required" class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:value="modelValue"
|
||||
type="email"
|
||||
class="input input-bordered"
|
||||
:class="{ 'input-error': Boolean(errorMessage) }"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:name="name"
|
||||
:autocomplete="autocomplete"
|
||||
:pattern="EMAIL_INPUT_PATTERN"
|
||||
:aria-invalid="Boolean(errorMessage)"
|
||||
:aria-describedby="describedBy"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="(event) => emit('focus', event)"
|
||||
/>
|
||||
<p v-if="help" :id="helpId" class="mt-2 text-xs text-gray-500">
|
||||
{{ help }}
|
||||
</p>
|
||||
<p v-if="errorMessage" :id="errorId" class="mt-2 text-xs text-error">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useId } from 'vue'
|
||||
import { normalizeEmail } from '~/utils/formatters/email'
|
||||
import { EMAIL_INPUT_PATTERN, EMAIL_VALIDATION_ERROR, emailSchema } from '~/shared/validation/email'
|
||||
|
||||
type Emits = {
|
||||
(event: 'update:modelValue', value: string): void
|
||||
(event: 'blur', value: FocusEvent): void
|
||||
(event: 'focus', value: FocusEvent): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
label?: string
|
||||
required?: boolean
|
||||
error?: string | null
|
||||
help?: string | null
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
name?: string
|
||||
id?: string
|
||||
autocomplete?: string
|
||||
normalizeOnBlur?: boolean
|
||||
validateOnBlur?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
label: 'Email',
|
||||
required: false,
|
||||
error: null,
|
||||
help: null,
|
||||
placeholder: 'ex: contact@example.com',
|
||||
disabled: false,
|
||||
name: undefined,
|
||||
id: undefined,
|
||||
autocomplete: 'email',
|
||||
normalizeOnBlur: false,
|
||||
validateOnBlur: false,
|
||||
}
|
||||
)
|
||||
|
||||
const fallbackId = useId()
|
||||
const inputId = computed(() => props.id || fallbackId)
|
||||
const helpId = computed(() => (props.help ? `${inputId.value}-help` : undefined))
|
||||
const errorMessage = computed(() => props.error || null)
|
||||
const errorId = computed(() => (errorMessage.value ? `${inputId.value}-error` : undefined))
|
||||
const describedBy = computed(() => [helpId.value, errorId.value].filter(Boolean).join(' ') || undefined)
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
|
||||
if (props.normalizeOnBlur) {
|
||||
const normalized = normalizeEmail(target.value)
|
||||
if (normalized !== target.value) {
|
||||
target.value = normalized
|
||||
emit('update:modelValue', normalized)
|
||||
}
|
||||
}
|
||||
|
||||
if (props.validateOnBlur && !errorMessage.value) {
|
||||
const validation = emailSchema.validate(target.value)
|
||||
if (!validation.valid) {
|
||||
target.setCustomValidity(EMAIL_VALIDATION_ERROR)
|
||||
} else {
|
||||
target.setCustomValidity('')
|
||||
}
|
||||
}
|
||||
|
||||
emit('blur', event)
|
||||
}
|
||||
</script>
|
||||
113
app/components/form/FieldPhone.vue
Normal file
113
app/components/form/FieldPhone.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="form-control">
|
||||
<label v-if="label" class="label" :for="inputId">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
<span v-if="required" class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:value="modelValue"
|
||||
type="tel"
|
||||
class="input input-bordered"
|
||||
:class="{ 'input-error': Boolean(errorMessage) }"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:name="name"
|
||||
:autocomplete="autocomplete"
|
||||
:pattern="PHONE_INPUT_PATTERN"
|
||||
inputmode="tel"
|
||||
:aria-invalid="Boolean(errorMessage)"
|
||||
:aria-describedby="describedBy"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="(event) => emit('focus', event)"
|
||||
/>
|
||||
<p v-if="help" :id="helpId" class="mt-2 text-xs text-gray-500">
|
||||
{{ help }}
|
||||
</p>
|
||||
<p v-if="errorMessage" :id="errorId" class="mt-2 text-xs text-error">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useId } from 'vue'
|
||||
import { formatPhone } from '~/utils/formatters/phone'
|
||||
import { PHONE_INPUT_PATTERN, PHONE_VALIDATION_ERROR, phoneSchema } from '~/shared/validation/phone'
|
||||
|
||||
type Emits = {
|
||||
(event: 'update:modelValue', value: string): void
|
||||
(event: 'blur', value: FocusEvent): void
|
||||
(event: 'focus', value: FocusEvent): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
label?: string
|
||||
required?: boolean
|
||||
error?: string | null
|
||||
help?: string | null
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
name?: string
|
||||
id?: string
|
||||
autocomplete?: string
|
||||
normalizeOnBlur?: boolean
|
||||
validateOnBlur?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
label: 'Téléphone',
|
||||
required: false,
|
||||
error: null,
|
||||
help: null,
|
||||
placeholder: 'Ex: 06 00 00 00 00',
|
||||
disabled: false,
|
||||
name: undefined,
|
||||
id: undefined,
|
||||
autocomplete: 'tel',
|
||||
normalizeOnBlur: false,
|
||||
validateOnBlur: false,
|
||||
}
|
||||
)
|
||||
|
||||
const fallbackId = useId()
|
||||
const inputId = computed(() => props.id || fallbackId)
|
||||
const helpId = computed(() => (props.help ? `${inputId.value}-help` : undefined))
|
||||
const errorMessage = computed(() => props.error || null)
|
||||
const errorId = computed(() => (errorMessage.value ? `${inputId.value}-error` : undefined))
|
||||
const describedBy = computed(() => [helpId.value, errorId.value].filter(Boolean).join(' ') || undefined)
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
|
||||
if (props.normalizeOnBlur) {
|
||||
const formatted = formatPhone(target.value)
|
||||
if (formatted !== target.value) {
|
||||
target.value = formatted
|
||||
emit('update:modelValue', formatted)
|
||||
}
|
||||
}
|
||||
|
||||
if (props.validateOnBlur && !errorMessage.value) {
|
||||
const validation = phoneSchema.validate(target.value)
|
||||
if (!validation.valid) {
|
||||
target.setCustomValidity(PHONE_VALIDATION_ERROR)
|
||||
} else {
|
||||
target.setCustomValidity('')
|
||||
}
|
||||
}
|
||||
|
||||
emit('blur', event)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user