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

🌟 Core Purpose

The Data Binding library serves as our data binding enhancement layer by providing:

  1. Enhanced Two-Way Binding - Advanced binding patterns beyond Angular's defaults
  2. Form Management - Sophisticated form handling and validation utilities
  3. Reactive Binding - Observable-based binding patterns
  4. Data Transformation - Real-time data transformation and mapping
  5. 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

  1. Use debouncing - For performance with frequent updates
  2. Validate early - Provide immediate feedback to users
  3. Transform consistently - Apply transformations at binding level
  4. Handle errors gracefully - Provide meaningful error messages

Form Management

  1. Type safety - Use strongly typed forms
  2. Modular validation - Create reusable validators
  3. Async validation - Handle server-side validation
  4. Reset patterns - Provide clear reset functionality

🔗 Dependencies and Usage

Used By

Integration Points

🌟 Key Benefits

Enhanced Binding

Form Management

Developer Experience

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.