Dynamic Behavior Conditional Logic

Conditional Logic

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 field
  • fieldPath - The field key to check
  • operator - 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

┌─────────────────────────────────────────────────────────────┐
│                    LOGIC EVALUATION FLOW                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Form Value Changes                                         │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────┐                                       │
│  │ Signal Updates  │◄──── Angular's reactive system        │
│  └────────┬────────┘                                       │
│           │                                                 │
│           ▼                                                 │
│  ┌─────────────────┐                                       │
│  │ Evaluate logic  │                                       │
│  │    conditions   │                                       │
│  └────────┬────────┘                                       │
│           │                                                 │
│     ┌─────┴─────┐                                          │
│     ▼           ▼                                          │
│  ┌──────┐   ┌──────┐                                       │
│  │ true │   │false │                                       │
│  └──┬───┘   └──┬───┘                                       │
│     │          │                                            │
│     ▼          ▼                                            │
│  Apply      Remove                                          │
│  effect     effect                                          │
│  (hide,     (show,
│  require,   optional,
│  readonly)  editable)                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

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: For complex logic involving multiple fields with JavaScript expressions, use javascript or custom type instead.

javascript

JavaScript expressions with access to fieldValue (current field) and formValue (entire form).

{
  type: 'javascript',
  expression: 'new Date(fieldValue) > new Date()',
}

Use when: Custom logic on field value or complex multi-field conditions

Examples:

// Check current field value
{
  key: 'eventDate',
  type: 'datepicker',
  value: null,
  logic: [{
    type: 'readonly',
    condition: {
      type: 'javascript',
      expression: 'new Date(fieldValue) < new Date()',
    },
  }],
}

// 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',
}

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.

{
  key: 'adminPanel',
  type: 'group',
  label: 'Administration',
  logic: [{
    type: 'hidden',
    condition: {
      type: 'or',
      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

interface ConditionalExpression {
  /** Expression type - includes 'and' and 'or' for combining conditions */
  type: 'fieldValue' | 'formValue' | 'javascript' | 'custom' | 'and' | 'or';

  /** Field path for fieldValue type */
  fieldPath?: string;

  /**
   * Comparison operator
   * - For 'fieldValue': compares field at fieldPath against value
   * - For 'formValue': compares entire form object against value
   */
  operator?:
    | 'equals'
    | 'notEquals'
    | 'greater'
    | 'less'
    | 'greaterOrEqual'
    | 'lessOrEqual'
    | 'contains'
    | 'startsWith'
    | 'endsWith'
    | 'matches';

  /** Value to compare against (for fieldValue/formValue with operator) */
  value?: unknown;

  /**
   * JavaScript expression string
   * - For 'javascript': Has access to fieldValue and formValue
   * - For 'custom': Name of registered custom function
   */
  expression?: string;

  /** Array of sub-conditions for 'and' and 'or' types */
  conditions?: ConditionalExpression[];
}

Expression types summary:

TypeUsesPurpose
fieldValuefieldPath, operator, valueCompare a specific field's value
formValueoperator, valueCompare entire form object
javascriptexpressionCustom JS with fieldValue/formValue access
customexpressionCall registered custom function
and/orconditionsCombine multiple conditions

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 */
        },
      ],
    },
  },
];