Control field behavior dynamically based on form state. Dynamic forms provides a declarative API for conditional visibility, required state, and readonly state that maps directly to Angular's signal forms.
Signal Forms Integration
The library integrates with Angular's signal forms logic functions:
import { hidden, readonly, required } from '@angular/forms/signals';All conditional logic configuration is applied using these functions, providing:
- Reactive updates when form state changes
- Type-safe conditional expressions
- Automatic re-evaluation on dependencies
- Integration with form validation state
Static Properties
Use simple boolean properties for fields with fixed states.
hidden
Hide a field from view (field still participates in form state):
{
key: 'internalId',
type: 'input',
value: '',
label: 'Internal ID',
hidden: true,
}The field is hidden from the UI but still included in the form value.
disabled
Disable user interaction:
{
key: 'systemField',
type: 'input',
value: 'auto-generated',
label: 'System Field',
disabled: true,
}Note: The disabled property is handled at the component level and does not use signal forms logic functions. It's a static UI property that prevents user interaction.
readonly
Make a field read-only (displays value but prevents modification):
{
key: 'createdAt',
type: 'input',
value: '2024-01-15',
label: 'Created Date',
readonly: true,
}Dynamic Conditional Logic
For conditional behavior based on form state, use the logic array with LogicConfig objects.
interface LogicConfig {
/** Logic type */
type: 'hidden' | 'readonly' | 'disabled' | 'required';
/** Boolean expression, static value, or form state condition */
condition: ConditionalExpression | boolean | FormStateCondition;
}FormStateCondition values ('formInvalid', 'formSubmitting', 'pageInvalid') are primarily used for button disabled logic.
Conditional Visibility (hidden)
Show or hide fields based on other field values.
Show Email When Contact Method is Email
{
key: 'contactMethod',
type: 'select',
value: '',
options: [
{ value: 'email', label: 'Email' },
{ value: 'phone', label: 'Phone' },
],
}
{
key: 'email',
type: 'input',
value: '',
label: 'Email Address',
logic: [{
type: 'hidden',
condition: {
type: 'fieldValue',
fieldPath: 'contactMethod',
operator: 'notEquals',
value: 'email',
},
}],
}When contactMethod !== 'email', the email field is hidden.
Conditional Required
Make fields required based on conditions.
Tax ID Required for Business Accounts
{
key: 'accountType',
type: 'radio',
value: 'personal',
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
],
}
{
key: 'taxId',
type: 'input',
value: '',
label: 'Tax ID',
logic: [{
type: 'required',
condition: {
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
},
}],
}Conditional Readonly
Make fields read-only based on conditions.
Lock Field After Submission
{
key: 'status',
type: 'select',
value: 'draft',
options: [
{ value: 'draft', label: 'Draft' },
{ value: 'submitted', label: 'Submitted' },
],
}
{
key: 'documentNumber',
type: 'input',
value: '',
label: 'Document Number',
logic: [{
type: 'readonly',
condition: {
type: 'fieldValue',
fieldPath: 'status',
operator: 'equals',
value: 'submitted',
},
}],
}Once status is "submitted", the document number becomes read-only.
Basic Conditional Expression
The most common conditional expression checks a specific field's value:
{
type: 'fieldValue',
fieldPath: 'fieldKey',
operator: 'equals',
value: 'expectedValue',
}Components:
type: 'fieldValue'- Check a specific fieldfieldPath- The field key to checkoperator- Comparison operator (see All Operators)value- Value to compare against
Quick Example
Show a field only when another field has a specific value:
{
key: 'email',
type: 'input',
value: '',
label: 'Email Address',
logic: [
{
type: 'hidden',
condition: {
type: 'fieldValue',
fieldPath: 'contactMethod',
operator: 'notEquals',
value: 'email',
},
},
{
type: 'required',
condition: {
type: 'fieldValue',
fieldPath: 'contactMethod',
operator: 'equals',
value: 'email',
},
},
],
}This field is hidden unless contactMethod === 'email', and required when visible. See the Examples page for complete form implementations.
When Logic Runs
Conditional logic is evaluated:
- On form value change - Any time a dependent field changes
- On initialization - When the form is created
- Reactively - Uses Angular's signal forms for automatic updates
Evaluation Flow
Expression Types
fieldValue
Check a specific field's value - the most common expression type.
{
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
}Use when: Checking a single field's value
Example:
{
key: 'companyName',
type: 'input',
value: '',
logic: [{
type: 'required',
condition: {
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
},
}],
}formValue
Compare the entire form value object against a specific value using operators.
{
type: 'formValue',
operator: 'equals',
value: { status: 'active', role: 'admin' },
}Use when: Checking if the entire form matches a specific state
Note: This type is rarely useful in practice — deep equality on an entire form object is an unusual requirement. For conditions that involve multiple specific fields, use javascript or custom expressions instead (e.g. formValue.status === 'active' && formValue.role === 'admin').
javascript
JavaScript expressions with access to fieldValue (current field) and formValue (entire form).
{
type: 'javascript',
expression: 'fieldValue > formValue.startDate',
}Use when: Custom logic on field value or complex multi-field conditions
Examples:
// Check current field against another field
{
key: 'endDate',
type: 'datepicker',
value: null,
logic: [{
type: 'readonly',
condition: {
type: 'javascript',
expression: 'fieldValue < formValue.startDate',
},
}],
}
// Check multiple form fields (replaces old formValue expression pattern)
{
key: 'stateProvince',
type: 'select',
value: '',
logic: [{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'formValue.country !== "US" && formValue.country !== "CA"',
},
}],
}Safe member access: Accessing nested properties on null or undefined returns undefined (no errors thrown):
{
type: 'javascript',
// Safe even when user, profile, or preferences is null/undefined
expression: 'formValue.user.profile.preferences.notifications === true',
}custom
Advanced custom expressions with access to both field and form values.
{
type: 'custom',
expression: 'fieldValue > formValue.minAge && fieldValue < formValue.maxAge',
}Safe member access: Like formValue expressions, nested property access is safe:
{
type: 'custom',
// Safe even when nested values are null/undefined
expression: 'fieldValue !== formValue.user.profile.firstName',
}Field State in Expressions
javascript and custom expressions have access to two additional variables for querying field interaction state:
fieldState— the current field's own stateformFieldState— state of any field in the form, by key
fieldState
Use fieldState to react to the current field's own interaction state:
// Lock the field as soon as the user edits it
{
key: 'accountNumber',
type: 'input',
logic: [{
type: 'readonly',
condition: {
type: 'javascript',
expression: 'fieldState.dirty',
},
}],
}Available properties:
| Property | Type | Description |
|---|---|---|
touched |
boolean |
User has focused and left the field |
dirty |
boolean |
User has changed the field value |
pristine |
boolean |
Equivalent to !dirty |
valid |
boolean |
Field has no validation errors |
invalid |
boolean |
Field has validation errors |
pending |
boolean |
Async validation is in progress |
hidden |
boolean |
Field is currently hidden |
readonly |
boolean |
Field is currently readonly |
disabled |
boolean |
Field is currently disabled |
formFieldState
Use formFieldState to react to another field's state. Access by field key:
// Make a field readonly once a related field has been touched
{
key: 'billingAddress',
type: 'input',
logic: [{
type: 'readonly',
condition: {
type: 'javascript',
expression: 'formFieldState.shippingAddress.dirty',
},
}],
}formFieldState has the same properties as fieldState, keyed by field name.
Example — show a confirmation field only after the primary field is dirty:
{
key: 'confirmEmail',
type: 'input',
label: 'Confirm Email',
logic: [{
type: 'hidden',
condition: {
type: 'javascript',
expression: '!formFieldState.email.dirty',
},
}],
}http
Evaluate a condition by sending an HTTP request and inspecting the response. The request fires automatically when declared query params change, with built-in debouncing.
{
type: 'http',
http: {
url: '/api/permissions',
queryParams: {
role: 'formValue.userRole',
},
},
responseExpression: 'response.canEdit',
pendingValue: false,
}Use when: Field visibility or state must be determined server-side (permissions, feature flags, country-specific rules).
Full example — hide an admin panel based on server permissions:
{
key: 'adminPanel',
type: 'input',
label: 'Admin Panel Access Code',
logic: [{
type: 'hidden',
condition: {
type: 'http',
http: {
url: '/api/permissions',
queryParams: {
role: 'formValue.userRole',
},
},
responseExpression: 'response.hideAdminPanel',
pendingValue: true, // Hide while checking
},
}],
}HTTP condition properties:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
type |
'http' |
Yes | — | Identifies this as an HTTP condition |
http |
HttpRequestConfig |
Yes | — | Request configuration (see below) |
responseExpression |
string |
No | !!response |
Expression evaluated with { response } in scope. Must return a boolean |
pendingValue |
boolean |
No | false |
Value returned while the request is in-flight |
cacheDurationMs |
number |
No | 30000 |
How long to cache responses (ms) |
debounceMs |
number |
No | 300 |
Debounce delay before re-sending (ms) |
HttpRequestConfig quick reference:
| Property | Description |
|---|---|
url |
Request URL. Use :key placeholders for path parameters |
method |
HTTP method. Defaults to 'GET' |
params |
Path parameters. :key placeholders in the URL replaced with evaluated expressions |
queryParams |
Key/value map. Values are expressions evaluated against form context |
body |
Request body for POST/PUT/PATCH |
evaluateBodyExpressions |
When true, top-level body string values are evaluated as expressions |
headers |
Request headers |
HTTP condition on required — server-driven required fields:
{
key: 'taxId',
type: 'input',
logic: [{
type: 'required',
condition: {
type: 'http',
http: {
url: '/api/tax-rules',
queryParams: { country: 'formValue.country' },
},
responseExpression: 'response.taxIdRequired',
pendingValue: false, // Optional while checking
},
}],
}async
Evaluate a condition using a custom async function registered in customFnConfig.asyncConditions. Functions receive the full form context and must return a Promise<boolean> or Observable<boolean>.
{
type: 'async',
asyncFunctionName: 'checkPermission',
pendingValue: false,
}Use when: Condition logic involves Angular service injection, complex async operations, or anything that http conditions cannot express directly.
Why
inject()works here:customFnConfigfunctions are called within an Angular injection context, so Angular'sinject()API is available — the same way it works in a constructor or field initializer. Importinjectfrom@angular/coreas usual.
Registration and usage:
import { inject } from '@angular/core';
const formConfig = {
customFnConfig: {
asyncConditions: {
checkReadonly: (context) => {
return inject(PermissionsService).canEdit(context.formValue.resourceId as string);
},
},
},
fields: [
{
key: 'salary',
type: 'input',
label: 'Salary',
logic: [
{
type: 'readonly',
condition: {
type: 'async',
asyncFunctionName: 'checkReadonly',
pendingValue: false, // Editable while checking
},
},
],
},
],
} as const satisfies FormConfig;Async condition properties:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
type |
'async' |
Yes | — | Identifies this as an async condition |
asyncFunctionName |
string |
Yes | — | Name registered in customFnConfig.asyncConditions |
pendingValue |
boolean |
No | false |
Value returned while the function is resolving |
debounceMs |
number |
No | 300 |
Debounce delay before re-evaluating (ms) |
Choosing pendingValue:
The right pendingValue depends on the logic type and desired UX:
| Logic type | pendingValue: false |
pendingValue: true |
|---|---|---|
hidden |
Visible while checking | Hidden while checking |
required |
Optional while checking | Required while checking |
readonly |
Editable while checking | Readonly while checking |
disabled |
Enabled while checking | Disabled while checking |
All Operators
Equality Operators
equals
Exact match comparison.
{
type: 'fieldValue',
fieldPath: 'status',
operator: 'equals',
value: 'active',
}notEquals
Not equal to comparison.
{
type: 'fieldValue',
fieldPath: 'status',
operator: 'notEquals',
value: 'archived',
}Comparison Operators
greater
Greater than comparison (numbers/dates).
{
type: 'fieldValue',
fieldPath: 'age',
operator: 'greater',
value: 18,
}less
Less than comparison.
{
type: 'fieldValue',
fieldPath: 'quantity',
operator: 'less',
value: 100,
}greaterOrEqual
Greater than or equal to.
{
type: 'fieldValue',
fieldPath: 'age',
operator: 'greaterOrEqual',
value: 21,
}lessOrEqual
Less than or equal to.
{
type: 'fieldValue',
fieldPath: 'discount',
operator: 'lessOrEqual',
value: 100,
}String Operators
contains
Check if string/array contains value.
{
type: 'fieldValue',
fieldPath: 'email',
operator: 'contains',
value: '@company.com',
}startsWith
Check if string starts with value.
{
type: 'fieldValue',
fieldPath: 'url',
operator: 'startsWith',
value: 'https://',
}endsWith
Check if string ends with value.
{
type: 'fieldValue',
fieldPath: 'email',
operator: 'endsWith',
value: '.gov',
}matches
Regular expression match.
{
type: 'fieldValue',
fieldPath: 'zipCode',
operator: 'matches',
value: '^[0-9]{5}$',
}Combining Conditions
AND Logic
All conditions must be true.
{
type: 'and',
conditions: [
{
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
},
{
type: 'fieldValue',
fieldPath: 'hasTeam',
operator: 'equals',
value: true,
},
{
type: 'fieldValue',
fieldPath: 'teamSize',
operator: 'greater',
value: 5,
},
],
}Use case: Field required when all conditions are met.
{
key: 'enterpriseFeatures',
type: 'checkbox',
label: 'Enable Enterprise Features',
logic: [{
type: 'hidden',
condition: {
type: 'and',
conditions: [
{
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
},
{
type: 'fieldValue',
fieldPath: 'plan',
operator: 'equals',
value: 'enterprise',
},
],
},
}],
}OR Logic
At least one condition must be true.
{
type: 'or',
conditions: [
{
type: 'fieldValue',
fieldPath: 'role',
operator: 'equals',
value: 'admin',
},
{
type: 'fieldValue',
fieldPath: 'role',
operator: 'equals',
value: 'owner',
},
],
}Use case: Show field for multiple roles — hide unless role is admin or owner.
// Hide the panel when role is neither 'admin' nor 'owner'
// (i.e. hidden when notEquals 'admin' AND notEquals 'owner')
{
key: 'adminPanel',
type: 'group',
label: 'Administration',
logic: [{
type: 'hidden',
condition: {
type: 'and',
conditions: [
{
type: 'fieldValue',
fieldPath: 'role',
operator: 'notEquals',
value: 'admin',
},
{
type: 'fieldValue',
fieldPath: 'role',
operator: 'notEquals',
value: 'owner',
},
],
},
}],
}Nested Logic
Combine AND/OR logic for complex conditions.
{
type: 'and',
conditions: [
{
type: 'fieldValue',
fieldPath: 'country',
operator: 'equals',
value: 'US',
},
{
type: 'or',
conditions: [
{
type: 'fieldValue',
fieldPath: 'age',
operator: 'greaterOrEqual',
value: 21,
},
{
type: 'fieldValue',
fieldPath: 'hasParentalConsent',
operator: 'equals',
value: true,
},
],
},
],
}This means: "Country must be US AND (age >= 21 OR has parental consent)"
Practical Examples
Show Field Based on Multiple Conditions
{
key: 'internationalShipping',
type: 'checkbox',
label: 'Enable International Shipping',
logic: [{
type: 'hidden',
condition: {
type: 'or',
conditions: [
{
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'free',
},
{
type: 'fieldValue',
fieldPath: 'verified',
operator: 'equals',
value: false,
},
],
},
}],
}Hidden for free accounts OR unverified accounts.
Required Field with Complex Logic
{
key: 'taxExemptionNumber',
type: 'input',
value: '',
label: 'Tax Exemption Number',
logic: [{
type: 'required',
condition: {
type: 'and',
conditions: [
{
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
},
{
type: 'fieldValue',
fieldPath: 'claimsTaxExemption',
operator: 'equals',
value: true,
},
{
type: 'or',
conditions: [
{
type: 'fieldValue',
fieldPath: 'country',
operator: 'equals',
value: 'US',
},
{
type: 'fieldValue',
fieldPath: 'country',
operator: 'equals',
value: 'CA',
},
],
},
],
},
}],
}Dynamic Read-Only Based on Status
{
key: 'orderItems',
type: 'group',
label: 'Order Items',
logic: [{
type: 'readonly',
condition: {
type: 'or',
conditions: [
{
type: 'fieldValue',
fieldPath: 'orderStatus',
operator: 'equals',
value: 'shipped',
},
{
type: 'fieldValue',
fieldPath: 'orderStatus',
operator: 'equals',
value: 'delivered',
},
{
type: 'fieldValue',
fieldPath: 'orderStatus',
operator: 'equals',
value: 'cancelled',
},
],
},
}],
}Order items become read-only once order is shipped, delivered, or cancelled.
Best Practices
Keep conditions readable:
// ✅ Good - Easy to understand
{
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
}
// ❌ Avoid - Hard to maintain
{
type: 'formValue',
expression: 'formValue.accountType === "business" && formValue.country !== null && formValue.hasTeam',
}ConditionalExpression Interface
ConditionalExpression is a discriminated union of all expression types:
// Sync expressions
type ConditionalExpression =
| { type: 'fieldValue'; fieldPath: string; operator: Operator; value: unknown }
| { type: 'formValue'; operator: Operator; value: unknown }
| { type: 'javascript'; expression: string }
| { type: 'custom'; expression: string }
| { type: 'and'; conditions: ConditionalExpression[] }
| { type: 'or'; conditions: ConditionalExpression[] }
// Async expressions
| HttpCondition
| AsyncCondition;
type Operator =
| 'equals'
| 'notEquals'
| 'greater'
| 'less'
| 'greaterOrEqual'
| 'lessOrEqual'
| 'contains'
| 'startsWith'
| 'endsWith'
| 'matches';
interface HttpCondition {
type: 'http';
http: HttpRequestConfig;
responseExpression?: string; // Evaluated with { response }. Defaults to !!response
pendingValue?: boolean; // Default: false
cacheDurationMs?: number; // Default: 30000
debounceMs?: number; // Default: 300
}
interface AsyncCondition {
type: 'async';
asyncFunctionName: string;
pendingValue?: boolean; // Default: false
debounceMs?: number; // Default: 300
}Expression types summary:
| Type | Sync/Async | Key properties | Purpose |
|---|---|---|---|
fieldValue |
Sync | fieldPath, operator, value |
Compare a specific field's value |
formValue |
Sync | operator, value |
Compare entire form object |
javascript |
Sync | expression |
Custom JS with fieldValue/formValue/fieldState/formFieldState |
custom |
Sync | expression |
Inline expression with fieldValue/formValue (safe member access) |
and/or |
Sync | conditions |
Combine multiple conditions |
http |
Async | http, responseExpression |
Server-driven condition via HTTP request |
async |
Async | asyncFunctionName |
Custom async function registered in config |
Common Patterns
Show/Hide Field Pattern
logic: [
{
type: 'hidden',
condition: {
/* when to hide */
},
},
];Conditional Required Pattern
logic: [
{
type: 'hidden',
condition: {
/* when to hide */
},
},
{
type: 'required',
condition: {
/* when to require */
},
},
];Multiple Conditions Pattern
logic: [
{
type: 'hidden',
condition: {
type: 'and', // or 'or'
conditions: [
{
/* condition 1 */
},
{
/* condition 2 */
},
],
},
},
];External Data in Conditions
Access external application state (user roles, permissions, feature flags) in conditions using externalData:
const config = {
externalData: {
userRole: computed(() => authService.role()),
featureFlags: computed(() => ({
advancedMode: featureService.isAdvanced(),
})),
},
fields: [
{
key: 'adminNotes',
type: 'textarea',
label: 'Admin Notes',
logic: [
{
type: 'hidden',
condition: {
type: 'javascript',
expression: "externalData.userRole !== 'admin'",
},
},
],
},
{
key: 'advancedSettings',
type: 'input',
label: 'Advanced Settings',
logic: [
{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'externalData.featureFlags.advancedMode !== true',
},
},
],
},
],
} as const satisfies FormConfig;Key points:
- Each property in
externalDatamust be a Signal (signal()orcomputed()) - Changes to external signals automatically re-evaluate conditions
- Access values via
externalData.propertyNamein JavaScript expressions
Related
- Value Derivation — Computed field values
- Async Derivation — HTTP and async function derivations, stopOnUserOverride
- Validation — Conditional validation
- Custom Validators — Async and HTTP validators
- Type Safety — TypeScript integration
- Examples — Real-world form patterns