@senka-ai/ui
Version:
A modern, type-safe Svelte 5 UI component library with full theme support, accessibility standards, and robust state management patterns
314 lines (313 loc) • 9.17 kB
JavaScript
/**
* Unified form validation architecture for Senka components
* Provides reusable validation logic and state management
*/
/**
* Validates a single value against an array of validation rules
*/
export function validateValue(value, rules = []) {
const errors = [];
for (const rule of rules) {
if (!rule.validator(value)) {
errors.push(rule.message);
}
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Common validation rules for educational app forms
*/
export const validationRules = {
/**
* Validates required fields
*/
required: (message = 'This field is required') => ({
message,
validator: (value) => {
if (typeof value === 'string')
return value.trim().length > 0;
if (Array.isArray(value))
return value.length > 0;
return value != null && value !== undefined;
},
}),
/**
* Validates minimum length for strings
*/
minLength: (min, message) => ({
message: message || `Must be at least ${min} characters`,
validator: (value) => typeof value === 'string' && value.length >= min,
}),
/**
* Validates maximum length for strings
*/
maxLength: (max, message) => ({
message: message || `Must be no more than ${max} characters`,
validator: (value) => typeof value === 'string' && value.length <= max,
}),
/**
* Validates email format
*/
email: (message = 'Please enter a valid email address') => ({
message,
validator: (value) => {
if (typeof value !== 'string')
return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
},
}),
/**
* Validates numeric values
*/
numeric: (message = 'Please enter a valid number') => ({
message,
validator: (value) => {
if (typeof value === 'number')
return !isNaN(value);
if (typeof value === 'string')
return !isNaN(Number(value));
return false;
},
}),
/**
* Validates minimum value for numbers
*/
min: (minValue, message) => ({
message: message || `Must be at least ${minValue}`,
validator: (value) => {
const num = typeof value === 'string' ? Number(value) : value;
return !isNaN(num) && num >= minValue;
},
}),
/**
* Validates maximum value for numbers
*/
max: (maxValue, message) => ({
message: message || `Must be no more than ${maxValue}`,
validator: (value) => {
const num = typeof value === 'string' ? Number(value) : value;
return !isNaN(num) && num <= maxValue;
},
}),
/**
* Validates patterns using regex
*/
pattern: (regex, message = 'Invalid format') => ({
message,
validator: (value) => typeof value === 'string' && regex.test(value),
}),
/**
* Validates Romanian educational ID format (CNP)
*/
romanianCNP: (message = 'Please enter a valid CNP') => ({
message,
validator: (value) => {
if (typeof value !== 'string')
return false;
// Basic CNP validation: 13 digits starting with valid month/gender code
const cnpRegex = /^[1-8]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{6}$/;
return cnpRegex.test(value);
},
}),
};
/**
* Creates a reactive form validation state manager
*/
export function useFormValidation(initialValues, validationSchema = {}) {
// Initialize form state
let formState = $state({
fields: {},
isValid: true,
isDirty: false,
isSubmitting: false,
});
// Initialize field states
for (const key in initialValues) {
formState.fields[key] = {
value: initialValues[key],
isDirty: false,
isTouched: false,
errors: [],
isValid: true,
};
}
/**
* Updates a field value and validates it
*/
function updateField(fieldName, value, touch = true) {
const field = formState.fields[fieldName];
if (!field)
return;
field.value = value;
field.isDirty = true;
if (touch)
field.isTouched = true;
// Validate the field
const rules = validationSchema[fieldName] || [];
const validation = validateValue(value, rules);
field.errors = validation.errors;
field.isValid = validation.isValid;
// Update form-level state
updateFormState();
}
/**
* Marks a field as touched (for blur events)
*/
function touchField(fieldName) {
const field = formState.fields[fieldName];
if (field) {
field.isTouched = true;
}
}
/**
* Validates all fields and updates form state
*/
function validateForm() {
for (const fieldName in formState.fields) {
const field = formState.fields[fieldName];
const rules = validationSchema[fieldName] || [];
const validation = validateValue(field.value, rules);
field.errors = validation.errors;
field.isValid = validation.isValid;
field.isTouched = true;
}
updateFormState();
return formState.isValid;
}
/**
* Updates form-level validation state
*/
function updateFormState() {
const fields = Object.values(formState.fields);
formState.isValid = fields.every((field) => field.isValid);
formState.isDirty = fields.some((field) => field.isDirty);
}
/**
* Resets the form to initial values
*/
function resetForm() {
for (const key in initialValues) {
const field = formState.fields[key];
if (field) {
field.value = initialValues[key];
field.isDirty = false;
field.isTouched = false;
field.errors = [];
field.isValid = true;
}
}
formState.isSubmitting = false;
updateFormState();
}
/**
* Sets form submission state
*/
function setSubmitting(isSubmitting) {
formState.isSubmitting = isSubmitting;
}
/**
* Gets the current form values
*/
function getFormValues() {
const values = {};
for (const key in formState.fields) {
values[key] = formState.fields[key].value;
}
return values;
}
/**
* Gets field helper props for form components
*/
function getFieldProps(fieldName) {
const field = formState.fields[fieldName];
if (!field)
return {};
return {
value: field.value,
error: field.isTouched && field.errors.length > 0 ? field.errors[0] : undefined,
onchange: (value) => updateField(fieldName, value),
onblur: () => touchField(fieldName),
};
}
return {
// State access
get formState() {
return formState;
},
get isValid() {
return formState.isValid;
},
get isDirty() {
return formState.isDirty;
},
get isSubmitting() {
return formState.isSubmitting;
},
// Actions
updateField,
touchField,
validateForm,
resetForm,
setSubmitting,
getFormValues,
getFieldProps,
};
}
/**
* Simplified validation hook for individual form fields
*/
export function useFieldValidation(initialValue, rules = []) {
let fieldState = $state({
value: initialValue,
isDirty: false,
isTouched: false,
errors: [],
isValid: true,
});
function updateValue(value, touch = true) {
fieldState.value = value;
fieldState.isDirty = true;
if (touch)
fieldState.isTouched = true;
const validation = validateValue(value, rules);
fieldState.errors = validation.errors;
fieldState.isValid = validation.isValid;
}
function touch() {
fieldState.isTouched = true;
}
function reset() {
fieldState.value = initialValue;
fieldState.isDirty = false;
fieldState.isTouched = false;
fieldState.errors = [];
fieldState.isValid = true;
}
return {
get state() {
return fieldState;
},
get value() {
return fieldState.value;
},
get error() {
return fieldState.isTouched && fieldState.errors.length > 0 ? fieldState.errors[0] : undefined;
},
get isValid() {
return fieldState.isValid;
},
get isDirty() {
return fieldState.isDirty;
},
get isTouched() {
return fieldState.isTouched;
},
updateValue,
touch,
reset,
};
}