- Introduced CSS custom properties for spacing, typography, colors, and shadows. - Developed styles for form sections, grids, invoice items, and summary components. - Implemented responsive design adjustments for various screen sizes. - Added utility classes for text, spacing, and flex layouts. - Included dark mode and high contrast mode support. - Established loading states and validation/error styles. - Enhanced accessibility features with focus styles and screen reader utilities.
401 lines
9.3 KiB
JavaScript
401 lines
9.3 KiB
JavaScript
/**
|
|
* COMPOSABLE: useFormValidation
|
|
*
|
|
* Provides reusable form validation logic with real-time validation,
|
|
* error tracking, and field-level validation rules.
|
|
*
|
|
* Usage:
|
|
* ```js
|
|
* import { useFormValidation } from '@/composables/useFormValidation'
|
|
*
|
|
* const validation = useFormValidation()
|
|
*
|
|
* // Validate a field
|
|
* validation.validateField('email', form.email, {
|
|
* required: true,
|
|
* pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
* })
|
|
*
|
|
* // Check if field has error
|
|
* validation.hasError('email') // true/false
|
|
*
|
|
* // Get error message
|
|
* validation.getError('email') // "E-Mail ist erforderlich"
|
|
* ```
|
|
*/
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
export function useFormValidation() {
|
|
// State
|
|
const errors = ref({})
|
|
const touched = ref({})
|
|
const validating = ref({})
|
|
|
|
/**
|
|
* Validates a single field with given rules
|
|
* @param {string} fieldName - Name of the field
|
|
* @param {any} value - Current field value
|
|
* @param {object} rules - Validation rules
|
|
* @returns {boolean} - True if valid
|
|
*/
|
|
const validateField = (fieldName, value, rules = {}) => {
|
|
validating.value[fieldName] = true
|
|
const fieldErrors = []
|
|
|
|
// Required validation
|
|
if (rules.required) {
|
|
if (value === null || value === undefined || value === '' ||
|
|
(Array.isArray(value) && value.length === 0)) {
|
|
fieldErrors.push('Dieses Feld ist erforderlich')
|
|
}
|
|
}
|
|
|
|
// If field is empty and not required, skip other validations
|
|
if (!value && !rules.required) {
|
|
errors.value[fieldName] = []
|
|
validating.value[fieldName] = false
|
|
return true
|
|
}
|
|
|
|
// Min length validation
|
|
if (rules.minLength && value && value.length < rules.minLength) {
|
|
fieldErrors.push(`Mindestens ${rules.minLength} Zeichen erforderlich`)
|
|
}
|
|
|
|
// Max length validation
|
|
if (rules.maxLength && value && value.length > rules.maxLength) {
|
|
fieldErrors.push(`Maximal ${rules.maxLength} Zeichen erlaubt`)
|
|
}
|
|
|
|
// Pattern validation
|
|
if (rules.pattern && value && !rules.pattern.test(value)) {
|
|
fieldErrors.push(rules.patternMessage || 'Ungültiges Format')
|
|
}
|
|
|
|
// Min value validation (for numbers)
|
|
if (rules.min !== undefined && value < rules.min) {
|
|
fieldErrors.push(`Wert muss mindestens ${rules.min} sein`)
|
|
}
|
|
|
|
// Max value validation (for numbers)
|
|
if (rules.max !== undefined && value > rules.max) {
|
|
fieldErrors.push(`Wert darf maximal ${rules.max} sein`)
|
|
}
|
|
|
|
// Email validation
|
|
if (rules.email && value) {
|
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
if (!emailPattern.test(value)) {
|
|
fieldErrors.push('Ungültige E-Mail-Adresse')
|
|
}
|
|
}
|
|
|
|
// URL validation
|
|
if (rules.url && value) {
|
|
try {
|
|
new URL(value)
|
|
} catch {
|
|
fieldErrors.push('Ungültige URL')
|
|
}
|
|
}
|
|
|
|
// Date validation
|
|
if (rules.date && value) {
|
|
const date = new Date(value)
|
|
if (isNaN(date.getTime())) {
|
|
fieldErrors.push('Ungültiges Datum')
|
|
}
|
|
}
|
|
|
|
// Custom validation function
|
|
if (rules.custom && typeof rules.custom === 'function') {
|
|
const customError = rules.custom(value)
|
|
if (customError) {
|
|
fieldErrors.push(customError)
|
|
}
|
|
}
|
|
|
|
// Async validation
|
|
if (rules.asyncValidator && typeof rules.asyncValidator === 'function') {
|
|
rules.asyncValidator(value).then(error => {
|
|
if (error) {
|
|
errors.value[fieldName] = [error]
|
|
}
|
|
validating.value[fieldName] = false
|
|
})
|
|
} else {
|
|
validating.value[fieldName] = false
|
|
}
|
|
|
|
// Update errors
|
|
errors.value[fieldName] = fieldErrors
|
|
|
|
return fieldErrors.length === 0
|
|
}
|
|
|
|
/**
|
|
* Validates multiple fields at once
|
|
* @param {object} formData - Object with field names and values
|
|
* @param {object} rulesMap - Object with field names and their rules
|
|
* @returns {boolean} - True if all fields are valid
|
|
*/
|
|
const validateForm = (formData, rulesMap) => {
|
|
let isValid = true
|
|
|
|
Object.keys(rulesMap).forEach(fieldName => {
|
|
const value = getNestedValue(formData, fieldName)
|
|
const rules = rulesMap[fieldName]
|
|
const fieldValid = validateField(fieldName, value, rules)
|
|
|
|
if (!fieldValid) {
|
|
isValid = false
|
|
touchField(fieldName)
|
|
}
|
|
})
|
|
|
|
return isValid
|
|
}
|
|
|
|
/**
|
|
* Marks a field as touched
|
|
* @param {string} fieldName - Name of the field
|
|
*/
|
|
const touchField = (fieldName) => {
|
|
touched.value[fieldName] = true
|
|
}
|
|
|
|
/**
|
|
* Marks multiple fields as touched
|
|
* @param {string[]} fieldNames - Array of field names
|
|
*/
|
|
const touchFields = (fieldNames) => {
|
|
fieldNames.forEach(fieldName => {
|
|
touched.value[fieldName] = true
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Marks all fields as touched
|
|
*/
|
|
const touchAll = () => {
|
|
Object.keys(errors.value).forEach(fieldName => {
|
|
touched.value[fieldName] = true
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Checks if a field has an error and has been touched
|
|
* @param {string} fieldName - Name of the field
|
|
* @returns {boolean}
|
|
*/
|
|
const hasError = (fieldName) => {
|
|
return touched.value[fieldName] &&
|
|
errors.value[fieldName] &&
|
|
errors.value[fieldName].length > 0
|
|
}
|
|
|
|
/**
|
|
* Gets the first error message for a field
|
|
* @param {string} fieldName - Name of the field
|
|
* @returns {string|null}
|
|
*/
|
|
const getError = (fieldName) => {
|
|
if (!hasError(fieldName)) return null
|
|
return errors.value[fieldName][0]
|
|
}
|
|
|
|
/**
|
|
* Gets all error messages for a field
|
|
* @param {string} fieldName - Name of the field
|
|
* @returns {string[]}
|
|
*/
|
|
const getErrors = (fieldName) => {
|
|
if (!hasError(fieldName)) return []
|
|
return errors.value[fieldName]
|
|
}
|
|
|
|
/**
|
|
* Clears errors for a specific field
|
|
* @param {string} fieldName - Name of the field
|
|
*/
|
|
const clearFieldError = (fieldName) => {
|
|
errors.value[fieldName] = []
|
|
touched.value[fieldName] = false
|
|
}
|
|
|
|
/**
|
|
* Clears all errors
|
|
*/
|
|
const clearErrors = () => {
|
|
errors.value = {}
|
|
touched.value = {}
|
|
}
|
|
|
|
/**
|
|
* Resets validation state
|
|
*/
|
|
const reset = () => {
|
|
errors.value = {}
|
|
touched.value = {}
|
|
validating.value = {}
|
|
}
|
|
|
|
/**
|
|
* Checks if the entire form is valid
|
|
* @returns {boolean}
|
|
*/
|
|
const isValid = computed(() => {
|
|
return Object.keys(errors.value).every(
|
|
fieldName => !errors.value[fieldName] || errors.value[fieldName].length === 0
|
|
)
|
|
})
|
|
|
|
/**
|
|
* Checks if any field is currently being validated
|
|
* @returns {boolean}
|
|
*/
|
|
const isValidating = computed(() => {
|
|
return Object.values(validating.value).some(v => v === true)
|
|
})
|
|
|
|
/**
|
|
* Gets nested value from object using dot notation
|
|
* @param {object} obj - Object to get value from
|
|
* @param {string} path - Path using dot notation (e.g., 'items.0.description')
|
|
* @returns {any}
|
|
*/
|
|
const getNestedValue = (obj, path) => {
|
|
return path.split('.').reduce((acc, part) => {
|
|
return acc && acc[part] !== undefined ? acc[part] : undefined
|
|
}, obj)
|
|
}
|
|
|
|
/**
|
|
* Sets a custom error for a field
|
|
* @param {string} fieldName - Name of the field
|
|
* @param {string|string[]} error - Error message(s)
|
|
*/
|
|
const setError = (fieldName, error) => {
|
|
errors.value[fieldName] = Array.isArray(error) ? error : [error]
|
|
touched.value[fieldName] = true
|
|
}
|
|
|
|
/**
|
|
* Sets multiple errors at once
|
|
* @param {object} errorsMap - Object with field names and error messages
|
|
*/
|
|
const setErrors = (errorsMap) => {
|
|
Object.entries(errorsMap).forEach(([fieldName, error]) => {
|
|
setError(fieldName, error)
|
|
})
|
|
}
|
|
|
|
return {
|
|
// State
|
|
errors,
|
|
touched,
|
|
validating,
|
|
|
|
// Methods
|
|
validateField,
|
|
validateForm,
|
|
touchField,
|
|
touchFields,
|
|
touchAll,
|
|
hasError,
|
|
getError,
|
|
getErrors,
|
|
clearFieldError,
|
|
clearErrors,
|
|
reset,
|
|
setError,
|
|
setErrors,
|
|
|
|
// Computed
|
|
isValid,
|
|
isValidating
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Common validation rules presets
|
|
*/
|
|
export const validationRules = {
|
|
required: {
|
|
required: true
|
|
},
|
|
|
|
email: {
|
|
required: true,
|
|
email: true
|
|
},
|
|
|
|
optionalEmail: {
|
|
email: true
|
|
},
|
|
|
|
phone: {
|
|
pattern: /^[\d\s()+\-/.]+$/,
|
|
patternMessage: 'Ungültige Telefonnummer'
|
|
},
|
|
|
|
url: {
|
|
url: true
|
|
},
|
|
|
|
invoiceNumber: {
|
|
required: true,
|
|
minLength: 3,
|
|
pattern: /^[A-Z0-9-]+$/i,
|
|
patternMessage: 'Nur Buchstaben, Zahlen und Bindestriche erlaubt'
|
|
},
|
|
|
|
postalCode: {
|
|
pattern: /^\d{5}$/,
|
|
patternMessage: 'Postleitzahl muss 5 Zahlen enthalten'
|
|
},
|
|
|
|
iban: {
|
|
pattern: /^[A-Z]{2}\d{2}[A-Z0-9]+$/,
|
|
patternMessage: 'Ungültiges IBAN-Format'
|
|
},
|
|
|
|
taxId: {
|
|
pattern: /^DE\d{9}$/,
|
|
patternMessage: 'Ungültige Steuernummer (Format: DE123456789)'
|
|
},
|
|
|
|
currency: {
|
|
min: 0,
|
|
custom: (value) => {
|
|
if (value && isNaN(parseFloat(value))) {
|
|
return 'Bitte geben Sie einen gültigen Betrag ein'
|
|
}
|
|
return null
|
|
}
|
|
},
|
|
|
|
percentage: {
|
|
min: 0,
|
|
max: 100,
|
|
custom: (value) => {
|
|
if (value && (isNaN(parseFloat(value)) || value < 0 || value > 100)) {
|
|
return 'Prozentsatz muss zwischen 0 und 100 liegen'
|
|
}
|
|
return null
|
|
}
|
|
},
|
|
|
|
quantity: {
|
|
required: true,
|
|
min: 0.01,
|
|
custom: (value) => {
|
|
if (value && (isNaN(parseFloat(value)) || value <= 0)) {
|
|
return 'Menge muss größer als 0 sein'
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
}
|