Reactively derive field component properties (like minDate, options, label, placeholder) based on form values. While value derivations set a field's form value, property derivations set component input properties.
Quick Start
Property derivations use type: 'derivation' with targetProperty in the logic array and are self-targeting: the logic is placed on the field whose property should be derived.
{
key: 'endDate',
type: 'datepicker',
label: 'End Date',
logic: [{
type: 'derivation',
targetProperty: 'minDate',
expression: 'formValue.startDate',
}]
}When startDate changes, the minDate property on endDate's datepicker component is automatically updated.
Derivation Sources
Property derivations support five mutually exclusive ways to compute a value: synchronous (value, expression, functionName/fn) and asynchronous (source: 'http', source: 'asyncFunction').
Expression-Based
Use JavaScript expressions with access to formValue:
{
key: 'endDate',
type: 'datepicker',
label: 'End Date',
logic: [{
type: 'derivation',
targetProperty: 'minDate',
expression: 'formValue.startDate',
}]
}Available variables:
formValue- Object containing all form field valuesexternalData- External application state (when configured in FormConfig)
Static Value
Set a constant property value when a condition is met:
{
key: 'phone',
type: 'input',
label: 'Phone',
logic: [{
type: 'derivation',
targetProperty: 'label',
value: 'Mobile Phone',
condition: {
type: 'fieldValue',
fieldPath: 'contactType',
operator: 'equals',
value: 'mobile',
},
}]
}Custom Function
Use a registered function for complex logic:
// In form config
customFnConfig: {
derivations: {
getCitiesForCountry: (ctx) => {
const cities: Record<string, { label: string; value: string }[]> = {
'US': [{ label: 'New York', value: 'nyc' }, { label: 'LA', value: 'la' }],
'DE': [{ label: 'Berlin', value: 'berlin' }],
};
return cities[ctx.formValue.country as string] ?? [];
},
},
},
fields: [
{
key: 'city',
type: 'select',
label: 'City',
options: [],
logic: [{
type: 'derivation',
targetProperty: 'options',
functionName: 'getCitiesForCountry',
dependsOn: ['country'],
}],
},
],HTTP
Use source: 'http' to derive a property from an HTTP response — typically used to populate a select's options from a backend search endpoint. The request fires when any dependsOn field changes, with automatic in-flight cancellation when dependencies change again.
{
key: 'streetDropdown',
type: 'select',
options: [],
label: 'Street Suggestions',
logic: [{
type: 'derivation',
source: 'http',
targetProperty: 'options',
http: {
url: '/api/address/streets/search',
method: 'GET',
queryParams: { q: 'formValue.street' },
},
responseExpression: 'response.map(d => ({ value: d.id, label: d.streetNameShort }))',
dependsOn: ['street'],
trigger: 'debounced',
debounceMs: 300,
}],
}source: 'http'is required.dependsOnis required and must be non-empty — wildcards ('*') are rejected.responseExpressionis evaluated against{ response }. Object literals and arrow functions are supported, so you can map response rows into theFieldOptionshape inline.trigger: 'debounced'with adebounceMsis strongly recommended for autocomplete-style inputs to avoid spamming the server on every keystroke.- Requires
provideHttpClient()in your application providers.
See Async Derivations → HTTP for the full request configuration (path params, headers, POST bodies, conditional firing).
Async Function
Use source: 'asyncFunction' when you need custom client-side logic — Angular service injection, complex transformations, or any async computation that's not a plain HTTP call. The function receives the full EvaluationContext and can return a Promise or Observable.
import { inject } from '@angular/core';
import { map } from 'rxjs';
import type { AsyncDerivationFunction, FormConfig } from '@ng-forge/dynamic-forms';
const fetchCities: AsyncDerivationFunction = (context) =>
inject(CityService)
.search(context.formValue.country as string)
.pipe(map((rows) => rows.map((r) => ({ value: r.code, label: r.name }))));
const config = {
fields: [
{
key: 'city',
type: 'select',
options: [],
label: 'City',
logic: [
{
type: 'derivation',
source: 'asyncFunction',
targetProperty: 'options',
asyncFn: fetchCities,
dependsOn: ['country'],
},
],
},
],
} as const satisfies FormConfig;For JSON-serializable configs (API/OpenAPI/MCP), register the function in customFnConfig.asyncDerivations and reference it by name:
customFnConfig: {
asyncDerivations: {
fetchCities: (ctx) => inject(CityService).search(ctx.formValue.country as string),
},
},
// then on the field:
logic: [{
type: 'derivation',
source: 'asyncFunction',
targetProperty: 'options',
asyncFunctionName: 'fetchCities',
dependsOn: ['country'],
}],The same XOR rule applies as elsewhere in the library: asyncFn and asyncFunctionName are mutually exclusive.
Target Properties
Simple Properties
Set any direct component input property:
targetProperty: 'minDate'; // Date constraint
targetProperty: 'maxDate'; // Date constraint
targetProperty: 'options'; // Select/radio options
targetProperty: 'label'; // Field label
targetProperty: 'placeholder'; // Input placeholder
targetProperty: 'hint'; // Hint text
targetProperty: 'rows'; // Textarea rowsNested Properties (Dot Notation)
Set properties nested one level deep using dot notation:
targetProperty: 'props.appearance'; // Material appearance
targetProperty: 'props.color'; // Material color
targetProperty: 'meta.autocomplete'; // Custom metadataNote: Maximum nesting depth is 2 levels (one dot). Deeper paths like
props.nested.deepare not supported.
Trigger Timing
Control when property derivations evaluate:
| Trigger | Description | Use Case |
|---|---|---|
onChange |
Immediately on value change (default) | Date constraints, dynamic options |
debounced |
After value stabilizes | Expensive lookups, search queries |
Debounced Property Derivations
Use trigger: 'debounced' for expensive operations:
{
key: 'productSearch',
type: 'select',
label: 'Product',
options: [],
logic: [{
type: 'derivation',
targetProperty: 'options',
functionName: 'searchProducts',
trigger: 'debounced',
debounceMs: 300,
dependsOn: ['searchQuery'],
}]
}Conditional Property Derivations
Only apply property derivations when conditions are met:
{
key: 'email',
type: 'input',
label: 'Email',
logic: [
{
type: 'derivation',
targetProperty: 'label',
value: 'Work Email',
condition: {
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'business',
},
},
{
type: 'derivation',
targetProperty: 'label',
value: 'Personal Email',
condition: {
type: 'fieldValue',
fieldPath: 'accountType',
operator: 'equals',
value: 'personal',
},
},
],
}Dependencies
Automatic Detection
For expressions, dependencies are automatically extracted:
{
key: 'endDate',
type: 'datepicker',
label: 'End Date',
logic: [{
type: 'derivation',
targetProperty: 'minDate',
expression: 'formValue.startDate',
// Automatically depends on: startDate
}]
}Explicit Dependencies
For custom functions, specify dependencies explicitly:
{
key: 'city',
type: 'select',
label: 'City',
options: [],
logic: [{
type: 'derivation',
targetProperty: 'options',
functionName: 'getCitiesForCountry',
dependsOn: ['country'],
}]
}Without dependsOn, custom functions re-evaluate on any form value change.
Array Field Property Derivations
Inside arrays, formValue is scoped to the current array item:
{
key: 'lineItems',
type: 'array',
fields: [
{
key: 'item',
type: 'group',
fields: [
{ key: 'startDate', type: 'datepicker', label: 'Start' },
{
key: 'endDate',
type: 'datepicker',
label: 'End',
logic: [{
type: 'derivation',
targetProperty: 'minDate',
// formValue is scoped to the current array item
expression: 'formValue.startDate',
}]
},
],
},
],
}Each array item independently derives its own endDate.minDate from its own startDate.
Complete Example
const travelForm = {
customFnConfig: {
derivations: {
getCitiesForCountry: (ctx) => {
const cities: Record<string, { label: string; value: string }[]> = {
US: [
{ label: 'New York', value: 'nyc' },
{ label: 'Los Angeles', value: 'la' },
{ label: 'Chicago', value: 'chi' },
],
DE: [
{ label: 'Berlin', value: 'berlin' },
{ label: 'Munich', value: 'munich' },
],
};
return cities[ctx.formValue.country as string] ?? [];
},
},
},
fields: [
{
key: 'country',
type: 'select',
label: 'Country',
required: true,
options: [
{ label: 'USA', value: 'US' },
{ label: 'Germany', value: 'DE' },
],
},
{
key: 'city',
type: 'select',
label: 'City',
required: true,
options: [],
logic: [
{
type: 'derivation',
targetProperty: 'options',
functionName: 'getCitiesForCountry',
dependsOn: ['country'],
},
],
},
{ key: 'startDate', type: 'datepicker', label: 'Travel Start', required: true },
{
key: 'endDate',
type: 'datepicker',
label: 'Travel End',
required: true,
logic: [
{
type: 'derivation',
targetProperty: 'minDate',
expression: 'formValue.startDate',
},
],
},
{
key: 'notes',
type: 'textarea',
label: 'Notes',
logic: [
{
type: 'derivation',
targetProperty: 'props.appearance',
value: 'fill',
condition: {
type: 'fieldValue',
fieldPath: 'country',
operator: 'equals',
value: 'DE',
},
},
{
type: 'derivation',
targetProperty: 'props.appearance',
value: 'outline',
condition: {
type: 'fieldValue',
fieldPath: 'country',
operator: 'notEquals',
value: 'DE',
},
},
],
},
{ key: 'submit', type: 'submit', label: 'Book Trip' },
],
} as const satisfies FormConfig;Comparison with Value Derivation
| Aspect | Value Derivation | Property Derivation |
|---|---|---|
| Logic type | type: 'derivation' |
type: 'derivation' + targetProperty |
| Sets | Field's form value | Component input property |
| Target | Implicit (self) | targetProperty: 'minDate' |
| Shorthand | derivation: 'expr' |
None (must use logic block) |
| Chaining | Topologically sorted | No chaining (single pass) |
| Custom functions | customFnConfig.derivations |
customFnConfig.derivations |
| Max iterations | Configurable (default 10) | Single pass |
Debugging
Add debugName to property derivations for easier identification in logs:
logic: [
{
type: 'derivation',
debugName: 'endDate minDate constraint',
targetProperty: 'minDate',
expression: 'formValue.startDate',
},
];DerivationLogicConfig Interface
Both value derivations and property derivations use the unified DerivationLogicConfig. The presence of targetProperty is what distinguishes a property derivation from a value derivation.
interface DerivationLogicConfig {
/** Logic type identifier (unified for both value and property derivations) */
type: 'derivation';
/** Property to set on the field component. When present, this is a property derivation. When absent, this is a value derivation. */
targetProperty?: string;
/** 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 (mutually exclusive) */
value?: unknown;
/** JavaScript expression (mutually exclusive) */
expression?: string;
/** Name of registered custom function (mutually exclusive) */
functionName?: string;
/** Explicit field dependencies */
dependsOn?: string[];
/** Condition for when derivation applies (default: true) */
condition?: ConditionalExpression | boolean;
}External Data in Property Derivations
Use external application state in property derivation custom functions:
const config = {
externalData: {
userRegion: computed(() => this.regionService.current()),
},
customFnConfig: {
derivations: {
getCurrencyOptions: (ctx) => {
const optionsByRegion: Record<string, { label: string; value: string }[]> = {
EU: [
{ label: 'EUR', value: 'eur' },
{ label: 'GBP', value: 'gbp' },
],
US: [
{ label: 'USD', value: 'usd' },
{ label: 'CAD', value: 'cad' },
],
};
return optionsByRegion[ctx.externalData.userRegion as string] ?? [];
},
},
},
fields: [
{
key: 'currency',
type: 'select',
label: 'Currency',
options: [],
logic: [
{
type: 'derivation',
targetProperty: 'options',
functionName: 'getCurrencyOptions',
},
],
},
],
} as const satisfies FormConfig;External data values are reactively tracked - when signals change, property derivations are re-evaluated.
Related
- Value Derivation (see tab above) - Compute field form values
- Conditional Logic - Control field visibility and state
- Array Fields - Working with array fields