Automatically compute and set field values based on other form values. Derivations enable calculated fields, auto-fill patterns, and value transformations.

Quick Start

Derivations are defined ON the field that receives the computed value (self-targeting).

Shorthand Syntax (Preferred)

{
  key: 'total',
  type: 'input',
  label: 'Total',
  readonly: true,
  derivation: 'formValue.quantity * formValue.unitPrice',
}

Logic Block Syntax

{
  key: 'total',
  type: 'input',
  label: 'Total',
  readonly: true,
  logic: [{
    type: 'derivation',
    expression: 'formValue.quantity * formValue.unitPrice',
  }],
}

When quantity or unitPrice changes, total is automatically recalculated.

Derivation Types

Expression-Based

Use JavaScript expressions with access to formValue:

{
  key: 'fullName',
  type: 'input',
  label: 'Full Name',
  readonly: true,
  derivation: '(formValue.firstName || "") + " " + (formValue.lastName || "")',
}

Available variables:

  • formValue - Object containing all form field values
  • externalData - External application state (when configured in FormConfig)

Static Value

Set a constant value when a condition is met:

{
  key: 'phonePrefix',
  type: 'input',
  label: 'Phone Prefix',
  readonly: true,
  logic: [{
    type: 'derivation',
    value: '+1',
    condition: {
      type: 'fieldValue',
      fieldPath: 'country',
      operator: 'equals',
      value: 'USA',
    },
  }],
}

Custom Function

Use a registered function for complex logic:

// In form config
customFnConfig: {
  derivations: {
    calculateTax: (ctx) => ctx.formValue.subtotal * getTaxRate(ctx.formValue.state),
  },
},
fields: [
  {
    key: 'tax',
    type: 'input',
    readonly: true,
    logic: [{
      type: 'derivation',
      functionName: 'calculateTax',
      dependsOn: ['subtotal', 'state'],
    }],
  },
],

Inline alternative (fn)

For code-only configs you can attach the function directly via fn and skip the customFnConfig.derivations registration. fn is mutually exclusive with functionName — TypeScript rejects setting both, and the runtime logs a warning + prefers fn if a JSON config sets both keys.

import type { CustomFunction } from '@ng-forge/dynamic-forms';

// getTaxRate is a TS helper from your codebase — the inline fn closes over
// it directly, no need to register or serialize it.
const calculateTax: CustomFunction = (ctx) => ctx.formValue.subtotal * getTaxRate(ctx.formValue.state);

const config = {
  fields: [
    {
      key: 'tax',
      type: 'input',
      readonly: true,
      logic: [
        {
          type: 'derivation',
          fn: calculateTax,
          dependsOn: ['subtotal', 'state'], // see "Default dependencies" below
        },
      ],
    },
  ],
};

Default dependencies for function derivations: if you omit dependsOn, both functionName and fn derivations default to a wildcard ('*'), meaning the derivation re-runs on every form change. In development, the library logs a one-time Derivation: custom functions without explicit dependsOn detected warning so you can narrow it down. Specify dependsOn whenever you know the exact inputs to avoid this.

Use functionName when the config must survive JSON serialization (APIs, OpenAPI, databases, MCP). Use fn for code-only authoring where you'd rather not maintain a separate registry — especially when the function closes over TS enums, constants, or class-scoped helpers that don't round-trip through a string registry.

See also: the same XOR pattern applies to async derivations (asyncFn), conditions (fn / asyncFn), and validators. See Configuration for setting up customFnConfig.

Trigger Timing

Control when derivations evaluate:

Trigger Description Use Case
onChange Immediately on value change (default) Computed totals, conditional prefixes
debounced After value stabilizes Self-transforms, format masking

Debounced Derivations

Use trigger: 'debounced' for self-transforming fields to avoid interrupting the user while typing:

{
  key: 'email',
  type: 'input',
  label: 'Email',
  logic: [{
    type: 'derivation',
    expression: 'formValue.email.toLowerCase()',
    trigger: 'debounced',
    debounceMs: 500, // optional, defaults to 500
  }],
}

The transformation applies after the user stops typing for 500ms.

Conditional Derivations

Only apply derivations when conditions are met:

{
  key: 'currency',
  type: 'input',
  label: 'Currency',
  readonly: true,
  logic: [
    {
      type: 'derivation',
      value: 'USD',
      condition: {
        type: 'fieldValue',
        fieldPath: 'country',
        operator: 'equals',
        value: 'USA',
      },
    },
    {
      type: 'derivation',
      value: 'EUR',
      condition: {
        type: 'fieldValue',
        fieldPath: 'country',
        operator: 'equals',
        value: 'Germany',
      },
    },
  ],
}

Multiple derivations on the same field are evaluated in order.

Dependencies

Automatic Detection

For expressions, dependencies are automatically extracted:

{
  key: 'total',
  type: 'input',
  label: 'Total',
  readonly: true,
  derivation: 'formValue.quantity * formValue.unitPrice',
  // Automatically depends on: quantity, unitPrice
}

Explicit Dependencies

For custom functions, specify dependencies explicitly:

{
  key: 'discount',
  type: 'input',
  label: 'Discount',
  readonly: true,
  logic: [{
    type: 'derivation',
    functionName: 'calculateDiscount',
    dependsOn: ['total', 'memberLevel'],
  }],
}

Without dependsOn, custom functions re-evaluate on any form change.

Complete Example

const orderForm = {
  fields: [
    {
      key: 'quantity',
      type: 'input',
      value: 1,
      label: 'Quantity',
      props: { type: 'number' },
    },
    {
      key: 'unitPrice',
      type: 'input',
      value: 10,
      label: 'Unit Price',
      props: { type: 'number' },
    },
    {
      key: 'subtotal',
      type: 'input',
      value: 0,
      label: 'Subtotal',
      readonly: true,
      derivation: 'formValue.quantity * formValue.unitPrice',
    },
    {
      key: 'tax',
      type: 'input',
      value: 0,
      label: 'Tax (10%)',
      readonly: true,
      derivation: 'formValue.subtotal * 0.1',
    },
    {
      key: 'total',
      type: 'input',
      value: 0,
      label: 'Total',
      readonly: true,
      derivation: 'formValue.subtotal + formValue.tax',
    },
  ],
} as const satisfies FormConfig;

Array Field Derivations

Inside arrays, formValue is scoped to the current array item:

{
  key: 'lineItems',
  type: 'array',
  fields: [
    {
      key: 'itemRow',
      type: 'row',
      fields: [
        { key: 'quantity', type: 'input', label: 'Qty', value: 1 },
        { key: 'unitPrice', type: 'input', label: 'Price', value: 0 },
        {
          key: 'lineTotal',
          type: 'input',
          label: 'Total',
          value: 0,
          readonly: true,
          // formValue is scoped to the current array item
          derivation: 'formValue.quantity * formValue.unitPrice',
        },
      ],
    },
  ],
}

Accessing Root Form Value

Inside array items, formValue refers to the current array item. Use rootFormValue to access the entire form:

{
  key: 'lineTotal',
  type: 'input',
  label: 'Total',
  readonly: true,
  // formValue = current array item { quantity, unitPrice }
  // rootFormValue = entire form { lineItems, discount, ... }
  derivation: 'formValue.quantity * formValue.unitPrice * (1 - rootFormValue.discount / 100)',
}

Debugging Derivations

Enable derivation logging to troubleshoot issues:

Configuration

Configure derivation logging via withLoggerConfig:

// In your providers
provideDynamicForm(...withMaterialFields(), withLoggerConfig({ derivations: 'verbose' }));

// Or just summary level
provideDynamicForm(...withMaterialFields(), withLoggerConfig({ derivations: 'summary' }));

Log Levels

Level Output
none No logging (default)
summary Cycle completion with counts
verbose Individual derivation evaluations

Using debugName

Add names to derivations for easier identification in logs:

{
  key: 'lineTotal',
  type: 'input',
  label: 'Total',
  readonly: true,
  logic: [{
    type: 'derivation',
    debugName: 'Calculate line total',
    expression: 'formValue.quantity * formValue.unitPrice',
  }],
}

Console output (verbose mode):

Derivation - Starting cycle (onChange) with 5 derivation(s)
Derivation - Iteration 1
Derivation - Applied "Calculate line total" { field: 'lineTotal', newValue: 150 }
Derivation - Skipped: phonePrefix (condition not met)
Derivation - Cycle complete (onChange) { applied: 1, skipped: 4, errors: 0, iterations: 1 }

Bidirectional Derivations

Create two-way bindings between fields:

// Celsius to Fahrenheit
{
  key: 'fahrenheit',
  type: 'input',
  value: 32,
  derivation: 'formValue.celsius * 9 / 5 + 32',
}

// Fahrenheit to Celsius
{
  key: 'celsius',
  type: 'input',
  value: 0,
  derivation: '(formValue.fahrenheit - 32) * 5 / 9',
}

Cycle Detection

The system automatically detects derivation cycles and warns during development:

Bidirectional derivation detected: celsius <-> fahrenheit
Bidirectional derivations stabilize via equality check.

Floating-Point Precision

Bidirectional derivations stabilize when the computed value equals the current value. For floating-point operations, consider:

  1. Rounding in expressions:

    derivation: 'Math.round(formValue.usd * exchangeRate * 100) / 100';
  2. Using integers: Store cents instead of dollars

  3. One-way derivation: If bidirectional isn't required

Derivation Processing

Evaluation Order

Derivations are topologically sorted based on dependencies:

Derivation Dependency Graph

Fields are topologically sorted so dependencies resolve first

1
quantity×unitPrice
subtotal
2
subtotal+tax
total

This ensures subtotal is computed before total.

Iteration Limits

To prevent infinite loops, derivations are limited to 10 iterations per cycle. If exceeded:

Derivation - Max iterations reached (onChange).
This may indicate a loop in derivation logic.

DerivationLogicConfig Interface

interface DerivationLogicConfig {
  /** Logic type identifier */
  type: 'derivation';

  /** Optional name for debugging */
  debugName?: string;

  /** When to evaluate: 'onChange' (default) or 'debounced' */
  trigger?: 'onChange' | 'debounced';

  /** Debounce duration in ms (default: 500) */
  debounceMs?: number;

  /** Static value to set */
  value?: unknown;

  /** JavaScript expression (has access to formValue) */
  expression?: string;

  /** Name of registered custom function. Mutually exclusive with `fn`. */
  functionName?: string;

  /** Inline custom function (code-only). Mutually exclusive with `functionName`. */
  fn?: CustomFunction;

  /** Explicit field dependencies */
  dependsOn?: string[];

  /** Condition for when derivation applies */
  condition?: ConditionalExpression | boolean;
}

External Data in Derivations

Use external application state in derivation expressions:

const config = {
  externalData: {
    discountRate: computed(() => pricingService.currentDiscount()),
  },
  fields: [
    {
      key: 'discountedPrice',
      type: 'input',
      label: 'Discounted Price',
      readonly: true,
      derivation: 'formValue.price * (1 - externalData.discountRate)',
    },
  ],
} as const satisfies FormConfig;

External data values are reactively tracked - when signals change, derivations are re-evaluated.

Next Steps

  • Conditional Logic — Control field visibility, required state, and disabled state dynamically
  • i18n — Translate labels and messages with Observables and Signals
  • Expression Parser — Understand the security model behind derivation expressions
  • Property Derivation (see tab above) — Derive component properties (minDate, options, label) from form values
  • Async Derivation (see tab above) — HTTP and async function derivations, stopOnUserOverride
  • Array Fields — Working with array fields
  • Examples — Real-world form patterns