Derive field values from HTTP responses or custom async functions. Async derivations are ideal for server-driven computed values, address/zip lookups, exchange rates, and any value that requires a network call.
HTTP Derivations
Use source: 'http' to derive a field value from an HTTP response. The request fires when declared dependencies change, with automatic debouncing and in-flight request cancellation.
Basic Example
{
key: 'exchangeRate',
type: 'input',
label: 'Exchange Rate',
readonly: true,
logic: [{
type: 'derivation',
source: 'http',
http: {
url: '/api/exchange-rate',
method: 'GET',
queryParams: {
currency: 'formValue.currency',
},
},
responseExpression: 'response.rate',
dependsOn: ['currency'],
}],
}When currency changes, a GET request is sent and response.rate becomes the new field value.
HttpRequestConfig
| Property | Type | Required | Description |
|---|---|---|---|
url |
string |
Yes | Request URL. Use :key placeholders for path parameters |
method |
string |
No | HTTP method. Defaults to 'GET' |
params |
Record |
No | Path parameters. Values are expressions evaluated against the form context |
queryParams |
Record |
No | Query parameters. Values are expressions evaluated against the form context |
body |
Record |
No | Request body (for POST/PUT/PATCH) |
evaluateBodyExpressions |
boolean |
No | When true, top-level string values in body are treated as expressions |
headers |
Record |
No | Request headers |
Path Parameters
Use :key placeholders in the URL and provide values via params. Values are expressions evaluated against the form context, then encodeURIComponent-encoded:
http: {
url: '/api/users/:userId/orders/:orderId',
params: {
userId: 'formValue.userId',
orderId: 'formValue.orderId',
},
}Query Params as Expressions
Values in queryParams are evaluated as expressions, giving you dynamic request parameters built from form values:
http: {
url: '/api/shipping-estimate',
queryParams: {
fromZip: 'formValue.originZip',
toZip: 'formValue.destinationZip',
weight: 'formValue.packageWeight',
},
}POST with Dynamic Body
For POST requests, use evaluateBodyExpressions: true to build the body from form values:
http: {
url: '/api/calculate-tax',
method: 'POST',
body: {
subtotal: 'formValue.subtotal',
state: 'formValue.state',
items: 'formValue.lineItems',
},
evaluateBodyExpressions: true,
},Top-level string values in body are evaluated as expressions. Nested objects are passed through as-is.
Extracting the Response Value
responseExpression is evaluated with { response } in scope and extracts the derived value:
responseExpression: 'response.rate'; // Simple property
responseExpression: 'response.data.price'; // Nested property
responseExpression: 'response.items[0].value'; // Array accessRequired Fields
source: 'http' enforces two fields at the TypeScript level:
dependsOn: string[]: Explicit field dependencies. The request only re-fires when these fields change.responseExpression: string: Expression to extract the derived value from the response body.
Controlling Request Timing
HTTP and async function derivations are always debounced, regardless of trigger. Requests wait for dependencies to stop changing (default: 300ms). Use debounceMs to adjust the delay:
{
type: 'derivation',
source: 'http',
http: {
url: '/api/username-suggest',
queryParams: { q: 'formValue.companyName' },
},
responseExpression: 'response.suggestion',
dependsOn: ['companyName'],
debounceMs: 400,
}The request waits 400ms after the user stops typing before firing.
Conditional HTTP Derivation
Add a condition to only send the request when certain criteria are met:
{
key: 'suggestedPrice',
type: 'input',
readonly: true,
logic: [{
type: 'derivation',
source: 'http',
http: {
url: '/api/price-suggest',
queryParams: { productId: 'formValue.productId' },
},
responseExpression: 'response.suggestedPrice',
dependsOn: ['productId'],
condition: {
type: 'fieldValue',
fieldPath: 'productId',
operator: 'notEquals',
value: '',
},
}],
}Async Function Derivations
Use source: 'asyncFunction' when you need custom logic: Angular service injection, complex transformations, or any async computation not expressible as a plain HTTP config. Functions receive the full evaluation context and can return a Promise or Observable.
Basic Example
// 1. Define and register the function in customFnConfig
const formConfig = {
customFnConfig: {
asyncDerivations: {
lookupCity: async (context) => {
const response = await fetch(`/api/geocode?zip=${context.formValue.zipCode}`);
const data = await response.json();
return data.city;
},
},
},
// 2. Reference it on the target field
fields: [
{
key: 'city',
type: 'input',
label: 'City',
readonly: true,
logic: [
{
type: 'derivation',
source: 'asyncFunction',
asyncFunctionName: 'lookupCity',
dependsOn: ['zipCode'],
},
],
},
],
} as const satisfies FormConfig;Using RxJS Observables
Functions can return an Observable instead of a Promise, which is useful when using Angular services:
customFnConfig: {
asyncDerivations: {
fetchAddress: (context) => {
return inject(AddressService)
.lookup(context.formValue.zipCode as string)
.pipe(map(result => result.formattedAddress));
},
},
},Available Context
Async derivation functions receive the full EvaluationContext:
| Variable | Description |
|---|---|
context.formValue |
All current form field values |
context.fieldValue |
The current value of the target field |
context.externalData |
External data passed to the form (if configured) |
context.rootFormValue |
Entire form value when inside array fields |
Conditional Async Derivation
Combine with condition to only run the function when needed:
{
key: 'city',
type: 'input',
label: 'City',
readonly: true,
logic: [{
type: 'derivation',
source: 'asyncFunction',
asyncFunctionName: 'lookupCity',
dependsOn: ['zipCode'],
condition: {
type: 'fieldValue',
fieldPath: 'enableLookup',
operator: 'equals',
value: true,
},
}],
}The async function is only called when enableLookup === true.
Required Fields
source: 'asyncFunction' requires:
- One of
asyncFunctionName: stringorasyncFn: AsyncDerivationFunction: see Inline alternative below. The two are mutually exclusive. dependsOn: string[]: Explicit field dependencies. Without this, the function would re-run on every form change.
Inline alternative (asyncFn)
When you don't need JSON-serializability, set asyncFn directly instead of registering the function in customFnConfig.asyncDerivations:
import type { AsyncDerivationFunction, FormConfig } from '@ng-forge/dynamic-forms';
import { inject } from '@angular/core';
import { map } from 'rxjs';
const lookupCity: AsyncDerivationFunction = (context) =>
inject(AddressService)
.lookup(context.formValue.zipCode as string)
.pipe(map((r) => r.city));
const formConfig = {
fields: [
{
key: 'city',
type: 'input',
readonly: true,
logic: [
{
type: 'derivation',
source: 'asyncFunction',
asyncFn: lookupCity,
dependsOn: ['zipCode'],
},
],
},
],
} as const satisfies FormConfig;Inject Angular services inside asyncFn the same way you would inside a registered function. Both run in the form's injection context.
Use asyncFunctionName for configs loaded over the wire (APIs, OpenAPI, MCP); use asyncFn for code-only configs. TypeScript rejects setting both keys, and the runtime warns + prefers asyncFn if a JSON config sneaks both through.
See also: the same XOR pattern applies to sync derivations (fn), conditions (fn / asyncFn), and validators.
Stop On User Override
stopOnUserOverride turns a derivation into a "smart default": the field is auto-filled initially, but derivation stops once the user manually edits it.
Basic Example
{
key: 'displayName',
type: 'input',
label: 'Display Name',
logic: [{
type: 'derivation',
expression: 'formValue.firstName + " " + formValue.lastName',
stopOnUserOverride: true,
}],
}The display name is auto-filled from first and last name. Once the user changes it directly, auto-fill stops.
Re-Engaging on Dependency Change
Use reEngageOnDependencyChange: true to clear the user override when a dependency changes. The derivation respects user edits, but re-engages if the underlying data changes:
{
key: 'phonePrefix',
type: 'input',
label: 'Phone Prefix',
logic: [
{
type: 'derivation',
value: '+1',
condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'US' },
stopOnUserOverride: true,
reEngageOnDependencyChange: true,
dependsOn: ['country'],
},
{
type: 'derivation',
value: '+44',
condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'UK' },
stopOnUserOverride: true,
reEngageOnDependencyChange: true,
dependsOn: ['country'],
},
],
}When the user changes the country, the phone prefix is auto-filled again, overriding any previous manual edit.
Without reEngageOnDependencyChange: once the user edits the field, the derivation never runs again for that field instance.
With reEngageOnDependencyChange: the user override is cleared when a declared dependency changes, and the derivation re-fires.
Works with All Sources
stopOnUserOverride and reEngageOnDependencyChange work across all derivation sources:
// HTTP derivation with user override
{
type: 'derivation',
source: 'http',
http: {
url: '/api/suggest-price',
queryParams: { category: 'formValue.category' },
},
responseExpression: 'response.suggestedPrice',
dependsOn: ['category'],
stopOnUserOverride: true,
reEngageOnDependencyChange: true,
}
// Async function derivation with user override
{
type: 'derivation',
source: 'asyncFunction',
asyncFunctionName: 'fetchDefaultTitle',
dependsOn: ['template'],
stopOnUserOverride: true,
}DerivationLogicConfig Reference (Async)
HTTP Derivation Fields
{
type: 'derivation';
source: 'http'; // Required — identifies HTTP mode
http: HttpRequestConfig; // Required — request configuration
dependsOn: string[]; // Required — explicit field dependencies
responseExpression: string; // Required — expression to extract value from response
// Shared optional fields
trigger?: 'onChange' | 'debounced'; // No effect here: HTTP requests are always debounced
debounceMs?: number; // Default: 300
condition?: ConditionalExpression | boolean;
stopOnUserOverride?: boolean;
reEngageOnDependencyChange?: boolean;
debugName?: string;
}Async Function Derivation Fields
{
type: 'derivation';
source: 'asyncFunction'; // Required — identifies async function mode
// Required — one of:
// • asyncFunctionName: string — name in customFnConfig.asyncDerivations (JSON-safe)
// • asyncFn: AsyncDerivationFunction — inline async function (code-only)
// The two are mutually exclusive (XOR) at the type level.
asyncFunctionName?: string;
asyncFn?: AsyncDerivationFunction;
dependsOn: string[]; // Required — explicit field dependencies
// Shared optional fields
trigger?: 'onChange' | 'debounced'; // No effect here: async functions are always debounced
debounceMs?: number; // Default: 300
condition?: ConditionalExpression | boolean;
stopOnUserOverride?: boolean;
reEngageOnDependencyChange?: boolean;
debugName?: string;
}Related
- Values: Expression, static value, and function-based derivations
- Properties: Derive component properties from form values
- HTTP Conditions: HTTP-driven field visibility and state
- Custom Validators: Async and HTTP validation