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