Data Binding
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Data Binding Library
The Data Binding library provides advanced two-way data binding utilities, form management, and reactive binding patterns for our monorepo applications. It enhances Angular's built-in data binding with additional features and abstractions.
📍 Library Details
- Location:
libs/data-binding/ - Dependencies: None (Framework layer)
- Type: Framework Library
- Tags:
npm:public - Purpose: Advanced data binding, form management, and reactive patterns
🌟 Core Purpose
The Data Binding library serves as our data binding enhancement layer by providing:
- Enhanced Two-Way Binding - Advanced binding patterns beyond Angular's defaults
- Form Management - Sophisticated form handling and validation utilities
- Reactive Binding - Observable-based binding patterns
- Data Transformation - Real-time data transformation and mapping
- Binding Utilities - Helper functions and directives for complex binding scenarios
🏗️ Architecture Position
Data Binding sits in the Framework Libraries layer of our dependency hierarchy:
Feature Libraries (widgets, editor-ui, shared-ui, auth, etc.)
↑ depends on
Framework Libraries (ux-framework, theme-system, data-binding, etc.) ← You are here
↑ depends on
Core Libraries (permissions)
↑ depends on
Foundation Libraries (core-types, core)
🔗 Key Features
Enhanced Two-Way Binding Directive
Advanced binding directive with transformation and validation:
import type { SafeAny } from '@libs-foundational/core-models';
import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { Subject, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs';
@Directive({
selector: '[enhancedBinding]',
standalone: true
})
export class EnhancedBindingDirective implements OnInit, OnDestroy {
@Input() enhancedBinding: SafeAny;
@Input() bindingOptions: BindingOptions = {};
@Output() enhancedBindingChange = new EventEmitter<SafeAny>();
@Output() bindingError = new EventEmitter<BindingError>();
private valueSubject = new Subject<SafeAny>();
private destroy$ = new Subject<void>();
ngOnInit(): void {
const {
debounceMs = 300,
distinctCheck = true,
transform,
validate,
onError
} = this.bindingOptions;
let stream = this.valueSubject.asObservable();
// Apply debouncing
if (debounceMs > 0) {
stream = stream.pipe(debounceTime(debounceMs));
}
// Apply distinct check
if (distinctCheck) {
stream = stream.pipe(distinctUntilChanged());
}
stream.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
try {
let processedValue = value;
// Apply transformation
if (transform) {
processedValue = transform(value);
}
// Apply validation
if (validate) {
const validationResult = validate(processedValue);
if (!validationResult.isValid) {
const error: BindingError = {
type: 'validation',
message: validationResult.message || 'Validation failed',
value: processedValue,
originalValue: value
};
this.bindingError.emit(error);
if (onError) onError(error);
return;
}
}
this.enhancedBindingChange.emit(processedValue);
} catch (error) {
const bindingError: BindingError = {
type: 'transformation',
message: (error as Error).message,
value,
originalValue: value
};
this.bindingError.emit(bindingError);
if (this.bindingOptions.onError) {
this.bindingOptions.onError(bindingError);
}
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
updateValue(value: SafeAny): void {
this.valueSubject.next(value);
}
}
interface BindingOptions {
debounceMs?: number;
distinctCheck?: boolean;
transform?: (value: SafeAny) => SafeAny;
validate?: (value: SafeAny) => ValidationResult;
onError?: (error: BindingError) => void;
}
interface ValidationResult {
isValid: boolean;
message?: string;
errors?: Record<string, string>;
}
interface BindingError {
type: 'validation' | 'transformation' | 'network' | 'unknown';
message: string;
value: SafeAny;
originalValue: SafeAny;
context?: SafeAny;
}
Advanced Form Builder Service
Enhanced form building with reactive patterns:
@Injectable({
providedIn: 'root'
})
export class AdvancedFormBuilderService {
constructor(private fb: FormBuilder) {}
createEnhancedForm<T extends Record<string, SafeAny>>(
config: EnhancedFormConfig<T>
): EnhancedFormGroup<T> {
const controls: Record<string, FormControl> = {};
const validators: Record<string, ValidatorFn[]> = {};
const transformers: Record<string, TransformFn> = {};
// Build form controls with enhanced features
Object.entries(config.fields).forEach(([key, fieldConfig]) => {
const control = new FormControl(
fieldConfig.defaultValue,
fieldConfig.validators || []
);
controls[key] = control;
if (fieldConfig.transform) {
transformers[key] = fieldConfig.transform;
}
// Add real-time validation
if (fieldConfig.asyncValidators) {
control.setAsyncValidators(fieldConfig.asyncValidators);
}
// Add conditional validation
if (fieldConfig.conditionalValidators) {
this.setupConditionalValidation(control, fieldConfig.conditionalValidators);
}
});
const formGroup = this.fb.group(controls) as EnhancedFormGroup<T>;
// Add enhanced methods
this.enhanceFormGroup(formGroup, config, transformers);
return formGroup;
}
private enhanceFormGroup<T extends Record<string, SafeAny>>(
formGroup: EnhancedFormGroup<T>,
config: EnhancedFormConfig<T>,
transformers: Record<string, TransformFn>
): void {
// Add getTypedValue method
formGroup.getTypedValue = (): T => {
const rawValue = formGroup.getRawValue();
const transformedValue: SafeAny = {};
Object.entries(rawValue).forEach(([key, value]) => {
if (transformers[key]) {
transformedValue[key] = transformers[key](value);
} else {
transformedValue[key] = value;
}
});
return transformedValue as T;
};
// Add validation summary
formGroup.getValidationSummary = (): ValidationSummary => {
const errors: Record<string, string[]> = {};
let hasErrors = false;
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control?.errors) {
errors[key] = Object.keys(control.errors).map(errorKey => {
const error = control.errors![errorKey];
return typeof error === 'string' ? error : `${errorKey} error`;
});
hasErrors = true;
}
});
return {
isValid: !hasErrors,
errors,
errorCount: Object.values(errors).flat().length
};
};
// Add reset with defaults
formGroup.resetWithDefaults = (): void => {
const defaultValues: SafeAny = {};
Object.entries(config.fields).forEach(([key, fieldConfig]) => {
defaultValues[key] = fieldConfig.defaultValue;
});
formGroup.reset(defaultValues);
};
// Add patch with validation
formGroup.patchValueSafe = (value: Partial<T>): boolean => {
try {
// Validate each field before patching
Object.entries(value).forEach(([key, val]) => {
const control = formGroup.get(key);
if (control) {
control.setValue(val);
control.updateValueAndValidity();
if (control.invalid) {
throw new Error(`Invalid value for field ${key}`);
}
}
});
return true;
} catch (error) {
console.error('Patch value failed:', error);
return false;
}
};
}
private setupConditionalValidation(
control: FormControl,
conditionalValidators: ConditionalValidator[]
): void {
conditionalValidators.forEach(cv => {
cv.condition$.subscribe(shouldValidate => {
if (shouldValidate) {
control.setValidators(cv.validators);
} else {
control.clearValidators();
}
control.updateValueAndValidity();
});
});
}
// Validation helpers
createCustomValidator(
validatorFn: (value: SafeAny) => boolean,
errorMessage: string
): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!validatorFn(control.value)) {
return { custom: { message: errorMessage } };
}
return null;
};
}
createAsyncValidator(
validatorFn: (value: SafeAny) => Observable<boolean>,
errorMessage: string
): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return validatorFn(control.value).pipe(
map(isValid => isValid ? null : { async: { message: errorMessage } })
);
};
}
}
interface EnhancedFormConfig<T extends Record<string, SafeAny>> {
fields: {
[K in keyof T]: FieldConfig<T[K]>;
};
globalValidators?: ValidatorFn[];
options?: {
updateOn?: 'change' | 'blur' | 'submit';
emitEvent?: boolean;
};
}
interface FieldConfig<T> {
defaultValue: T;
validators?: ValidatorFn[];
asyncValidators?: AsyncValidatorFn[];
conditionalValidators?: ConditionalValidator[];
transform?: TransformFn<T>;
disabled?: boolean;
}
interface ConditionalValidator {
condition$: Observable<boolean>;
validators: ValidatorFn[];
}
interface ValidationSummary {
isValid: boolean;
errors: Record<string, string[]>;
errorCount: number;
}
interface EnhancedFormGroup<T> extends FormGroup {
getTypedValue(): T;
getValidationSummary(): ValidationSummary;
resetWithDefaults(): void;
patchValueSafe(value: Partial<T>): boolean;
}
type TransformFn<T = SafeAny> = (value: T) => T;
Reactive Binding Service
Observable-based binding management:
@Injectable({
providedIn: 'root'
})
export class ReactiveBindingService {
private bindings = new Map<string, BindingContext>();
createBinding<T>(
id: string,
initialValue: T,
options: ReactiveBindingOptions<T> = {}
): ReactiveBinding<T> {
const subject = new BehaviorSubject<T>(initialValue);
const errors$ = new Subject<BindingError>();
const binding: ReactiveBinding<T> = {
id,
value$: subject.asObservable(),
errors$: errors$.asObservable(),
setValue(value: T): void {
try {
let processedValue = value;
// Apply transformation
if (options.transform) {
processedValue = options.transform(value);
}
// Apply validation
if (options.validate) {
const validationResult = options.validate(processedValue);
if (!validationResult.isValid) {
const error: BindingError = {
type: 'validation',
message: validationResult.message || 'Validation failed',
value: processedValue,
originalValue: value
};
errors$.next(error);
return;
}
}
subject.next(processedValue);
} catch (error) {
const bindingError: BindingError = {
type: 'transformation',
message: (error as Error).message,
value,
originalValue: value
};
errors$.next(bindingError);
}
},
getCurrentValue(): T {
return subject.value;
},
updateValue(updater: (current: T) => T): void {
const currentValue = subject.value;
const newValue = updater(currentValue);
this.setValue(newValue);
},
reset(): void {
subject.next(initialValue);
},
dispose(): void {
subject.complete();
errors$.complete();
this.removeBinding(id);
}
};
const context: BindingContext = {
binding,
subject,
errors$,
options
};
this.bindings.set(id, context);
return binding;
}
getBinding<T>(id: string): ReactiveBinding<T> | null {
const context = this.bindings.get(id);
return context ? context.binding as ReactiveBinding<T> : null;
}
removeBinding(id: string): boolean {
const context = this.bindings.get(id);
if (context) {
context.subject.complete();
context.errors$.complete();
this.bindings.delete(id);
return true;
}
return false;
}
bindMultiple<T extends Record<string, SafeAny>>(
bindings: { [K in keyof T]: T[K] }
): Observable<T> {
const observables: { [K in keyof T]: Observable<T[K]> } = {} as SafeAny;
Object.entries(bindings).forEach(([key, initialValue]) => {
const bindingId = `multi_${key}_${Date.now()}`;
const binding = this.createBinding(bindingId, initialValue);
observables[key as keyof T] = binding.value$;
});
return combineLatest(observables);
}
createComputedBinding<T, R>(
sourceBindings: string[],
computeFn: (...values: SafeAny[]) => R,
options: ReactiveBindingOptions<R> = {}
): ReactiveBinding<R> {
const sources = sourceBindings
.map(id => this.getBinding(id))
.filter(binding => binding !== null)
.map(binding => binding!.value$);
if (sources.length === 0) {
throw new Error('No valid source bindings found');
}
const computedId = `computed_${Date.now()}`;
const initialValue = computeFn(...sources.map(s => (s as BehaviorSubject<SafeAny>).value));
const computedBinding = this.createBinding(computedId, initialValue, options);
// Subscribe to source changes
combineLatest(sources).subscribe(values => {
const computedValue = computeFn(...values);
computedBinding.setValue(computedValue);
});
return computedBinding;
}
}
interface ReactiveBindingOptions<T> {
transform?: (value: T) => T;
validate?: (value: T) => ValidationResult;
debounceMs?: number;
distinctUntilChanged?: boolean;
}
interface ReactiveBinding<T> {
readonly id: string;
readonly value$: Observable<T>;
readonly errors$: Observable<BindingError>;
setValue(value: T): void;
getCurrentValue(): T;
updateValue(updater: (current: T) => T): void;
reset(): void;
dispose(): void;
}
interface BindingContext {
binding: ReactiveBinding<SafeAny>;
subject: BehaviorSubject<SafeAny>;
errors$: Subject<BindingError>;
options: ReactiveBindingOptions<SafeAny>;
}
📦 Common Usage Patterns
Enhanced Form Example
interface UserFormData {
firstName: string;
lastName: string;
email: string;
age: number;
preferences: UserPreferences;
}
interface UserPreferences {
newsletter: boolean;
theme: 'light' | 'dark';
language: string;
}
@Component({
selector: 'app-user-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, DataBindingModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div class="form-section">
<h3>Personal Information</h3>
<input
formControlName="firstName"
placeholder="First Name"
[enhancedBinding]="userForm.get('firstName')?.value"
[bindingOptions]="firstNameBindingOptions"
(enhancedBindingChange)="onFirstNameChange($event)"
(bindingError)="onBindingError('firstName', $event)">
<input
formControlName="lastName"
placeholder="Last Name"
[enhancedBinding]="userForm.get('lastName')?.value"
[bindingOptions]="lastNameBindingOptions"
(enhancedBindingChange)="onLastNameChange($event)">
<input
type="email"
formControlName="email"
placeholder="Email Address"
[enhancedBinding]="userForm.get('email')?.value"
[bindingOptions]="emailBindingOptions"
(enhancedBindingChange)="onEmailChange($event)">
<input
type="number"
formControlName="age"
placeholder="Age"
[enhancedBinding]="userForm.get('age')?.value"
[bindingOptions]="ageBindingOptions"
(enhancedBindingChange)="onAgeChange($event)">
</div>
<div class="form-section" formGroupName="preferences">
<h3>Preferences</h3>
<label>
<input type="checkbox" formControlName="newsletter">
Subscribe to newsletter
</label>
<select formControlName="theme">
<option value="light">Light Theme</option>
<option value="dark">Dark Theme</option>
</select>
<select formControlName="language">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</div>
<div class="form-actions">
<button type="button" (click)="resetForm()">Reset</button>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</div>
<div class="validation-summary" *ngIf="validationSummary">
<h4>Validation Summary</h4>
<div *ngFor="let error of validationSummary.errors | keyvalue">
<strong>{{ error.key }}:</strong>
<ul>
<li *ngFor="let message of error.value">{{ message }}</li>
</ul>
</div>
</div>
</form>
`
})
export class UserFormComponent implements OnInit {
userForm!: EnhancedFormGroup<UserFormData>;
validationSummary: ValidationSummary | null = null;
// Binding options for different fields
firstNameBindingOptions: BindingOptions = {
debounceMs: 300,
transform: (value: string) => value.trim().toLowerCase(),
validate: (value: string) => ({
isValid: value.length >= 2,
message: value.length < 2 ? 'First name must be at least 2 characters' : undefined
})
};
lastNameBindingOptions: BindingOptions = {
debounceMs: 300,
transform: (value: string) => value.trim().toLowerCase()
};
emailBindingOptions: BindingOptions = {
debounceMs: 500,
validate: (value: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return {
isValid: emailRegex.test(value),
message: !emailRegex.test(value) ? 'Please enter a valid email address' : undefined
};
}
};
ageBindingOptions: BindingOptions = {
transform: (value: SafeAny) => parseInt(value, 10) || 0,
validate: (value: number) => ({
isValid: value >= 13 && value <= 120,
message: value < 13 ? 'Must be at least 13 years old' :
value > 120 ? 'Please enter a valid age' : undefined
})
};
constructor(private formBuilder: AdvancedFormBuilderService) {}
ngOnInit(): void {
this.userForm = this.formBuilder.createEnhancedForm<UserFormData>({
fields: {
firstName: {
defaultValue: '',
validators: [Validators.required, Validators.minLength(2)]
},
lastName: {
defaultValue: '',
validators: [Validators.required, Validators.minLength(2)]
},
email: {
defaultValue: '',
validators: [Validators.required, Validators.email],
asyncValidators: [this.createEmailUniqueValidator()]
},
age: {
defaultValue: 0,
validators: [Validators.required, Validators.min(13), Validators.max(120)]
},
preferences: {
defaultValue: {
newsletter: false,
theme: 'light' as const,
language: 'en'
}
}
}
});
// Update validation summary on form changes
this.userForm.statusChanges.subscribe(() => {
this.validationSummary = this.userForm.getValidationSummary();
});
}
onFirstNameChange(value: string): void {
this.userForm.get('firstName')?.setValue(value);
}
onLastNameChange(value: string): void {
this.userForm.get('lastName')?.setValue(value);
}
onEmailChange(value: string): void {
this.userForm.get('email')?.setValue(value);
}
onAgeChange(value: number): void {
this.userForm.get('age')?.setValue(value);
}
onBindingError(field: string, error: BindingError): void {
console.error(`Binding error in ${field}:`, error);
// Handle binding errors (show user feedback, etc.)
}
onSubmit(): void {
if (this.userForm.valid) {
const formData = this.userForm.getTypedValue();
console.log('Form submitted:', formData);
// Handle form submission
}
}
resetForm(): void {
this.userForm.resetWithDefaults();
}
private createEmailUniqueValidator(): AsyncValidatorFn {
return this.formBuilder.createAsyncValidator(
(email: string) => {
// Simulate API call to check email uniqueness
return timer(1000).pipe(
map(() => !email.includes('taken')) // Simulate logic
);
},
'Email address is already taken'
);
}
}
🛠️ Development
Building the Library
# Build data-binding library
npx nx build data-binding
# Build with watch mode
npx nx build data-binding --watch
Testing
# Run tests
npx nx test data-binding
# Run tests with coverage
npx nx test data-binding --coverage
# Run linting
npx nx lint data-binding
Integration
// Proper import patterns
import {
EnhancedBindingDirective,
AdvancedFormBuilderService,
ReactiveBindingService
} from '@data-binding';
// Use in your application
📋 Best Practices
Data Binding
- Use debouncing - For performance with frequent updates
- Validate early - Provide immediate feedback to users
- Transform consistently - Apply transformations at binding level
- Handle errors gracefully - Provide meaningful error messages
Form Management
- Type safety - Use strongly typed forms
- Modular validation - Create reusable validators
- Async validation - Handle server-side validation
- Reset patterns - Provide clear reset functionality
🔗 Dependencies and Usage
Used By
- Editor UI - Advanced form handling in editors
- Shared UI - Common form components
- Widgets - Widget configuration forms
- All Applications - Enhanced form management
Integration Points
- Angular Forms - Extends Angular Reactive Forms
- RxJS - Reactive programming patterns
- Validation Libraries - Custom validation integration
- Component Library - Form component integration
🌟 Key Benefits
Enhanced Binding
- Real-time transformation of data during binding
- Comprehensive validation with custom and async validators
- Error handling with detailed error information
- Performance optimization with debouncing and distinct checks
Form Management
- Type safety with enhanced form groups
- Advanced validation with conditional and async validators
- Better UX with validation summaries and error handling
- Reusable patterns for common form scenarios
Developer Experience
- Easy integration with existing Angular forms
- Reactive patterns with observable-based binding
- Comprehensive testing support with mockable services
- Flexible configuration for different use cases
The Data Binding library enhances Angular's built-in data binding capabilities with advanced features that improve both developer experience and user experience in form-heavy applications.