UNPKG

ng-smart-forms

Version:

Zero-config reactive forms with built-in validation, auto-save, and smart error handling for Angular

340 lines (331 loc) 13.5 kB
import * as i0 from '@angular/core'; import { Injectable, Input, Directive, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { Subject, BehaviorSubject, takeUntil } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; class AutoSaveService { saveSubject = new Subject(); statusSubject = new BehaviorSubject('idle'); constructor() { this.saveSubject.pipe(debounceTime(2000), distinctUntilChanged()).subscribe(data => { this.saveData(data); }); } triggerSave(data) { this.statusSubject.next('saving'); this.saveSubject.next(data); } getSaveStatus() { return this.statusSubject.asObservable(); } saveData(data) { // Simulate API call or localStorage save setTimeout(() => { try { localStorage.setItem('ngx-smart-forms-data', JSON.stringify(data)); this.statusSubject.next('saved'); setTimeout(() => { this.statusSubject.next('idle'); }, 2000); } catch (error) { this.statusSubject.next('error'); } }, 1000); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoSaveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoSaveService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoSaveService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class SmartFormDirective { el; renderer; autoSaveService; smartForm; config = {}; destroy$ = new Subject(); defaultConfig = { autoSave: true, autoSaveDelay: 2000, showErrorsOnTouch: true, validateOnChange: true, errorMessages: { required: 'This field is required', email: 'Please enter a valid email address', minlength: 'Minimum length not met', maxlength: 'Maximum length exceeded', pattern: 'Invalid format', strongPassword: 'Password must contain uppercase, lowercase, number, and special character', noSpaces: 'Spaces are not allowed', phoneNumber: 'Please enter a valid phone number', creditCard: 'Please enter a valid credit card number' } }; constructor(el, renderer, autoSaveService) { this.el = el; this.renderer = renderer; this.autoSaveService = autoSaveService; } ngOnInit() { this.config = { ...this.defaultConfig, ...this.config }; this.setupAutoSave(); this.setupValidation(); this.addStatusIndicator(); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } setupAutoSave() { if (this.config.autoSave) { this.smartForm.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { if (this.smartForm.valid) { this.autoSaveService.triggerSave(value); } }); } } setupValidation() { if (this.config.validateOnChange) { this.smartForm.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.updateErrorDisplay(); }); } this.smartForm.statusChanges .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.updateErrorDisplay(); }); } updateErrorDisplay() { Object.keys(this.smartForm.controls).forEach(key => { const control = this.smartForm.get(key); const errorElement = this.el.nativeElement.querySelector(`[data-error="${key}"]`); if (control && errorElement) { const shouldShow = control.errors && (control.touched || !this.config.showErrorsOnTouch); if (shouldShow) { const errorMessage = this.getErrorMessage(control.errors); errorElement.textContent = errorMessage; this.renderer.setStyle(errorElement, 'display', 'block'); } else { this.renderer.setStyle(errorElement, 'display', 'none'); } } }); } getErrorMessage(errors) { const errorKey = Object.keys(errors)[0]; return this.config.errorMessages?.[errorKey] || 'Invalid input'; } addStatusIndicator() { if (this.config.autoSave) { const statusEl = this.renderer.createElement('div'); this.renderer.addClass(statusEl, 'smart-form-status'); this.renderer.setStyle(statusEl, 'font-size', '12px'); this.renderer.setStyle(statusEl, 'color', '#666'); this.renderer.setStyle(statusEl, 'margin-top', '5px'); this.renderer.appendChild(this.el.nativeElement, statusEl); this.autoSaveService.getSaveStatus() .pipe(takeUntil(this.destroy$)) .subscribe(status => { const messages = { idle: '', saving: '💾 Saving...', saved: '✅ Saved', error: '❌ Save failed' }; statusEl.textContent = messages[status]; }); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartFormDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: AutoSaveService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SmartFormDirective, isStandalone: true, selector: "[smartForm]", inputs: { smartForm: "smartForm", config: "config" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartFormDirective, decorators: [{ type: Directive, args: [{ selector: '[smartForm]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: AutoSaveService }], propDecorators: { smartForm: [{ type: Input }], config: [{ type: Input }] } }); class SmartInputDirective { el; renderer; smartInput; fieldConfig = {}; constructor(el, renderer) { this.el = el; this.renderer = renderer; } ngOnInit() { this.setupFieldEnhancements(); this.addErrorContainer(); } setupFieldEnhancements() { const input = this.el.nativeElement; // Add placeholder if configured if (this.fieldConfig.placeholder) { this.renderer.setAttribute(input, 'placeholder', this.fieldConfig.placeholder); } // Add input type if configured if (this.fieldConfig.type) { this.renderer.setAttribute(input, 'type', this.fieldConfig.type); } // Add visual feedback classes this.renderer.addClass(input, 'smart-input'); // Add focus/blur effects this.renderer.listen(input, 'focus', () => { this.renderer.addClass(input, 'smart-input-focused'); }); this.renderer.listen(input, 'blur', () => { this.renderer.removeClass(input, 'smart-input-focused'); }); } addErrorContainer() { const errorDiv = this.renderer.createElement('div'); this.renderer.addClass(errorDiv, 'smart-input-error'); this.renderer.setStyle(errorDiv, 'color', '#e74c3c'); this.renderer.setStyle(errorDiv, 'font-size', '12px'); this.renderer.setStyle(errorDiv, 'margin-top', '5px'); this.renderer.setStyle(errorDiv, 'display', 'none'); const fieldName = this.el.nativeElement.getAttribute('name') || 'field'; this.renderer.setAttribute(errorDiv, 'data-error', fieldName); // Use appendChild to parent or insertBefore with nextSibling const parent = this.el.nativeElement.parentNode; const nextSibling = this.el.nativeElement.nextSibling; if (nextSibling) { this.renderer.insertBefore(parent, errorDiv, nextSibling); } else { this.renderer.appendChild(parent, errorDiv); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartInputDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SmartInputDirective, isStandalone: true, selector: "[smartInput]", inputs: { smartInput: "smartInput", fieldConfig: "fieldConfig" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartInputDirective, decorators: [{ type: Directive, args: [{ selector: '[smartInput]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { smartInput: [{ type: Input }], fieldConfig: [{ type: Input }] } }); class SmartFormModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartFormModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SmartFormModule, imports: [CommonModule, ReactiveFormsModule, SmartFormDirective, SmartInputDirective], exports: [SmartFormDirective, SmartInputDirective] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartFormModule, imports: [CommonModule, ReactiveFormsModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SmartFormModule, decorators: [{ type: NgModule, args: [{ declarations: [], imports: [ CommonModule, ReactiveFormsModule, SmartFormDirective, SmartInputDirective ], exports: [ SmartFormDirective, SmartInputDirective ] }] }] }); class SmartValidators { static strongPassword() { return (control) => { const value = control.value; if (!value) return null; const hasUpper = /[A-Z]/.test(value); const hasLower = /[a-z]/.test(value); const hasNumber = /[0-9]/.test(value); const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value); const minLength = value.length >= 8; const valid = hasUpper && hasLower && hasNumber && hasSpecial && minLength; if (!valid) { return { strongPassword: { hasUpper, hasLower, hasNumber, hasSpecial, minLength } }; } return null; }; } static noSpaces() { return (control) => { const value = control.value; if (!value) return null; return value.includes(' ') ? { noSpaces: true } : null; }; } static phoneNumber() { return (control) => { const value = control.value; if (!value) return null; const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; return phoneRegex.test(value) ? null : { phoneNumber: true }; }; } static creditCard() { return (control) => { const value = control.value; if (!value) return null; // Luhn algorithm let sum = 0; let isEven = false; for (let i = value.length - 1; i >= 0; i--) { let digit = parseInt(value.charAt(i), 10); if (isEven) { digit *= 2; if (digit > 9) { digit -= 9; } } sum += digit; isEven = !isEven; } return sum % 10 === 0 ? null : { creditCard: true }; }; } } /* * Public API Surface of ngx-smart-forms */ /** * Generated bundle index. Do not edit. */ export { AutoSaveService, SmartFormDirective, SmartFormModule, SmartInputDirective, SmartValidators }; //# sourceMappingURL=ng-smart-forms.mjs.map