@rxxuzi/gumi
Version:
Clean & minimal design system with delightful interactions
268 lines • 9.15 kB
JavaScript
// components/form.ts
// Gumi.js v1.0.0 - Form Validation
import { $, on, addClass, removeClass } from '../core/dom';
import { createElement } from '../core/dom';
export class FormValidator {
constructor(form, options = {}) {
this.errors = new Map();
const el = $(form);
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error('Form element not found');
}
this.form = el;
this.options = {
showErrors: true,
validateOnChange: true,
...options
};
this.init();
}
/**
* Initialize form validator
*/
init() {
// Prevent default HTML5 validation
this.form.noValidate = true;
// Add submit handler
on(this.form, 'submit', (e) => {
e.preventDefault();
if (this.validate()) {
// Form is valid, trigger custom event
const submitEvent = new CustomEvent('valid-submit', {
detail: { form: this.form },
cancelable: true
});
if (this.form.dispatchEvent(submitEvent)) {
// Only submit if event wasn't prevented
if (!this.options.rules) {
this.form.submit();
}
}
}
});
// Add change/input handlers if enabled
if (this.options.validateOnChange) {
const inputs = this.getInputs();
inputs.forEach(input => {
on(input, 'blur', () => this.validateField(input));
on(input, 'input', () => {
if (this.errors.has(input.name)) {
this.validateField(input);
}
});
});
}
}
/**
* Get all form inputs
*/
getInputs() {
return Array.from(this.form.querySelectorAll('input, textarea, select'));
}
/**
* Validate entire form
*/
validate() {
this.clearErrors();
const inputs = this.getInputs();
let isValid = true;
inputs.forEach(input => {
if (!this.validateField(input)) {
isValid = false;
}
});
return isValid;
}
/**
* Validate single field
*/
validateField(input) {
const name = input.name;
const value = input.value.trim();
const type = input.type;
// Clear previous error
this.clearFieldError(input);
// Check HTML5 validation attributes
if (input.hasAttribute('required') && !value) {
this.setFieldError(input, 'This field is required');
return false;
}
if (input.hasAttribute('minlength')) {
const minLength = parseInt(input.getAttribute('minlength') || '0');
if (value.length < minLength) {
this.setFieldError(input, `Minimum length is ${minLength} characters`);
return false;
}
}
if (input.hasAttribute('maxlength')) {
const maxLength = parseInt(input.getAttribute('maxlength') || '0');
if (value.length > maxLength) {
this.setFieldError(input, `Maximum length is ${maxLength} characters`);
return false;
}
}
if (input.hasAttribute('min') && type === 'number') {
const min = parseFloat(input.getAttribute('min') || '0');
if (parseFloat(value) < min) {
this.setFieldError(input, `Minimum value is ${min}`);
return false;
}
}
if (input.hasAttribute('max') && type === 'number') {
const max = parseFloat(input.getAttribute('max') || '0');
if (parseFloat(value) > max) {
this.setFieldError(input, `Maximum value is ${max}`);
return false;
}
}
// Check type-specific validation
if (type === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
this.setFieldError(input, 'Please enter a valid email address');
return false;
}
}
if (type === 'url' && value) {
try {
new URL(value);
}
catch (_a) {
this.setFieldError(input, 'Please enter a valid URL');
return false;
}
}
// Check custom rules
if (this.options.rules && name && this.options.rules[name]) {
const rule = this.options.rules[name];
if (rule.required && !value) {
this.setFieldError(input, rule.message || 'This field is required');
return false;
}
if (rule.min !== undefined) {
const numValue = type === 'number' ? parseFloat(value) : value.length;
if (numValue < rule.min) {
this.setFieldError(input, rule.message || `Minimum ${type === 'number' ? 'value' : 'length'} is ${rule.min}`);
return false;
}
}
if (rule.max !== undefined) {
const numValue = type === 'number' ? parseFloat(value) : value.length;
if (numValue > rule.max) {
this.setFieldError(input, rule.message || `Maximum ${type === 'number' ? 'value' : 'length'} is ${rule.max}`);
return false;
}
}
if (rule.pattern && value) {
if (!rule.pattern.test(value)) {
this.setFieldError(input, rule.message || 'Invalid format');
return false;
}
}
if (rule.custom && !rule.custom(value)) {
this.setFieldError(input, rule.message || 'Invalid value');
return false;
}
}
return true;
}
/**
* Set field error
*/
setFieldError(input, message) {
var _a, _b;
this.errors.set(input.name, message);
addClass(input, 'error');
if (this.options.showErrors) {
// Remove any existing error message
const existingError = (_a = input.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector('.error-message');
if (existingError) {
existingError.remove();
}
// Create and insert error message
const errorEl = createElement('span', {
className: 'error-message',
text: message,
style: {
color: 'var(--apple-error)',
fontSize: 'var(--apple-text-sm)',
marginTop: 'var(--apple-space-1)',
display: 'block'
}
});
(_b = input.parentElement) === null || _b === void 0 ? void 0 : _b.appendChild(errorEl);
}
}
/**
* Clear field error
*/
clearFieldError(input) {
var _a;
this.errors.delete(input.name);
removeClass(input, 'error');
const errorEl = (_a = input.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector('.error-message');
if (errorEl) {
errorEl.remove();
}
}
/**
* Clear all errors
*/
clearErrors() {
this.errors.clear();
const inputs = this.getInputs();
inputs.forEach(input => {
var _a;
removeClass(input, 'error');
const errorEl = (_a = input.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector('.error-message');
if (errorEl) {
errorEl.remove();
}
});
}
/**
* Get all errors
*/
getErrors() {
return new Map(this.errors);
}
/**
* Set custom rules
*/
setRules(rules) {
this.options.rules = rules;
}
/**
* Add custom validator
*/
addValidator(name, rule) {
if (!this.options.rules) {
this.options.rules = {};
}
this.options.rules[name] = rule;
}
/**
* Static helper to validate form
*/
static validateForm(selector) {
const form = $(selector);
if (!form || !(form instanceof HTMLFormElement))
return false;
const inputs = form.querySelectorAll('input[required], textarea[required], select[required]');
let isValid = true;
inputs.forEach(input => {
const el = input;
if (!el.value.trim()) {
isValid = false;
addClass(el, 'error');
// Remove error class on input
const inputHandler = () => {
removeClass(el, 'error');
};
el.addEventListener('input', inputHandler, { once: true });
}
});
return isValid;
}
}
//# sourceMappingURL=form.js.map