UNPKG

@ordojs/forms

Version:

Comprehensive form handling system for OrdoJS

472 lines 16.1 kB
/** * Core form state management with reactive field-level tracking */ export class ReactiveForm { constructor(config = {}) { this.fieldConfigs = new Map(); this.fieldStates = new Map(); this.subscribers = new Set(); this.fieldSubscribers = new Map(); this.validationTimeouts = new Map(); this.config = { initialValues: config.initialValues || {}, validateOnChange: config.validateOnChange ?? true, validateOnBlur: config.validateOnBlur ?? true, validateOnSubmit: config.validateOnSubmit ?? true, resetOnSubmit: config.resetOnSubmit ?? false, enableReinitialize: config.enableReinitialize ?? false, onSubmit: config.onSubmit || (() => { }), onValidationError: config.onValidationError || (() => { }), onReset: config.onReset || (() => { }) }; this.state = this.createInitialState(); } createInitialState() { return { values: { ...this.config.initialValues }, errors: {}, touched: {}, dirty: {}, valid: true, validating: false, submitting: false, submitted: false, submitCount: 0 }; } createInitialFieldState(config) { const value = this.state.values[config.name] ?? config.defaultValue ?? ''; return { value, error: null, touched: false, dirty: false, valid: true, validating: false, focused: false }; } notifySubscribers() { this.subscribers.forEach(callback => callback(this.state)); } notifyFieldSubscribers(fieldName) { const fieldState = this.fieldStates.get(fieldName); if (fieldState) { const subscribers = this.fieldSubscribers.get(fieldName); if (subscribers) { subscribers.forEach(callback => callback(fieldState)); } } } async validateFieldInternal(name) { const config = this.fieldConfigs.get(name); const fieldState = this.fieldStates.get(name); if (!config || !fieldState) { return true; } // Set validating state fieldState.validating = true; this.state.validating = true; this.notifyFieldSubscribers(name); this.notifySubscribers(); let isValid = true; let error = null; try { // Run synchronous rules first if (config.rules) { for (const rule of config.rules) { const result = rule.validate(fieldState.value, this.state.values); if (typeof result === 'boolean' && !result) { error = { type: rule.type, message: rule.message, field: name }; isValid = false; break; } else if (result instanceof Promise) { const asyncResult = await result; if (!asyncResult) { error = { type: rule.type, message: rule.message, field: name }; isValid = false; break; } } } } // Run async rules if sync rules passed if (isValid && config.asyncRules) { for (const rule of config.asyncRules) { const result = await rule.validate(fieldState.value, this.state.values); if (!result) { error = { type: rule.type, message: rule.message, field: name }; isValid = false; break; } } } } catch (err) { error = { type: 'validation_error', message: err instanceof Error ? err.message : 'Validation failed', field: name }; isValid = false; } // Update field state fieldState.valid = isValid; fieldState.error = error; fieldState.validating = false; // Update form state if (error) { this.state.errors[name] = error; } else { delete this.state.errors[name]; } // Update form validity this.updateFormValidity(); this.notifyFieldSubscribers(name); this.notifySubscribers(); return isValid; } updateFormValidity() { const hasErrors = Object.keys(this.state.errors).length > 0; const hasValidatingFields = Array.from(this.fieldStates.values()).some(field => field.validating); this.state.valid = !hasErrors; this.state.validating = hasValidatingFields; } debounceValidation(name, ms) { const existingTimeout = this.validationTimeouts.get(name); if (existingTimeout) { clearTimeout(existingTimeout); } const timeout = setTimeout(() => { this.validateField(name); this.validationTimeouts.delete(name); }, ms); this.validationTimeouts.set(name, timeout); } // FormInstance implementation getState() { return { ...this.state }; } getFieldState(name) { const fieldState = this.fieldStates.get(name); return fieldState ? { ...fieldState } : undefined; } getValues() { return { ...this.state.values }; } getValue(name) { return this.state.values[name]; } getErrors() { return { ...this.state.errors }; } getError(name) { return this.state.errors[name] || null; } setValue(name, value, shouldValidate = true) { const config = this.fieldConfigs.get(name); const fieldState = this.fieldStates.get(name); if (!fieldState) { return; } // Transform value if transformer exists const transformedValue = config?.transform ? config.transform(value) : value; // Update state const oldValue = fieldState.value; fieldState.value = transformedValue; fieldState.dirty = transformedValue !== (config?.defaultValue ?? ''); this.state.values[name] = transformedValue; this.state.dirty[name] = fieldState.dirty; // Clear existing error if value changed if (oldValue !== transformedValue && fieldState.error) { fieldState.error = null; delete this.state.errors[name]; this.updateFormValidity(); } this.notifyFieldSubscribers(name); this.notifySubscribers(); // Validate if required if (shouldValidate && this.config.validateOnChange && config?.validateOnChange !== false) { const debounceMs = config?.debounceMs ?? 300; if (debounceMs > 0) { this.debounceValidation(name, debounceMs); } else { this.validateField(name); } } } setValues(values, shouldValidate = true) { Object.entries(values).forEach(([name, value]) => { this.setValue(name, value, false); }); if (shouldValidate) { this.validateForm(); } } setError(name, error) { const fieldState = this.fieldStates.get(name); if (fieldState) { fieldState.error = error; fieldState.valid = false; } this.state.errors[name] = error; this.updateFormValidity(); this.notifyFieldSubscribers(name); this.notifySubscribers(); } setErrors(errors) { Object.entries(errors).forEach(([name, error]) => { this.setError(name, error); }); } clearError(name) { const fieldState = this.fieldStates.get(name); if (fieldState) { fieldState.error = null; fieldState.valid = true; } delete this.state.errors[name]; this.updateFormValidity(); this.notifyFieldSubscribers(name); this.notifySubscribers(); } clearErrors() { this.fieldStates.forEach((fieldState, name) => { fieldState.error = null; fieldState.valid = true; }); this.state.errors = {}; this.updateFormValidity(); this.fieldStates.forEach((_, name) => { this.notifyFieldSubscribers(name); }); this.notifySubscribers(); } registerField(config) { this.fieldConfigs.set(config.name, config); if (!this.fieldStates.has(config.name)) { this.fieldStates.set(config.name, this.createInitialFieldState(config)); } // Initialize value if not already set if (!(config.name in this.state.values)) { this.state.values[config.name] = config.defaultValue ?? ''; } this.notifySubscribers(); return () => { this.unregisterField(config.name); }; } unregisterField(name) { this.fieldConfigs.delete(name); this.fieldStates.delete(name); this.fieldSubscribers.delete(name); delete this.state.values[name]; delete this.state.errors[name]; delete this.state.touched[name]; delete this.state.dirty[name]; // Clear any pending validation timeout const timeout = this.validationTimeouts.get(name); if (timeout) { clearTimeout(timeout); this.validationTimeouts.delete(name); } this.updateFormValidity(); this.notifySubscribers(); } touchField(name) { const fieldState = this.fieldStates.get(name); if (fieldState) { fieldState.touched = true; this.state.touched[name] = true; this.notifyFieldSubscribers(name); this.notifySubscribers(); } } untouchField(name) { const fieldState = this.fieldStates.get(name); if (fieldState) { fieldState.touched = false; this.state.touched[name] = false; this.notifyFieldSubscribers(name); this.notifySubscribers(); } } focusField(name) { const fieldState = this.fieldStates.get(name); if (fieldState) { fieldState.focused = true; this.notifyFieldSubscribers(name); } } blurField(name) { const fieldState = this.fieldStates.get(name); const config = this.fieldConfigs.get(name); if (fieldState) { fieldState.focused = false; this.touchField(name); this.notifyFieldSubscribers(name); // Validate on blur if enabled if (this.config.validateOnBlur && config?.validateOnBlur !== false) { this.validateField(name); } } } async validateField(name) { return this.validateFieldInternal(name); } async validateForm() { const validationPromises = Array.from(this.fieldConfigs.keys()).map(name => this.validateFieldInternal(name)); const results = await Promise.all(validationPromises); return results.every(result => result); } isValid() { return this.state.valid; } isValidating() { return this.state.validating; } async submit() { this.state.submitting = true; this.state.submitCount++; this.notifySubscribers(); try { // Validate form if required if (this.config.validateOnSubmit) { const isValid = await this.validateForm(); if (!isValid) { this.config.onValidationError(this.state.errors, this); return; } } // Call onSubmit handler await this.config.onSubmit(this.state.values, this); this.state.submitted = true; // Reset form if configured if (this.config.resetOnSubmit) { this.reset(); } } finally { this.state.submitting = false; this.notifySubscribers(); } } reset(values) { const resetValues = values || this.config.initialValues; // Reset form state this.state = this.createInitialState(); this.state.values = { ...resetValues }; // Reset field states this.fieldStates.forEach((fieldState, name) => { const config = this.fieldConfigs.get(name); const resetValue = resetValues[name] ?? config?.defaultValue ?? ''; fieldState.value = resetValue; fieldState.error = null; fieldState.touched = false; fieldState.dirty = false; fieldState.valid = true; fieldState.validating = false; fieldState.focused = false; }); // Clear validation timeouts this.validationTimeouts.forEach(timeout => clearTimeout(timeout)); this.validationTimeouts.clear(); this.config.onReset(this); // Notify all subscribers this.fieldStates.forEach((_, name) => { this.notifyFieldSubscribers(name); }); this.notifySubscribers(); } resetField(name, value) { const config = this.fieldConfigs.get(name); const fieldState = this.fieldStates.get(name); if (config && fieldState) { const resetValue = value ?? config.defaultValue ?? ''; fieldState.value = resetValue; fieldState.error = null; fieldState.touched = false; fieldState.dirty = false; fieldState.valid = true; fieldState.validating = false; this.state.values[name] = resetValue; delete this.state.errors[name]; this.state.touched[name] = false; this.state.dirty[name] = false; // Clear validation timeout const timeout = this.validationTimeouts.get(name); if (timeout) { clearTimeout(timeout); this.validationTimeouts.delete(name); } this.updateFormValidity(); this.notifyFieldSubscribers(name); this.notifySubscribers(); } } isDirty() { return Object.values(this.state.dirty).some(Boolean); } isTouched() { return Object.values(this.state.touched).some(Boolean); } isSubmitting() { return this.state.submitting; } hasBeenSubmitted() { return this.state.submitted; } getSubmitCount() { return this.state.submitCount; } handleSubmit(event) { if (event) { event.preventDefault(); } return this.submit(); } handleReset(event) { if (event) { event.preventDefault(); } this.reset(); } handleFieldChange(name, value) { this.setValue(name, value); } handleFieldBlur(name) { this.blurField(name); } handleFieldFocus(name) { this.focusField(name); } subscribe(callback) { this.subscribers.add(callback); return () => { this.subscribers.delete(callback); }; } subscribeToField(name, callback) { if (!this.fieldSubscribers.has(name)) { this.fieldSubscribers.set(name, new Set()); } const subscribers = this.fieldSubscribers.get(name); subscribers.add(callback); return () => { subscribers.delete(callback); if (subscribers.size === 0) { this.fieldSubscribers.delete(name); } }; } } /** * Create a new reactive form instance */ export function createForm(config) { return new ReactiveForm(config); } //# sourceMappingURL=form.js.map