dynamic-forms / Interface

CustomFnConfig

Signal forms adapter configuration for advanced form behavior.

Provides configuration options for signal forms integration including legacy migration, custom functions, and custom validators.

Properties

NameTypeDescription
asyncValidators
Record<string, AsyncCustomValidator<unknown, unknown, unknown>> | undefined

Async custom validators using Angular's resource-based validateAsync() API

Angular's validateAsync uses the resource API for async validation. Validators must provide params, factory, onSuccess, and optional onError callbacks.

Structure:

  • params: Function that computes params from field context
  • factory: Function that creates ResourceRef from params signal
  • onSuccess: Maps resource result to validation errors
  • onError: Optional handler for resource errors

Use Cases:

  • Database lookups via services with resource API
  • Complex async business logic with Angular resources

Note: For HTTP validation, prefer httpValidators which provides a simpler API specifically designed for HTTP requests.

Example usage

Database Lookup with Resource

asyncValidators: {
  checkUsernameAvailable: {
    params: (ctx) => ({ username: ctx.value() }),
    factory: (params) => {
      const injector = inject(Injector);
      return resource({
        request: () => params(),
        loader: ({ request }) => {
          if (!request?.username) return null;
          const service = injector.get(UserService);
          return firstValueFrom(service.checkAvailability(request.username));
        }
      });
    },
    onSuccess: (result, ctx) => {
      if (!result) return null;
      return result.available ? null : { kind: 'usernameTaken' };
    },
    onError: (error, ctx) => {
      console.error('Availability check failed:', error);
      return null; // Don't block form on network errors
    }
  }
}
customFunctions
Record<string, CustomFunction> | undefined

Custom evaluation functions for conditional expressions.

Used for: when/readonly/disabled logic Return type: any value (typically boolean)

Example usage
customFunctions: {
  isAdult: (context) => context.age >= 18,
  calculateAge: (context) => {
    const birthDate = new Date(context.birthDate);
    return new Date().getFullYear() - birthDate.getFullYear();
  }
}
httpValidators
Record<string, HttpCustomValidator<unknown, unknown>> | undefined

HTTP validators using Angular's validateHttp() API

Angular's validateHttp provides HTTP validation with automatic request cancellation and integration with the resource API.

Structure:

  • request: Function that returns URL string or HttpResourceRequest
  • onSuccess: REQUIRED - Maps HTTP response to validation errors (inverted logic!)
  • onError: Optional handler for HTTP errors

Benefits:

  • Automatic request cancellation when field value changes
  • Built-in integration with Angular's resource management
  • Simpler than asyncValidators for HTTP use cases

Important: onSuccess uses inverted logic - it maps SUCCESSFUL HTTP responses to validation errors. For example, if the API returns { available: false }, your onSuccess should return { kind: 'usernameTaken' }.

Example usage

Username Availability Check (GET)

httpValidators: {
  checkUsername: {
    request: (ctx) => {
      const username = ctx.value();
      if (!username) return undefined; // Skip validation if empty
      return `/api/users/check-username?username=${encodeURIComponent(username)}`;
    },
    onSuccess: (response, ctx) => {
      // Inverted logic: successful response may indicate validation failure
      return response.available ? null : { kind: 'usernameTaken' };
    },
    onError: (error, ctx) => {
      console.error('Availability check failed:', error);
      return null; // Don't block form on network errors
    }
  }
}

Address Validation (POST with Body)

httpValidators: {
  validateAddress: {
    request: (ctx) => {
      const zipCode = ctx.value();
      if (!zipCode) return undefined;

      return {
        url: '/api/validate-address',
        method: 'POST',
        body: {
          street: ctx.valueOf('street' as any),
          city: ctx.valueOf('city' as any),
          zipCode: zipCode
        },
        headers: { 'Content-Type': 'application/json' }
      };
    },
    onSuccess: (response) => {
      return response.valid ? null : { kind: 'invalidAddress' };
    }
  }
}
validators
Record<string, CustomValidator> | undefined

Custom validators using Angular's public FieldContext API

(ctx, params?) => ValidationError | ValidationError[] | null

Validators receive FieldContext which provides access to:

  • Current field value: ctx.value()
  • Field state: ctx.state (errors, touched, dirty, etc.)
  • Other field values: ctx.valueOf(path) - public API!
  • Other field states: ctx.stateOf(path)
  • Parameters from JSON configuration

Return Types:

  • Single error: { kind: 'errorKind' } for field-level validation
  • Multiple errors: [{ kind: 'error1' }, { kind: 'error2' }] for cross-field validation
  • No error: null when validation passes
Example usage

Single Field Validation

validators: {
  noSpaces: (ctx) => {
    const value = ctx.value();
    if (typeof value === 'string' && value.includes(' ')) {
      return { kind: 'noSpaces' };
    }
    return null;
  }
}

Cross-Field Validation (Public API)

validators: {
  lessThan: (ctx, params) => {
    const value = ctx.value();
    const compareToPath = params?.field as string;

    // Use valueOf() to access other field - public API!
    const otherValue = ctx.valueOf(compareToPath as any);

    if (otherValue !== undefined && value >= otherValue) {
      return { kind: 'notLessThan' };
    }
    return null;
  }
}

Multiple Errors

validators: {
  validateDateRange: (ctx) => {
    const errors: ValidationError[] = [];
    const startDate = ctx.valueOf('startDate' as any);
    const endDate = ctx.valueOf('endDate' as any);

    if (!startDate) errors.push({ kind: 'startDateRequired' });
    if (!endDate) errors.push({ kind: 'endDateRequired' });
    if (startDate && endDate && startDate > endDate) {
      errors.push({ kind: 'invalidDateRange' });
    }

    return errors.length > 0 ? errors : null;
  }
}

Example usage

customFnConfig: {
  customFunctions: {
    isAdult: (context) => context.age >= 18,
    formatCurrency: (context) => new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(context.value)
  },
  simpleValidators: {
    noSpaces: (value) => {
      return typeof value === 'string' && value.includes(' ')
        ? { kind: 'noSpaces', message: 'Spaces not allowed' }
        : null;
    }
  },
  contextValidators: {
    lessThanField: (ctx, params) => {
      const value = ctx.value();
      const otherField = params?.field as string;
      const otherValue = ctx.root()[otherField]?.value();
      if (otherValue !== undefined && value >= otherValue) {
        return { kind: 'notLessThan', message: `Must be less than ${otherField}` };
      }
      return null;
    }
  }
}