myCRM/docs/design/composables/useFormValidation.js
olli 82b022ba3b Add comprehensive styles for invoice form components and utilities
- 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.
2025-12-13 10:02:30 +01:00

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
}
}
}