Custom Resolvers
Custom Resolvers
Section titled “Custom Resolvers”While ngx-formwork provides built-in resolvers for components and validators, you may want to implement custom resolvers for specialized use cases. This guide explains how to create and use custom component and validator resolvers in your application.
Understanding Resolvers
Section titled “Understanding Resolvers”In ngx-formwork, resolvers are responsible for providing components and validators to the form system at runtime. The built-in resolvers use Angular’s dependency injection to resolve these dependencies from the configured tokens.
There are two types of resolvers:
- Component Resolver: Maps component type strings (like ‘text’, ‘group’) to actual component types
- Validator Resolver: Maps validator name strings to validator functions
Creating a Custom Component Resolver
Section titled “Creating a Custom Component Resolver”To create a custom component resolver, implement the ComponentResolver
interface:
import { Signal, Type, computed, Injectable, signal } from '@angular/core';import { ComponentResolver } from 'ngx-formwork';
@Injectable()export class AppCustomComponentResolver implements ComponentResolver { // Create a signal with your component mapping private readonly _componentMap = signal(new Map<string, Type<unknown>>([ ['custom-text', MyCustomTextComponent], ['special-group', MySpecialGroupComponent], // Add more components as needed ]));
// Expose as readonly signal as required by the interface readonly registrations = this._componentMap.asReadonly();
// Optional: Add methods to dynamically update components addComponent(key: string, component: Type<unknown>): void { const currentMap = new Map(this._componentMap()); currentMap.set(key, component); this._componentMap.set(currentMap); }
removeComponent(key: string): void { const currentMap = new Map(this._componentMap()); currentMap.delete(key); this._componentMap.set(currentMap); }}
Advanced Component Resolver
Section titled “Advanced Component Resolver”For more complex scenarios, you might want to create a resolver that combines multiple sources:
import { Signal, Type, computed, inject, Injectable } from '@angular/core';import { ComponentResolver, NGX_FW_COMPONENT_REGISTRATIONS } from 'ngx-formwork';
@Injectable()export class HybridComponentResolver implements ComponentResolver { // Inject the default registrations private readonly defaultRegistrations = inject(NGX_FW_COMPONENT_REGISTRATIONS);
// Create your dynamic registrations private readonly dynamicRegistrations = signal(new Map<string, Type<unknown>>());
// Combine them with computed readonly registrations: Signal<ReadonlyMap<string, Type<unknown>>> = computed(() => { const result = new Map<string, Type<unknown>>(this.defaultRegistrations);
// Override with dynamic registrations for (const [key, component] of this.dynamicRegistrations()) { result.set(key, component); }
return result; });
// Methods to update dynamic registrations updateDynamicComponent(key: string, component: Type<unknown>): void { const current = new Map(this.dynamicRegistrations()); current.set(key, component); this.dynamicRegistrations.set(current); }}
Creating a Custom Validator Resolver
Section titled “Creating a Custom Validator Resolver”Implementing a custom validator resolver follows a similar pattern but requires handling both synchronous and asynchronous validators:
import { Signal, Injectable, signal } from '@angular/core';import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';import { ValidatorResolver } from 'ngx-formwork';
@Injectable()export class AppCustomValidatorResolver implements ValidatorResolver { // Create signals for both types of validators private readonly _validatorMap = signal(new Map<string, ValidatorFn[]>([ ['customRequired', [Validators.required, myCustomRequiredValidator]], ['passwordStrength', [passwordStrengthValidator]], // Add more validators as needed ]));
private readonly _asyncValidatorMap = signal(new Map<string, AsyncValidatorFn[]>([ ['uniqueUsername', [uniqueUsernameValidator]], ['serverCheck', [serverCheckValidator]], // Add more async validators as needed ]));
// Expose as readonly signals as required by the interface readonly registrations = this._validatorMap.asReadonly(); readonly asyncRegistrations = this._asyncValidatorMap.asReadonly();
// Optional: Add methods to dynamically update validators addValidator(key: string, validators: ValidatorFn[]): void { const currentMap = new Map(this._validatorMap()); currentMap.set(key, validators); this._validatorMap.set(currentMap); }
addAsyncValidator(key: string, validators: AsyncValidatorFn[]): void { const currentMap = new Map(this._asyncValidatorMap()); currentMap.set(key, validators); this._asyncValidatorMap.set(currentMap); }}
Registering Custom Resolvers
Section titled “Registering Custom Resolvers”To use your custom resolvers, you need to provide them in your application configuration:
import { ApplicationConfig } from '@angular/core';import { provideFormwork } from 'ngx-formwork';import { NGX_FW_COMPONENT_RESOLVER, NGX_VALIDATOR_RESOLVER } from 'ngx-formwork';import { AppCustomComponentResolver } from './app-custom-component-resolver';import { AppCustomValidatorResolver } from './app-custom-validator-resolver';
export const appConfig: ApplicationConfig = { providers: [ // Configure formwork provideFormwork(),
// Provide your custom resolvers // These MUST come after provideFormwork() { provide: NGX_FW_COMPONENT_RESOLVER, useExisting: AppCustomComponentResolver }, { provide: NGX_VALIDATOR_RESOLVER, useExisting: AppCustomValidatorResolver }, ]};
Use Cases for Custom Resolvers
Section titled “Use Cases for Custom Resolvers”Custom resolvers can be particularly valuable in the following scenarios:
- Dynamic Component Loading - Load components on-demand based on user actions or application state
- Feature-Based Validators - Switch between different validation rule sets based on application features or user roles
- Permission-Based Components - Show or hide components based on user permissions
- Internationalized Validators - Use different validation rules based on locale or region
- A/B Testing - Swap components for different user groups to test UI variations
- Plugin Architecture - Allow third-party modules to register their own components and validators
- Environment-Specific Components - Use different implementations in development vs. production environments
Best Practices
Section titled “Best Practices”When implementing custom resolvers:
- Performance: Use
signal()
efficiently and avoid unnecessary computations - Immutability: Always create new Maps when updating signals
- Error Handling: Add proper error handling for missing components or validators
- Testing: Create unit tests to verify your resolver’s behavior
- Integration: Ensure smooth integration with existing ngx-formwork configurations
Debug Tips
Section titled “Debug Tips”If you encounter issues with your custom resolvers:
- Verify that your resolver is properly registered in the DI container
- Check that your resolver correctly implements the required interface
- Ensure your resolver is provided at the correct level (root or module)
- Verify that component and validator names match those used in your form configurations