@ng-forge/dynamic-form-mcp 1.3.0-next.2 → 1.3.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ng-forge-mcp.js
CHANGED
|
@@ -1578,6 +1578,19 @@ var UI_ADAPTERS = [
|
|
|
1578
1578
|
default: false
|
|
1579
1579
|
}
|
|
1580
1580
|
}
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
type: "datepicker",
|
|
1584
|
+
componentName: "IonicDatepickerFieldComponent",
|
|
1585
|
+
additionalProps: {
|
|
1586
|
+
locale: {
|
|
1587
|
+
name: "locale",
|
|
1588
|
+
type: "string",
|
|
1589
|
+
description: "BCP 47 locale for the picker and the displayed value",
|
|
1590
|
+
required: false,
|
|
1591
|
+
default: "browser locale"
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1581
1594
|
}
|
|
1582
1595
|
]
|
|
1583
1596
|
}
|
|
@@ -3058,6 +3071,7 @@ Use \`options.fieldWindowing\` for large flat forms:
|
|
|
3058
3071
|
- \`park.margin\` follows \`IntersectionObserver.rootMargin\`: use one to four \`px\` or \`%\` values. Unsupported units fall back to the inherited margin.
|
|
3059
3072
|
- \`fieldWindowing: false\` disables inherited deferred mounting for that form.
|
|
3060
3073
|
- A park-only object changes parking without changing the inherited mounting mode. Use \`{ park: false }\` to opt out or \`{ park: true }\` to opt in.
|
|
3074
|
+
- Deferred fields show an empty box. To show a skeleton, project \`<ng-template dfPlaceholder let-field>\` into the form template (import \`DfPlaceholder\`). \`dfPlaceholderKey="key"\` beats \`dfPlaceholder="type"\`, which beats an empty \`dfPlaceholder\`. This is template-only, not part of the config.
|
|
3061
3075
|
|
|
3062
3076
|
## Validation
|
|
3063
3077
|
|
|
@@ -3191,6 +3205,7 @@ Compares a specific field's value:
|
|
|
3191
3205
|
- \`equals\`, \`notEquals\`
|
|
3192
3206
|
- \`greater\`, \`less\`, \`greaterOrEqual\`, \`lessOrEqual\`
|
|
3193
3207
|
- \`contains\`, \`startsWith\`, \`endsWith\`, \`matches\`
|
|
3208
|
+
- An empty value (null, undefined or '') never satisfies the ordered or string operators. \`matches\` tests it as '', and 0 and false are not empty.
|
|
3194
3209
|
|
|
3195
3210
|
#### JavaScript Condition
|
|
3196
3211
|
|
|
@@ -6658,6 +6673,13 @@ var PrimeFormConfigSchema = createFormConfigSchema(PrimeFieldSchema, PrimeFormDe
|
|
|
6658
6673
|
|
|
6659
6674
|
// internal/dynamic-forms-validation/ionic/src/props/index.ts
|
|
6660
6675
|
import { z as z53 } from "zod";
|
|
6676
|
+
function isLocaleTag(value) {
|
|
6677
|
+
try {
|
|
6678
|
+
return Intl.getCanonicalLocales(value).length > 0;
|
|
6679
|
+
} catch {
|
|
6680
|
+
return false;
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6661
6683
|
var IonicColorSchema = z53.enum(["primary", "secondary", "tertiary", "success", "warning", "danger", "light", "medium", "dark"]);
|
|
6662
6684
|
var IonicLabelPlacementSchema = z53.enum(["start", "end", "fixed", "stacked", "floating"]);
|
|
6663
6685
|
var IonicJustifySchema = z53.enum(["start", "end", "space-between"]);
|
|
@@ -6719,6 +6741,7 @@ var IonicDatepickerPropsSchema = z53.object({
|
|
|
6719
6741
|
size: z53.enum(["cover", "fixed"]).optional(),
|
|
6720
6742
|
color: z53.enum(["primary", "secondary", "tertiary", "success", "warning", "danger"]).optional(),
|
|
6721
6743
|
hint: DynamicTextSchema.optional(),
|
|
6744
|
+
locale: z53.string().refine(isLocaleTag, { message: 'Expected a BCP 47 locale tag such as "de-DE"' }).optional(),
|
|
6722
6745
|
placeholder: DynamicTextSchema.optional()
|
|
6723
6746
|
});
|
|
6724
6747
|
var IonicSelectPropsSchema = z53.object({
|
|
@@ -9565,14 +9588,7 @@ derivation: 'formValue.discount ?? 0' // Nullish coalescing
|
|
|
9565
9588
|
derivation: 'formValue.price * (1 - externalData.discountRate)' // External data
|
|
9566
9589
|
\`\`\`
|
|
9567
9590
|
|
|
9568
|
-
**
|
|
9569
|
-
\`\`\`typescript
|
|
9570
|
-
derivation: \`(() => {
|
|
9571
|
-
const qty = formValue.quantity || 0;
|
|
9572
|
-
const price = formValue.unitPrice || 0;
|
|
9573
|
-
return qty * price;
|
|
9574
|
-
})()\`
|
|
9575
|
-
\`\`\`
|
|
9591
|
+
**Expressions only:** no statements (\`const\`, \`if\`, \`return\`), no \`new\`, and no globals (\`Math\`, \`String()\`, \`Array.isArray\`). Use a ternary for branching and a custom derivation function for anything longer.
|
|
9576
9592
|
|
|
9577
9593
|
**See also:** \`property-derivation\` topic for deriving component properties (minDate, options, label, etc.)`
|
|
9578
9594
|
},
|
|
@@ -10592,10 +10608,14 @@ const config = { fields: [...] } as const satisfies FormConfig;
|
|
|
10592
10608
|
| \`endsWith\` | String ends with | \`'hello'.endsWith('lo')\` |
|
|
10593
10609
|
| \`matches\` | Regex match | \`/^[0-9]+$/.test(value)\` |
|
|
10594
10610
|
|
|
10611
|
+
An empty field value (\`null\`, \`undefined\` or \`''\`) never satisfies \`greater\`, \`less\`, \`greaterOrEqual\`, \`lessOrEqual\`, \`contains\`, \`startsWith\` or \`endsWith\`. \`matches\` tests it as \`''\`, so \`'^$'\` matches an empty field. \`0\` and \`false\` are not empty.
|
|
10612
|
+
|
|
10595
10613
|
For \`matches\`, the value is a pattern string. Use the \`/pattern/flags\` form to pass regex flags, e.g. \`"/john/i"\` for case-insensitive matching. A plain pattern (no \`/.../\` delimiters) is matched case-sensitively with no flags.
|
|
10596
10614
|
|
|
10597
10615
|
## Combining Conditions
|
|
10598
10616
|
|
|
10617
|
+
An \`and\` or \`or\` group with no conditions never fires.
|
|
10618
|
+
|
|
10599
10619
|
**AND (all must be true):**
|
|
10600
10620
|
\`\`\`typescript
|
|
10601
10621
|
condition: {
|
|
@@ -10774,7 +10794,7 @@ expression: '(formValue.contacts?.length || 0) + " contacts"'
|
|
|
10774
10794
|
expression: '(formValue.firstName || "") + " " + (formValue.lastName || "")'
|
|
10775
10795
|
|
|
10776
10796
|
// Address formatting
|
|
10777
|
-
expression: \`[formValue.street, formValue.city, formValue.state, formValue.zip].filter(
|
|
10797
|
+
expression: \`[formValue.street, formValue.city, formValue.state, formValue.zip].filter(x => x).join(", ")\`
|
|
10778
10798
|
\`\`\`
|
|
10779
10799
|
|
|
10780
10800
|
## Math Calculations
|
|
@@ -10783,7 +10803,7 @@ expression: \`[formValue.street, formValue.city, formValue.state, formValue.zip]
|
|
|
10783
10803
|
expression: '(formValue.quantity || 0) * (formValue.unitPrice || 0)'
|
|
10784
10804
|
|
|
10785
10805
|
// Percentage
|
|
10786
|
-
expression: '
|
|
10806
|
+
expression: '((formValue.completed / formValue.total) * 100).toFixed(0) + "%"'
|
|
10787
10807
|
|
|
10788
10808
|
// Sum of array
|
|
10789
10809
|
expression: '(formValue.items || []).reduce((sum, item) => sum + (item.price || 0), 0)'
|
|
@@ -10794,13 +10814,8 @@ expression: '(formValue.items || []).reduce((sum, item) => sum + (item.price ||
|
|
|
10794
10814
|
// Ternary
|
|
10795
10815
|
expression: 'formValue.isPremium ? "Premium Member" : "Standard Member"'
|
|
10796
10816
|
|
|
10797
|
-
// Multiple conditions
|
|
10798
|
-
expression:
|
|
10799
|
-
if (formValue.score >= 90) return 'A';
|
|
10800
|
-
if (formValue.score >= 80) return 'B';
|
|
10801
|
-
if (formValue.score >= 70) return 'C';
|
|
10802
|
-
return 'D';
|
|
10803
|
-
})()\`
|
|
10817
|
+
// Multiple conditions (nested ternary)
|
|
10818
|
+
expression: 'formValue.score >= 90 ? "A" : formValue.score >= 80 ? "B" : formValue.score >= 70 ? "C" : "D"'
|
|
10804
10819
|
\`\`\``
|
|
10805
10820
|
},
|
|
10806
10821
|
"type-narrowing": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-forge/dynamic-form-mcp",
|
|
3
|
-
"version": "1.3.0-next.
|
|
3
|
+
"version": "1.3.0-next.4",
|
|
4
4
|
"description": "MCP server for ng-forge dynamic forms - AI-assisted form schema generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"zod": "^4.5.4"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@ng-forge/dynamic-forms": "^1.3.0-next.
|
|
57
|
-
"@ng-forge/dynamic-forms-bootstrap": "^1.3.0-next.
|
|
58
|
-
"@ng-forge/dynamic-forms-ionic": "^1.3.0-next.
|
|
59
|
-
"@ng-forge/dynamic-forms-material": "^1.3.0-next.
|
|
60
|
-
"@ng-forge/dynamic-forms-primeng": "^1.3.0-next.
|
|
56
|
+
"@ng-forge/dynamic-forms": "^1.3.0-next.4",
|
|
57
|
+
"@ng-forge/dynamic-forms-bootstrap": "^1.3.0-next.4",
|
|
58
|
+
"@ng-forge/dynamic-forms-ionic": "^1.3.0-next.4",
|
|
59
|
+
"@ng-forge/dynamic-forms-material": "^1.3.0-next.4",
|
|
60
|
+
"@ng-forge/dynamic-forms-primeng": "^1.3.0-next.4",
|
|
61
61
|
"tsx": "^4.19.0"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/index.js
CHANGED
|
@@ -1576,6 +1576,19 @@ var UI_ADAPTERS = [
|
|
|
1576
1576
|
default: false
|
|
1577
1577
|
}
|
|
1578
1578
|
}
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
type: "datepicker",
|
|
1582
|
+
componentName: "IonicDatepickerFieldComponent",
|
|
1583
|
+
additionalProps: {
|
|
1584
|
+
locale: {
|
|
1585
|
+
name: "locale",
|
|
1586
|
+
type: "string",
|
|
1587
|
+
description: "BCP 47 locale for the picker and the displayed value",
|
|
1588
|
+
required: false,
|
|
1589
|
+
default: "browser locale"
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1579
1592
|
}
|
|
1580
1593
|
]
|
|
1581
1594
|
}
|
|
@@ -3103,6 +3116,7 @@ Use \`options.fieldWindowing\` for large flat forms:
|
|
|
3103
3116
|
- \`park.margin\` follows \`IntersectionObserver.rootMargin\`: use one to four \`px\` or \`%\` values. Unsupported units fall back to the inherited margin.
|
|
3104
3117
|
- \`fieldWindowing: false\` disables inherited deferred mounting for that form.
|
|
3105
3118
|
- A park-only object changes parking without changing the inherited mounting mode. Use \`{ park: false }\` to opt out or \`{ park: true }\` to opt in.
|
|
3119
|
+
- Deferred fields show an empty box. To show a skeleton, project \`<ng-template dfPlaceholder let-field>\` into the form template (import \`DfPlaceholder\`). \`dfPlaceholderKey="key"\` beats \`dfPlaceholder="type"\`, which beats an empty \`dfPlaceholder\`. This is template-only, not part of the config.
|
|
3106
3120
|
|
|
3107
3121
|
## Validation
|
|
3108
3122
|
|
|
@@ -3236,6 +3250,7 @@ Compares a specific field's value:
|
|
|
3236
3250
|
- \`equals\`, \`notEquals\`
|
|
3237
3251
|
- \`greater\`, \`less\`, \`greaterOrEqual\`, \`lessOrEqual\`
|
|
3238
3252
|
- \`contains\`, \`startsWith\`, \`endsWith\`, \`matches\`
|
|
3253
|
+
- An empty value (null, undefined or '') never satisfies the ordered or string operators. \`matches\` tests it as '', and 0 and false are not empty.
|
|
3239
3254
|
|
|
3240
3255
|
#### JavaScript Condition
|
|
3241
3256
|
|
|
@@ -6703,6 +6718,13 @@ var PrimeFormConfigSchema = createFormConfigSchema(PrimeFieldSchema, PrimeFormDe
|
|
|
6703
6718
|
|
|
6704
6719
|
// internal/dynamic-forms-validation/ionic/src/props/index.ts
|
|
6705
6720
|
import { z as z53 } from "zod";
|
|
6721
|
+
function isLocaleTag(value) {
|
|
6722
|
+
try {
|
|
6723
|
+
return Intl.getCanonicalLocales(value).length > 0;
|
|
6724
|
+
} catch {
|
|
6725
|
+
return false;
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6706
6728
|
var IonicColorSchema = z53.enum(["primary", "secondary", "tertiary", "success", "warning", "danger", "light", "medium", "dark"]);
|
|
6707
6729
|
var IonicLabelPlacementSchema = z53.enum(["start", "end", "fixed", "stacked", "floating"]);
|
|
6708
6730
|
var IonicJustifySchema = z53.enum(["start", "end", "space-between"]);
|
|
@@ -6764,6 +6786,7 @@ var IonicDatepickerPropsSchema = z53.object({
|
|
|
6764
6786
|
size: z53.enum(["cover", "fixed"]).optional(),
|
|
6765
6787
|
color: z53.enum(["primary", "secondary", "tertiary", "success", "warning", "danger"]).optional(),
|
|
6766
6788
|
hint: DynamicTextSchema.optional(),
|
|
6789
|
+
locale: z53.string().refine(isLocaleTag, { message: 'Expected a BCP 47 locale tag such as "de-DE"' }).optional(),
|
|
6767
6790
|
placeholder: DynamicTextSchema.optional()
|
|
6768
6791
|
});
|
|
6769
6792
|
var IonicSelectPropsSchema = z53.object({
|
|
@@ -9639,14 +9662,7 @@ derivation: 'formValue.discount ?? 0' // Nullish coalescing
|
|
|
9639
9662
|
derivation: 'formValue.price * (1 - externalData.discountRate)' // External data
|
|
9640
9663
|
\`\`\`
|
|
9641
9664
|
|
|
9642
|
-
**
|
|
9643
|
-
\`\`\`typescript
|
|
9644
|
-
derivation: \`(() => {
|
|
9645
|
-
const qty = formValue.quantity || 0;
|
|
9646
|
-
const price = formValue.unitPrice || 0;
|
|
9647
|
-
return qty * price;
|
|
9648
|
-
})()\`
|
|
9649
|
-
\`\`\`
|
|
9665
|
+
**Expressions only:** no statements (\`const\`, \`if\`, \`return\`), no \`new\`, and no globals (\`Math\`, \`String()\`, \`Array.isArray\`). Use a ternary for branching and a custom derivation function for anything longer.
|
|
9650
9666
|
|
|
9651
9667
|
**See also:** \`property-derivation\` topic for deriving component properties (minDate, options, label, etc.)`
|
|
9652
9668
|
},
|
|
@@ -10666,10 +10682,14 @@ const config = { fields: [...] } as const satisfies FormConfig;
|
|
|
10666
10682
|
| \`endsWith\` | String ends with | \`'hello'.endsWith('lo')\` |
|
|
10667
10683
|
| \`matches\` | Regex match | \`/^[0-9]+$/.test(value)\` |
|
|
10668
10684
|
|
|
10685
|
+
An empty field value (\`null\`, \`undefined\` or \`''\`) never satisfies \`greater\`, \`less\`, \`greaterOrEqual\`, \`lessOrEqual\`, \`contains\`, \`startsWith\` or \`endsWith\`. \`matches\` tests it as \`''\`, so \`'^$'\` matches an empty field. \`0\` and \`false\` are not empty.
|
|
10686
|
+
|
|
10669
10687
|
For \`matches\`, the value is a pattern string. Use the \`/pattern/flags\` form to pass regex flags, e.g. \`"/john/i"\` for case-insensitive matching. A plain pattern (no \`/.../\` delimiters) is matched case-sensitively with no flags.
|
|
10670
10688
|
|
|
10671
10689
|
## Combining Conditions
|
|
10672
10690
|
|
|
10691
|
+
An \`and\` or \`or\` group with no conditions never fires.
|
|
10692
|
+
|
|
10673
10693
|
**AND (all must be true):**
|
|
10674
10694
|
\`\`\`typescript
|
|
10675
10695
|
condition: {
|
|
@@ -10848,7 +10868,7 @@ expression: '(formValue.contacts?.length || 0) + " contacts"'
|
|
|
10848
10868
|
expression: '(formValue.firstName || "") + " " + (formValue.lastName || "")'
|
|
10849
10869
|
|
|
10850
10870
|
// Address formatting
|
|
10851
|
-
expression: \`[formValue.street, formValue.city, formValue.state, formValue.zip].filter(
|
|
10871
|
+
expression: \`[formValue.street, formValue.city, formValue.state, formValue.zip].filter(x => x).join(", ")\`
|
|
10852
10872
|
\`\`\`
|
|
10853
10873
|
|
|
10854
10874
|
## Math Calculations
|
|
@@ -10857,7 +10877,7 @@ expression: \`[formValue.street, formValue.city, formValue.state, formValue.zip]
|
|
|
10857
10877
|
expression: '(formValue.quantity || 0) * (formValue.unitPrice || 0)'
|
|
10858
10878
|
|
|
10859
10879
|
// Percentage
|
|
10860
|
-
expression: '
|
|
10880
|
+
expression: '((formValue.completed / formValue.total) * 100).toFixed(0) + "%"'
|
|
10861
10881
|
|
|
10862
10882
|
// Sum of array
|
|
10863
10883
|
expression: '(formValue.items || []).reduce((sum, item) => sum + (item.price || 0), 0)'
|
|
@@ -10868,13 +10888,8 @@ expression: '(formValue.items || []).reduce((sum, item) => sum + (item.price ||
|
|
|
10868
10888
|
// Ternary
|
|
10869
10889
|
expression: 'formValue.isPremium ? "Premium Member" : "Standard Member"'
|
|
10870
10890
|
|
|
10871
|
-
// Multiple conditions
|
|
10872
|
-
expression:
|
|
10873
|
-
if (formValue.score >= 90) return 'A';
|
|
10874
|
-
if (formValue.score >= 80) return 'B';
|
|
10875
|
-
if (formValue.score >= 70) return 'C';
|
|
10876
|
-
return 'D';
|
|
10877
|
-
})()\`
|
|
10891
|
+
// Multiple conditions (nested ternary)
|
|
10892
|
+
expression: 'formValue.score >= 90 ? "A" : formValue.score >= 80 ? "B" : formValue.score >= 70 ? "C" : "D"'
|
|
10878
10893
|
\`\`\``
|
|
10879
10894
|
},
|
|
10880
10895
|
"type-narrowing": {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** ng-forge Best Practices */
|
|
2
|
-
export declare const INSTRUCTIONS = "# ng-forge Dynamic Forms - Best Practices\n\nYou MUST follow these practices when generating FormConfig objects for ng-forge.\n\n## Core Principles\n\n1. **Configuration-driven**: ng-forge is built on Angular Signal Forms. Define structure declaratively via `FormConfig`, not imperatively.\n2. **Type-safe**: Use TypeScript interfaces for form values. The library provides full type inference.\n3. **Validation-first**: Always include validation and user-friendly error messages.\n\n## FormConfig Structure\n\n```typescript\nconst config: FormConfig = {\n // Optional: Default validation messages for all fields\n defaultValidationMessages: {\n required: 'This field is required',\n email: 'Please enter a valid email',\n minLength: 'Must be at least {{requiredLength}} characters',\n },\n // Required: Array of field definitions\n fields: [\n // ... field definitions\n ],\n};\n```\n\n## Field Definition Rules\n\n### Required Properties\n\nEvery field MUST have:\n\n- `key`: Identifier, unique within its group scope (the same leaf key may repeat inside different groups, e.g. `createA.name` and `createB.name`). Page/row/array containers do not introduce a scope. Buttons (which don't bind to form values) don't need unique keys.\n - **DOM id / data-testid composition:** keys are joined with `_` through group ancestors to form the rendered DOM id. A leaf `street` inside group `address` renders as `id=\"address_street\"`. Inside an array item, the item index is appended after: `id=\"address_street_0\"`. Use this exact form for `for=`, `aria-describedby`, `aria-labelledby`, or CSS selectors that target group-nested fields.\n - **Form-level prefix (multiple forms on one page):** set `options.idPrefix` to scope every id to a form instance \u2014 it becomes the OUTERMOST segment: `{idPrefix}_{group}_{key}_{index}` (e.g. `billing_address_street_0`). When omitted, a lone form stays unprefixed, but if two or more forms are mounted at once the library auto-prefixes each (`df-1`, `df-2`, \u2026) so their ids don't collide. Account for an explicit prefix when writing `for=`/`aria-*`/selectors.\n - **Avoid `_` in keys when they could collide with a group-prefixed id.** A top-level key `foo_bar` and a leaf `bar` inside group `foo` would both render as `id=\"foo_bar\"`. The validator catches this collision at boot, but it's easier to just not put `_` in keys that share a name with an existing group + leaf pair.\n- `type`: One of the registered field types\n- `label`: Human-readable label (for accessibility)\n\n### Value Fields\n\nFor fields that collect user input (`input`, `textarea`, `select`, `checkbox`, `radio`, `datepicker`, `toggle`, `slider`):\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n required: true, // Shorthand validator\n email: true, // Shorthand validator\n placeholder: 'user@example.com', // Field-level, not inside props\n validationMessages: { // Always provide clear messages\n required: 'Email is required',\n email: 'Please enter a valid email'\n },\n props: {\n type: 'email', // HTML input type\n hint: 'We will never share your email' // UI library specific\n }\n}\n```\n\n### Select/Radio Fields\n\nMUST include `options` array:\n\n```typescript\n{\n key: 'country',\n type: 'select',\n label: 'Country',\n required: true,\n placeholder: 'Select a country',\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' }\n ]\n}\n```\n\n### Container Fields\n\n#### Row (Horizontal Layout)\n\nUse for side-by-side fields. Row fields do NOT create nested values - children are flat.\n\n```typescript\n{\n key: 'nameRow',\n type: 'row',\n fields: [\n { key: 'firstName', type: 'input', label: 'First', col: 6 },\n { key: 'lastName', type: 'input', label: 'Last', col: 6 }\n ]\n}\n// Result: { firstName: '...', lastName: '...' }\n```\n\nUse `col` property (1-12) to control column widths.\n\n#### Group (Nested Object)\n\nUse when you need nested object structure in form values:\n\n```typescript\n{\n key: 'address',\n type: 'group',\n fields: [\n { key: 'street', type: 'input', label: 'Street' },\n { key: 'city', type: 'input', label: 'City' }\n ]\n}\n// Result: { address: { street: '...', city: '...' } }\n```\n\n#### Array (Repeatable Items)\n\nUse for dynamic lists. Each item is wrapped in a `<div class=\"df-array-item\">` with `role=\"group\"` and `aria-label=\"Item N\"` for accessibility. Use `--df-array-item-gap` CSS variable to control spacing between items.\n\n```typescript\n{\n key: 'contacts',\n type: 'array',\n // \u26A0\uFE0F NO label property on arrays!\n fields: [\n {\n key: 'contact',\n type: 'group',\n fields: [\n { key: 'name', type: 'input', label: 'Name', required: true },\n { key: 'phone', type: 'input', label: 'Phone', required: true }\n ]\n }\n ]\n}\n// Result: { contacts: [{ name: '...', phone: '...' }, ...] }\n```\n\n#### Page (Multi-step Forms)\n\nUse for wizard-style forms:\n\n```typescript\n{\n fields: [\n {\n key: 'step1',\n type: 'page',\n fields: [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'next', type: 'next', label: 'Continue' },\n ],\n },\n {\n key: 'step2',\n type: 'page',\n fields: [\n { key: 'prev', type: 'previous', label: 'Back' },\n { key: 'submit', type: 'submit', label: 'Submit' },\n ],\n },\n ];\n}\n```\n\n##### Starting on a specific page (deep links, session resume)\n\nSet `options.initialPage` to open a paged form somewhere other than page 0:\n\n```typescript\n{\n options: { initialPage: 2 }, // lands on page 2 regardless of earlier pages\n fields: [ /* pages */ ]\n}\n```\n\nThe shorthand lands unconditionally, which is what restoring a saved session wants.\nUse `{ index: 2, validate: true }` to apply the same validity gate a forward\n`GoToPageEvent` uses, which stops on the first invalid page instead.\n\nOut-of-range indices clamp to the last page; negative or non-numeric values fall back\nto `0`; a hidden target resolves to the nearest visible page.\n\nTo move pages at runtime, dispatch `GoToPageEvent`. `validate: false` bypasses the\nvalidity gate only \u2014 the target must still be in range and visible:\n\n```typescript\ndispatcher.dispatch(new GoToPageEvent(3)); // gated, stops on first invalid page\ndispatcher.dispatch(new GoToPageEvent(3, { validate: false })); // skips the gate, for resume\n```\n\nPaged forms may virtualize inactive pages. DynamicForm `(initialized)` emits after\nthe initially active page is ready. Use `(activePageInitialized)` when work must run\nafter every visible-page transition; it announces once per visit.\n\n### Rendering Performance\n\nUse `options.pagePreloadWindow` for paged forms. It controls how many neighbouring\npages are mounted and have their component chunks preloaded. `0` keeps only the\nactive page in the render window; the default is `1`.\n\nUse `options.fieldWindowing` for large flat forms:\n\n```typescript\n{\n options: {\n fieldWindowing: {\n eager: 20,\n placeholderHeight: '4rem',\n park: { margin: '100%' },\n },\n },\n fields: [ /* flat fields */ ],\n}\n```\n\n- `eager` mounts that many leading leaf fields immediately. Remaining fields mount near the viewport.\n- `placeholderHeight` reserves layout space before a field mounts.\n- `park` keeps mounted offscreen DOM in place but removes its view from routine change detection. Safety-related state such as disabled, readonly, required, and validation state stays current. Other model-to-DOM updates catch up when the field returns.\n- `park.margin` follows `IntersectionObserver.rootMargin`: use one to four `px` or `%` values. Unsupported units fall back to the inherited margin.\n- `fieldWindowing: false` disables inherited deferred mounting for that form.\n- A park-only object changes parking without changing the inherited mounting mode. Use `{ park: false }` to opt out or `{ park: true }` to opt in.\n\n## Validation\n\n### Shorthand Validators (Preferred)\n\nUse shorthand properties directly on field definitions:\n\n```typescript\n{\n key: 'age',\n type: 'input',\n label: 'Age',\n required: true,\n min: 18,\n max: 120,\n validationMessages: {\n required: 'Age is required',\n min: 'Must be at least {{min}}',\n max: 'Cannot exceed {{max}}'\n }\n}\n```\n\nAvailable shorthands: `required`, `email`, `min`, `max`, `minLength`, `maxLength`, `pattern`\n\n### Nullable Values\n\nValue fields and checked fields (`checkbox`, `toggle`) accept a `nullable?: boolean` flag. When `true`:\n\n- `value` accepts `null` in addition to the field's normal value type.\n- An omitted `value` resolves to `null` (instead of the type-specific empty default like `''`, `NaN`, `false`, or `[]`).\n- Orthogonal to `required`. `nullable` declares that the **model** accepts `null`; `required` is a **validation** constraint. Angular's `Validators.required` treats `null` as invalid, so a field that is both `nullable` and `required` will fail required-validation when the value is `null` \u2014 the two flags describe different layers (data shape vs. validation), not conflicting semantics.\n- Map OpenAPI `nullable: true` (3.0) or `type: [T, null]` (3.1) to this flag.\n- Container fields (`group`, `array`, `row`, `page`) do NOT support `nullable` \u2014 their null semantics are ambiguous.\n\n```typescript\n{\n key: 'middleName',\n type: 'input',\n label: 'Middle Name',\n nullable: true,\n value: null, // valid; default-resolves to null\n}\n\n{\n key: 'active',\n type: 'checkbox',\n label: 'Active',\n nullable: true,\n value: null, // undecided checkbox state \u2014 model permits null, UI renders unchecked\n}\n```\n\nRead-side caveat: a user clearing a text input reads back as `\"\"`, not `null` \u2014 this is a DOM/Web IDL contract, identical to classic Reactive Forms. `nullable` is a contract for accepted values, not a guarantee of emitted ones.\n\n### Validation Messages\n\nALWAYS provide validation messages. Use template variables:\n\n- `{{requiredLength}}` - for minLength/maxLength\n- `{{min}}`, `{{max}}` - for min/max\n- `{{requiredPattern}}` - for pattern\n\nTypeScript-authored configs may also use error-aware message functions: any `validationMessages` or `defaultValidationMessages` value can be `(error: ValidationError) => DynamicText` instead of a string/Observable/Signal. The function receives the validation error (kind plus params like `maxLength`), so i18n layers can receive interpolation params natively:\n\n```typescript\nvalidationMessages: {\n maxLength: (error) => transloco.selectTranslate('validation.maxLength', {\n requiredLength: 'maxLength' in error ? error.maxLength : undefined,\n }),\n}\n```\n\nFunction messages are NOT JSON-serializable. JSON-driven configs MUST keep using string templates with `{{param}}` interpolation; MCP-generated configs should always emit string templates.\n\n## Conditional Logic\n\nUse `logic` array for conditional behavior:\n\n```typescript\n{\n key: 'businessName',\n type: 'input',\n label: 'Business Name',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business'\n }\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business'\n }\n }\n ]\n}\n```\n\n### Logic Types\n\n- `hidden` - Show/hide field\n- `disabled` - Enable/disable field\n- `required` - Conditional requirement\n\n### Condition Types\n\n#### Field Value Condition\n\nCompares a specific field's value:\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business'\n}\n```\n\n#### Comparison Operators\n\n- `equals`, `notEquals`\n- `greater`, `less`, `greaterOrEqual`, `lessOrEqual`\n- `contains`, `startsWith`, `endsWith`, `matches`\n\n#### JavaScript Condition\n\nEvaluates a JavaScript expression with access to `formValue`, `fieldValue`, `externalData`:\n\n```typescript\n{\n type: 'javascript',\n expression: 'formValue.items.length > 0'\n}\n```\n\n**Condition scoping:** in logic conditions, `formValue` is the whole form value regardless of where the field sits, except inside array items where it is scoped to the current item (use `rootFormValue` for the rest of the form). Reference group-nested fields by their full path, e.g. `formValue.person.firstName`. Note that `derivation` expressions scope differently: there `formValue` is also scoped to the parent group for group-nested fields.\n\n#### Custom Condition\n\nInvokes a registered custom function by name:\n\n```typescript\n{\n type: 'custom',\n functionName: 'myCustomCheck'\n}\n```\n\nRegister functions via `customFnConfig.customFunctions`.\n\n> **TypeScript-authored configs:** the same surface also accepts an inline `fn: CustomFunction` instead of `functionName` (XOR with `functionName`, code-only \u2014 not JSON-serializable). The MCP server emits `functionName` exclusively; consumers writing configs in TS may prefer `fn` when they don't need JSON round-tripping. Same caveat applies to async conditions (`asyncFn`), function derivations (`fn`), async function derivations (`asyncFn`), and validators (`fn`).\n\n#### Async Custom Condition\n\nInvokes a registered async function that returns `Promise<boolean>` or `Observable<boolean>`:\n\n```typescript\n{\n type: 'async',\n asyncFunctionName: 'checkPermission',\n pendingValue: false, // value while async resolution is pending (default: false)\n debounceMs: 300 // debounce for re-evaluation (default: 300)\n}\n```\n\nRegister functions via `customFnConfig.asyncConditions`. The function receives an `EvaluationContext` with access to `formValue`, `fieldValue`, and `externalData`.\n\n#### HTTP Condition\n\nEvaluates based on an HTTP response from a remote server. Fully declarative and JSON-serializable:\n\n```typescript\n{\n type: 'http',\n http: {\n url: '/api/permissions',\n method: 'GET',\n queryParams: {\n userId: 'formValue.userId',\n role: 'formValue.role'\n }\n },\n responseExpression: 'response.canEdit', // extract boolean from response (default: !!response)\n pendingValue: false, // value while HTTP request is in-flight (default: false)\n cacheDurationMs: 30000, // cache duration in ms (default: 30000)\n debounceMs: 300 // debounce for re-evaluation (default: 300)\n}\n```\n\nUse HTTP conditions when visibility, disabled state, or required state depends on server-side data (e.g., permissions, feature flags, inventory checks). The request re-evaluates reactively when dependent form values change.\n\n#### Logical Combinators\n\nCombine conditions with `and` (all must be true) or `or` (at least one must be true):\n\n```typescript\n{\n type: 'and',\n conditions: [\n { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'US' },\n { type: 'fieldValue', fieldPath: 'age', operator: 'greaterOrEqual', value: 18 }\n ]\n}\n```\n\n## UI Library Integration\n\nng-forge supports multiple UI libraries. Configure in `app.config.ts`:\n\n```typescript\nimport { provideDynamicForm } from '@ng-forge/dynamic-forms';\nimport { withMaterialFields } from '@ng-forge/dynamic-forms-material';\n\nexport const appConfig = {\n providers: [provideDynamicForm(...withMaterialFields())],\n};\n```\n\nAvailable adapters:\n\n- `@ng-forge/dynamic-forms-material` - Angular Material\n- `@ng-forge/dynamic-forms-bootstrap` - Bootstrap 5\n- `@ng-forge/dynamic-forms-primeng` - PrimeNG\n- `@ng-forge/dynamic-forms-ionic` - Ionic\n\n### Style Defaults (opt-in)\n\nEach adapter ships an opinionated Sass partial with host-framework colors/typography. Library SCSS uses `var(--df-name, structural-fallback)` so forms render usefully without any opt-in \u2014 only opt in if you want the adapter-themed look.\n\n```scss\n@use '@ng-forge/dynamic-forms-material/styles/defaults' as df;\n\n:root {\n @include df.apply-material-form-defaults;\n}\n```\n\nSubstitute `material` for `bootstrap`, `primeng`, or `ionic`. Material also ships `form-material-defaults-dark` for dark-mode hint/error colors. Ionic ships `apply-ionic-legacy-error-styles` that pairs with `withLegacyStatusClasses()` (see below).\n\n## Provider Features\n\n`provideDynamicForm()` accepts optional `with*` feature configurators alongside the adapter field bundle:\n\n### `withLegacyStatusClasses()` \u2014 opt into legacy ng-* CSS classes\n\nBy default, Angular Signal Forms uses its modern class strategy. If your CSS targets `.ng-touched`, `.ng-invalid`, `.ng-dirty`, etc. on form-bound elements, add this feature to wire up the compat-class strategy:\n\n```typescript\nimport { provideDynamicForm, withLegacyStatusClasses } from '@ng-forge/dynamic-forms';\nimport { withMaterialFields } from '@ng-forge/dynamic-forms-material';\n\nexport const appConfig = {\n providers: [\n provideDynamicForm(\n ...withMaterialFields(),\n withLegacyStatusClasses(),\n ),\n ],\n};\n```\n\nRecommended when consuming Ionic's `apply-ionic-legacy-error-styles` mixin, when using Bootstrap themes that style `is-invalid.ng-touched`, or any custom CSS that depends on the `ng-*` classes.\n\n### Other built-in features\n\n- `withLoggerConfig(options?)` \u2014 configure or disable the dynamic-form logger\n- `withEventFormValue()` \u2014 attach the current form value to every dispatched event\n- `withValueExclusionDefaults(options)` \u2014 set form-wide value exclusion defaults\n- `withValidationExecutionDefaults(options)` \u2014 set form-wide validation execution defaults (e.g. defer until touched)\n- `withExperimentalWebMcp()` \u2014 EXPERIMENTAL. Expose forms to browser AI agents as WebMCP tools. Opt a form in with `options.webMcp: { name, description }`; it registers `fill_{name}`, which validates and applies a partial patch of values to the form and returns what it set, which fields apply, which are still empty, and validation errors. Groups are merged key by key; lists are replaced whole, so an agent cannot rewrite a list whose items already hold values it is not allowed to set (a hidden id, a `readonly` price) \u2014 that write is refused rather than silently dropping them. Submission is a separate per-form opt-in: add `allowSubmit: true` to also register `submit_{name}`, which waits for the submission and reports its real outcome. Leave `allowSubmit` off for anything that spends money, sends a message, or cannot be undone. `webMcp.name` must be 1-121 characters of A-Z, a-z, 0-9, `_`, `-` or `.`, and short enough to read in a tool list. By default a response returns only the values the call set; `readback: 'all'` returns the whole readable model and hands the agent everything the form holds, so add it only when asked. Per field, `webMcp: { readable, writable }` (or `webMcp: false`) overrides the defaults: a `hidden` field type is neither readable nor writable, a password input is not readable, a `readonly` or derived field is not writable. Do NOT add `options.webMcp` unless the user explicitly asks for agent/WebMCP support.\n\nAll features are exported from `@ng-forge/dynamic-forms`.\n\n## Common Patterns\n\n### Always End with Submit Button\n\n```typescript\n{ type: 'submit', key: 'submit', label: 'Submit', props: { color: 'primary' } }\n```\n\n### Use Text Fields for Headings\n\n```typescript\n{ key: 'sectionTitle', type: 'text', label: 'Personal Info', props: { elementType: 'h3' } }\n```\n\n### Input Types via Props\n\n```typescript\n// Email input\n{ key: 'email', type: 'input', props: { type: 'email' } }\n\n// Password input\n{ key: 'password', type: 'input', props: { type: 'password' } }\n\n// Number input\n{ key: 'age', type: 'input', props: { type: 'number' } }\n\n// Phone input\n{ key: 'phone', type: 'input', props: { type: 'tel' } }\n```\n\n## Anti-patterns to Avoid\n\n1. **Missing labels** - Always include labels for accessibility\n2. **Missing validation messages** - Users need clear feedback\n3. **Duplicate keys within a group scope** - Keys must be unique within the same group. Same key in different groups is fine (different scoped paths). Buttons are exempt.\n4. **Select without options** - Select/radio fields must have options\n5. **Array without fields** - Array fields need a fields template\n6. **Group without fields** - Group fields need child fields\n7. **min > max** - Ensure min values are less than max values\n\n## CRITICAL: UI Library Differences\n\nDifferent UI libraries support different `props`. Always validate against the library the project actually uses.\n\nWhich props exist is answered per adapter, not here: use the adapter's own reference\n(`ngforge_lookup` with the adapter, or the `ng-forge-dynamic-forms-<adapter>` skill). A table\nrepeated here would be a second copy of the adapter registries and would drift from them.\n\nTwo adapters cannot both provide the same field type, so a project has one adapter per\nfield type; TypeScript reports error 2717 when two try.\n\n### Container Fields\n\nNo container has a `label`. Use a `text` field for a heading instead.\n\nValidation splits by whether the container owns a value:\n\n- `group` and `array` DO accept `required`, `validators`, `validationMessages`\n and `validateWhenHidden`. A validator there runs against the container's own\n subtree, so a cross-field rule (dateTo after dateFrom) belongs on the group\n rather than on one of its children. `ctx.value()` is the group's object or the\n array's item list.\n- `page`, `row` and `container` do NOT. They flatten into their parent and have\n no schema path, so a validator on them would have nothing to run against.\n\nLeaf-level validators such as `email`, `min` and `max` belong on the field that\nholds the value, on any container.\n\n### Common Mistakes to Avoid\n\n1. **Using `expressions` on fields** - This property is NOT supported on standard fields. Use `derivation` for computed values or `logic` for conditional behavior.\n\n2. **Adding `label` to container fields** - Pages, groups, and rows don't have labels:\n ```typescript\n // \u274C WRONG\n { key: 'address', type: 'group', label: 'Address', fields: [...] }\n\n // \u2705 CORRECT - Use a text field for the heading\n { key: 'addressTitle', type: 'text', label: 'Address', props: { elementType: 'h3' } }\n { key: 'address', type: 'group', fields: [...] }\n ```\n\n3. **Adding `id` to FormConfig** - The root FormConfig does not accept an `id` property.\n\n4. **Using `min`/`max` for a slider's range** - A slider's range is\n `minValue`, `maxValue` and `step`, at field level. `min` and `max` are\n validation shorthands: they typecheck, they validate clean, and the slider\n still renders its default 0-100 range, so nothing catches the mistake.\n ```typescript\n // \u274C WRONG - validates clean, renders the wrong range\n { key: 'volume', type: 'slider', label: 'Volume', props: { min: 0, max: 100 } }\n { key: 'volume', type: 'slider', label: 'Volume', min: 0, max: 100 }\n\n // \u2705 CORRECT\n { key: 'volume', type: 'slider', label: 'Volume', minValue: 0, maxValue: 100, step: 5 }\n ```\n\n5. **Using unsupported field types** - Each UI library supports specific field types. Use `ngforge_lookup` with `depth: \"schema\"` to see available types.\n\n## Validation Before Use\n\nNever hand back a config you have not checked. Two routes, depending on what is\navailable:\n\n- With the MCP server: `ngforge_validate({ uiIntegration: 'material', config })`\n- Without it: `npx --yes @ng-forge/dynamic-forms-cli@latest \"path/to/your.form.ts\" --ui material`\n\nBoth run the same schemas, so they agree.\n\n### What a clean run does and does not mean\n\nA clean run means the structure is right: the field types exist, required\nproperties are present, options are in the right place, and containers are not\ncarrying properties they cannot have.\n\nIt does NOT mean every property you wrote is real. The schemas strip unknown keys\nrather than rejecting them, so an invented or misplaced property passes\nvalidation and is silently dropped at runtime. Measured examples that pass today:\n`hideWhen` on a field and `targetProperty` inside a derivation.\n\n`as const satisfies FormConfig` is what catches those, because TypeScript checks\nthe property names. Use both: the type annotation for what exists, the validator\nfor what is structurally correct.\n\nTo see which properties a field type supports, use `ngforge_lookup` with\n`depth: \"schema\"`, or read the field-types reference in the skill.\n";
|
|
2
|
+
export declare const INSTRUCTIONS = "# ng-forge Dynamic Forms - Best Practices\n\nYou MUST follow these practices when generating FormConfig objects for ng-forge.\n\n## Core Principles\n\n1. **Configuration-driven**: ng-forge is built on Angular Signal Forms. Define structure declaratively via `FormConfig`, not imperatively.\n2. **Type-safe**: Use TypeScript interfaces for form values. The library provides full type inference.\n3. **Validation-first**: Always include validation and user-friendly error messages.\n\n## FormConfig Structure\n\n```typescript\nconst config: FormConfig = {\n // Optional: Default validation messages for all fields\n defaultValidationMessages: {\n required: 'This field is required',\n email: 'Please enter a valid email',\n minLength: 'Must be at least {{requiredLength}} characters',\n },\n // Required: Array of field definitions\n fields: [\n // ... field definitions\n ],\n};\n```\n\n## Field Definition Rules\n\n### Required Properties\n\nEvery field MUST have:\n\n- `key`: Identifier, unique within its group scope (the same leaf key may repeat inside different groups, e.g. `createA.name` and `createB.name`). Page/row/array containers do not introduce a scope. Buttons (which don't bind to form values) don't need unique keys.\n - **DOM id / data-testid composition:** keys are joined with `_` through group ancestors to form the rendered DOM id. A leaf `street` inside group `address` renders as `id=\"address_street\"`. Inside an array item, the item index is appended after: `id=\"address_street_0\"`. Use this exact form for `for=`, `aria-describedby`, `aria-labelledby`, or CSS selectors that target group-nested fields.\n - **Form-level prefix (multiple forms on one page):** set `options.idPrefix` to scope every id to a form instance \u2014 it becomes the OUTERMOST segment: `{idPrefix}_{group}_{key}_{index}` (e.g. `billing_address_street_0`). When omitted, a lone form stays unprefixed, but if two or more forms are mounted at once the library auto-prefixes each (`df-1`, `df-2`, \u2026) so their ids don't collide. Account for an explicit prefix when writing `for=`/`aria-*`/selectors.\n - **Avoid `_` in keys when they could collide with a group-prefixed id.** A top-level key `foo_bar` and a leaf `bar` inside group `foo` would both render as `id=\"foo_bar\"`. The validator catches this collision at boot, but it's easier to just not put `_` in keys that share a name with an existing group + leaf pair.\n- `type`: One of the registered field types\n- `label`: Human-readable label (for accessibility)\n\n### Value Fields\n\nFor fields that collect user input (`input`, `textarea`, `select`, `checkbox`, `radio`, `datepicker`, `toggle`, `slider`):\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n required: true, // Shorthand validator\n email: true, // Shorthand validator\n placeholder: 'user@example.com', // Field-level, not inside props\n validationMessages: { // Always provide clear messages\n required: 'Email is required',\n email: 'Please enter a valid email'\n },\n props: {\n type: 'email', // HTML input type\n hint: 'We will never share your email' // UI library specific\n }\n}\n```\n\n### Select/Radio Fields\n\nMUST include `options` array:\n\n```typescript\n{\n key: 'country',\n type: 'select',\n label: 'Country',\n required: true,\n placeholder: 'Select a country',\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' }\n ]\n}\n```\n\n### Container Fields\n\n#### Row (Horizontal Layout)\n\nUse for side-by-side fields. Row fields do NOT create nested values - children are flat.\n\n```typescript\n{\n key: 'nameRow',\n type: 'row',\n fields: [\n { key: 'firstName', type: 'input', label: 'First', col: 6 },\n { key: 'lastName', type: 'input', label: 'Last', col: 6 }\n ]\n}\n// Result: { firstName: '...', lastName: '...' }\n```\n\nUse `col` property (1-12) to control column widths.\n\n#### Group (Nested Object)\n\nUse when you need nested object structure in form values:\n\n```typescript\n{\n key: 'address',\n type: 'group',\n fields: [\n { key: 'street', type: 'input', label: 'Street' },\n { key: 'city', type: 'input', label: 'City' }\n ]\n}\n// Result: { address: { street: '...', city: '...' } }\n```\n\n#### Array (Repeatable Items)\n\nUse for dynamic lists. Each item is wrapped in a `<div class=\"df-array-item\">` with `role=\"group\"` and `aria-label=\"Item N\"` for accessibility. Use `--df-array-item-gap` CSS variable to control spacing between items.\n\n```typescript\n{\n key: 'contacts',\n type: 'array',\n // \u26A0\uFE0F NO label property on arrays!\n fields: [\n {\n key: 'contact',\n type: 'group',\n fields: [\n { key: 'name', type: 'input', label: 'Name', required: true },\n { key: 'phone', type: 'input', label: 'Phone', required: true }\n ]\n }\n ]\n}\n// Result: { contacts: [{ name: '...', phone: '...' }, ...] }\n```\n\n#### Page (Multi-step Forms)\n\nUse for wizard-style forms:\n\n```typescript\n{\n fields: [\n {\n key: 'step1',\n type: 'page',\n fields: [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'next', type: 'next', label: 'Continue' },\n ],\n },\n {\n key: 'step2',\n type: 'page',\n fields: [\n { key: 'prev', type: 'previous', label: 'Back' },\n { key: 'submit', type: 'submit', label: 'Submit' },\n ],\n },\n ];\n}\n```\n\n##### Starting on a specific page (deep links, session resume)\n\nSet `options.initialPage` to open a paged form somewhere other than page 0:\n\n```typescript\n{\n options: { initialPage: 2 }, // lands on page 2 regardless of earlier pages\n fields: [ /* pages */ ]\n}\n```\n\nThe shorthand lands unconditionally, which is what restoring a saved session wants.\nUse `{ index: 2, validate: true }` to apply the same validity gate a forward\n`GoToPageEvent` uses, which stops on the first invalid page instead.\n\nOut-of-range indices clamp to the last page; negative or non-numeric values fall back\nto `0`; a hidden target resolves to the nearest visible page.\n\nTo move pages at runtime, dispatch `GoToPageEvent`. `validate: false` bypasses the\nvalidity gate only \u2014 the target must still be in range and visible:\n\n```typescript\ndispatcher.dispatch(new GoToPageEvent(3)); // gated, stops on first invalid page\ndispatcher.dispatch(new GoToPageEvent(3, { validate: false })); // skips the gate, for resume\n```\n\nPaged forms may virtualize inactive pages. DynamicForm `(initialized)` emits after\nthe initially active page is ready. Use `(activePageInitialized)` when work must run\nafter every visible-page transition; it announces once per visit.\n\n### Rendering Performance\n\nUse `options.pagePreloadWindow` for paged forms. It controls how many neighbouring\npages are mounted and have their component chunks preloaded. `0` keeps only the\nactive page in the render window; the default is `1`.\n\nUse `options.fieldWindowing` for large flat forms:\n\n```typescript\n{\n options: {\n fieldWindowing: {\n eager: 20,\n placeholderHeight: '4rem',\n park: { margin: '100%' },\n },\n },\n fields: [ /* flat fields */ ],\n}\n```\n\n- `eager` mounts that many leading leaf fields immediately. Remaining fields mount near the viewport.\n- `placeholderHeight` reserves layout space before a field mounts.\n- `park` keeps mounted offscreen DOM in place but removes its view from routine change detection. Safety-related state such as disabled, readonly, required, and validation state stays current. Other model-to-DOM updates catch up when the field returns.\n- `park.margin` follows `IntersectionObserver.rootMargin`: use one to four `px` or `%` values. Unsupported units fall back to the inherited margin.\n- `fieldWindowing: false` disables inherited deferred mounting for that form.\n- A park-only object changes parking without changing the inherited mounting mode. Use `{ park: false }` to opt out or `{ park: true }` to opt in.\n- Deferred fields show an empty box. To show a skeleton, project `<ng-template dfPlaceholder let-field>` into the form template (import `DfPlaceholder`). `dfPlaceholderKey=\"key\"` beats `dfPlaceholder=\"type\"`, which beats an empty `dfPlaceholder`. This is template-only, not part of the config.\n\n## Validation\n\n### Shorthand Validators (Preferred)\n\nUse shorthand properties directly on field definitions:\n\n```typescript\n{\n key: 'age',\n type: 'input',\n label: 'Age',\n required: true,\n min: 18,\n max: 120,\n validationMessages: {\n required: 'Age is required',\n min: 'Must be at least {{min}}',\n max: 'Cannot exceed {{max}}'\n }\n}\n```\n\nAvailable shorthands: `required`, `email`, `min`, `max`, `minLength`, `maxLength`, `pattern`\n\n### Nullable Values\n\nValue fields and checked fields (`checkbox`, `toggle`) accept a `nullable?: boolean` flag. When `true`:\n\n- `value` accepts `null` in addition to the field's normal value type.\n- An omitted `value` resolves to `null` (instead of the type-specific empty default like `''`, `NaN`, `false`, or `[]`).\n- Orthogonal to `required`. `nullable` declares that the **model** accepts `null`; `required` is a **validation** constraint. Angular's `Validators.required` treats `null` as invalid, so a field that is both `nullable` and `required` will fail required-validation when the value is `null` \u2014 the two flags describe different layers (data shape vs. validation), not conflicting semantics.\n- Map OpenAPI `nullable: true` (3.0) or `type: [T, null]` (3.1) to this flag.\n- Container fields (`group`, `array`, `row`, `page`) do NOT support `nullable` \u2014 their null semantics are ambiguous.\n\n```typescript\n{\n key: 'middleName',\n type: 'input',\n label: 'Middle Name',\n nullable: true,\n value: null, // valid; default-resolves to null\n}\n\n{\n key: 'active',\n type: 'checkbox',\n label: 'Active',\n nullable: true,\n value: null, // undecided checkbox state \u2014 model permits null, UI renders unchecked\n}\n```\n\nRead-side caveat: a user clearing a text input reads back as `\"\"`, not `null` \u2014 this is a DOM/Web IDL contract, identical to classic Reactive Forms. `nullable` is a contract for accepted values, not a guarantee of emitted ones.\n\n### Validation Messages\n\nALWAYS provide validation messages. Use template variables:\n\n- `{{requiredLength}}` - for minLength/maxLength\n- `{{min}}`, `{{max}}` - for min/max\n- `{{requiredPattern}}` - for pattern\n\nTypeScript-authored configs may also use error-aware message functions: any `validationMessages` or `defaultValidationMessages` value can be `(error: ValidationError) => DynamicText` instead of a string/Observable/Signal. The function receives the validation error (kind plus params like `maxLength`), so i18n layers can receive interpolation params natively:\n\n```typescript\nvalidationMessages: {\n maxLength: (error) => transloco.selectTranslate('validation.maxLength', {\n requiredLength: 'maxLength' in error ? error.maxLength : undefined,\n }),\n}\n```\n\nFunction messages are NOT JSON-serializable. JSON-driven configs MUST keep using string templates with `{{param}}` interpolation; MCP-generated configs should always emit string templates.\n\n## Conditional Logic\n\nUse `logic` array for conditional behavior:\n\n```typescript\n{\n key: 'businessName',\n type: 'input',\n label: 'Business Name',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business'\n }\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business'\n }\n }\n ]\n}\n```\n\n### Logic Types\n\n- `hidden` - Show/hide field\n- `disabled` - Enable/disable field\n- `required` - Conditional requirement\n\n### Condition Types\n\n#### Field Value Condition\n\nCompares a specific field's value:\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business'\n}\n```\n\n#### Comparison Operators\n\n- `equals`, `notEquals`\n- `greater`, `less`, `greaterOrEqual`, `lessOrEqual`\n- `contains`, `startsWith`, `endsWith`, `matches`\n- An empty value (null, undefined or '') never satisfies the ordered or string operators. `matches` tests it as '', and 0 and false are not empty.\n\n#### JavaScript Condition\n\nEvaluates a JavaScript expression with access to `formValue`, `fieldValue`, `externalData`:\n\n```typescript\n{\n type: 'javascript',\n expression: 'formValue.items.length > 0'\n}\n```\n\n**Condition scoping:** in logic conditions, `formValue` is the whole form value regardless of where the field sits, except inside array items where it is scoped to the current item (use `rootFormValue` for the rest of the form). Reference group-nested fields by their full path, e.g. `formValue.person.firstName`. Note that `derivation` expressions scope differently: there `formValue` is also scoped to the parent group for group-nested fields.\n\n#### Custom Condition\n\nInvokes a registered custom function by name:\n\n```typescript\n{\n type: 'custom',\n functionName: 'myCustomCheck'\n}\n```\n\nRegister functions via `customFnConfig.customFunctions`.\n\n> **TypeScript-authored configs:** the same surface also accepts an inline `fn: CustomFunction` instead of `functionName` (XOR with `functionName`, code-only \u2014 not JSON-serializable). The MCP server emits `functionName` exclusively; consumers writing configs in TS may prefer `fn` when they don't need JSON round-tripping. Same caveat applies to async conditions (`asyncFn`), function derivations (`fn`), async function derivations (`asyncFn`), and validators (`fn`).\n\n#### Async Custom Condition\n\nInvokes a registered async function that returns `Promise<boolean>` or `Observable<boolean>`:\n\n```typescript\n{\n type: 'async',\n asyncFunctionName: 'checkPermission',\n pendingValue: false, // value while async resolution is pending (default: false)\n debounceMs: 300 // debounce for re-evaluation (default: 300)\n}\n```\n\nRegister functions via `customFnConfig.asyncConditions`. The function receives an `EvaluationContext` with access to `formValue`, `fieldValue`, and `externalData`.\n\n#### HTTP Condition\n\nEvaluates based on an HTTP response from a remote server. Fully declarative and JSON-serializable:\n\n```typescript\n{\n type: 'http',\n http: {\n url: '/api/permissions',\n method: 'GET',\n queryParams: {\n userId: 'formValue.userId',\n role: 'formValue.role'\n }\n },\n responseExpression: 'response.canEdit', // extract boolean from response (default: !!response)\n pendingValue: false, // value while HTTP request is in-flight (default: false)\n cacheDurationMs: 30000, // cache duration in ms (default: 30000)\n debounceMs: 300 // debounce for re-evaluation (default: 300)\n}\n```\n\nUse HTTP conditions when visibility, disabled state, or required state depends on server-side data (e.g., permissions, feature flags, inventory checks). The request re-evaluates reactively when dependent form values change.\n\n#### Logical Combinators\n\nCombine conditions with `and` (all must be true) or `or` (at least one must be true):\n\n```typescript\n{\n type: 'and',\n conditions: [\n { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'US' },\n { type: 'fieldValue', fieldPath: 'age', operator: 'greaterOrEqual', value: 18 }\n ]\n}\n```\n\n## UI Library Integration\n\nng-forge supports multiple UI libraries. Configure in `app.config.ts`:\n\n```typescript\nimport { provideDynamicForm } from '@ng-forge/dynamic-forms';\nimport { withMaterialFields } from '@ng-forge/dynamic-forms-material';\n\nexport const appConfig = {\n providers: [provideDynamicForm(...withMaterialFields())],\n};\n```\n\nAvailable adapters:\n\n- `@ng-forge/dynamic-forms-material` - Angular Material\n- `@ng-forge/dynamic-forms-bootstrap` - Bootstrap 5\n- `@ng-forge/dynamic-forms-primeng` - PrimeNG\n- `@ng-forge/dynamic-forms-ionic` - Ionic\n\n### Style Defaults (opt-in)\n\nEach adapter ships an opinionated Sass partial with host-framework colors/typography. Library SCSS uses `var(--df-name, structural-fallback)` so forms render usefully without any opt-in \u2014 only opt in if you want the adapter-themed look.\n\n```scss\n@use '@ng-forge/dynamic-forms-material/styles/defaults' as df;\n\n:root {\n @include df.apply-material-form-defaults;\n}\n```\n\nSubstitute `material` for `bootstrap`, `primeng`, or `ionic`. Material also ships `form-material-defaults-dark` for dark-mode hint/error colors. Ionic ships `apply-ionic-legacy-error-styles` that pairs with `withLegacyStatusClasses()` (see below).\n\n## Provider Features\n\n`provideDynamicForm()` accepts optional `with*` feature configurators alongside the adapter field bundle:\n\n### `withLegacyStatusClasses()` \u2014 opt into legacy ng-* CSS classes\n\nBy default, Angular Signal Forms uses its modern class strategy. If your CSS targets `.ng-touched`, `.ng-invalid`, `.ng-dirty`, etc. on form-bound elements, add this feature to wire up the compat-class strategy:\n\n```typescript\nimport { provideDynamicForm, withLegacyStatusClasses } from '@ng-forge/dynamic-forms';\nimport { withMaterialFields } from '@ng-forge/dynamic-forms-material';\n\nexport const appConfig = {\n providers: [\n provideDynamicForm(\n ...withMaterialFields(),\n withLegacyStatusClasses(),\n ),\n ],\n};\n```\n\nRecommended when consuming Ionic's `apply-ionic-legacy-error-styles` mixin, when using Bootstrap themes that style `is-invalid.ng-touched`, or any custom CSS that depends on the `ng-*` classes.\n\n### Other built-in features\n\n- `withLoggerConfig(options?)` \u2014 configure or disable the dynamic-form logger\n- `withEventFormValue()` \u2014 attach the current form value to every dispatched event\n- `withValueExclusionDefaults(options)` \u2014 set form-wide value exclusion defaults\n- `withValidationExecutionDefaults(options)` \u2014 set form-wide validation execution defaults (e.g. defer until touched)\n- `withExperimentalWebMcp()` \u2014 EXPERIMENTAL. Expose forms to browser AI agents as WebMCP tools. Opt a form in with `options.webMcp: { name, description }`; it registers `fill_{name}`, which validates and applies a partial patch of values to the form and returns what it set, which fields apply, which are still empty, and validation errors. Groups are merged key by key; lists are replaced whole, so an agent cannot rewrite a list whose items already hold values it is not allowed to set (a hidden id, a `readonly` price) \u2014 that write is refused rather than silently dropping them. Submission is a separate per-form opt-in: add `allowSubmit: true` to also register `submit_{name}`, which waits for the submission and reports its real outcome. Leave `allowSubmit` off for anything that spends money, sends a message, or cannot be undone. `webMcp.name` must be 1-121 characters of A-Z, a-z, 0-9, `_`, `-` or `.`, and short enough to read in a tool list. By default a response returns only the values the call set; `readback: 'all'` returns the whole readable model and hands the agent everything the form holds, so add it only when asked. Per field, `webMcp: { readable, writable }` (or `webMcp: false`) overrides the defaults: a `hidden` field type is neither readable nor writable, a password input is not readable, a `readonly` or derived field is not writable. Do NOT add `options.webMcp` unless the user explicitly asks for agent/WebMCP support.\n\nAll features are exported from `@ng-forge/dynamic-forms`.\n\n## Common Patterns\n\n### Always End with Submit Button\n\n```typescript\n{ type: 'submit', key: 'submit', label: 'Submit', props: { color: 'primary' } }\n```\n\n### Use Text Fields for Headings\n\n```typescript\n{ key: 'sectionTitle', type: 'text', label: 'Personal Info', props: { elementType: 'h3' } }\n```\n\n### Input Types via Props\n\n```typescript\n// Email input\n{ key: 'email', type: 'input', props: { type: 'email' } }\n\n// Password input\n{ key: 'password', type: 'input', props: { type: 'password' } }\n\n// Number input\n{ key: 'age', type: 'input', props: { type: 'number' } }\n\n// Phone input\n{ key: 'phone', type: 'input', props: { type: 'tel' } }\n```\n\n## Anti-patterns to Avoid\n\n1. **Missing labels** - Always include labels for accessibility\n2. **Missing validation messages** - Users need clear feedback\n3. **Duplicate keys within a group scope** - Keys must be unique within the same group. Same key in different groups is fine (different scoped paths). Buttons are exempt.\n4. **Select without options** - Select/radio fields must have options\n5. **Array without fields** - Array fields need a fields template\n6. **Group without fields** - Group fields need child fields\n7. **min > max** - Ensure min values are less than max values\n\n## CRITICAL: UI Library Differences\n\nDifferent UI libraries support different `props`. Always validate against the library the project actually uses.\n\nWhich props exist is answered per adapter, not here: use the adapter's own reference\n(`ngforge_lookup` with the adapter, or the `ng-forge-dynamic-forms-<adapter>` skill). A table\nrepeated here would be a second copy of the adapter registries and would drift from them.\n\nTwo adapters cannot both provide the same field type, so a project has one adapter per\nfield type; TypeScript reports error 2717 when two try.\n\n### Container Fields\n\nNo container has a `label`. Use a `text` field for a heading instead.\n\nValidation splits by whether the container owns a value:\n\n- `group` and `array` DO accept `required`, `validators`, `validationMessages`\n and `validateWhenHidden`. A validator there runs against the container's own\n subtree, so a cross-field rule (dateTo after dateFrom) belongs on the group\n rather than on one of its children. `ctx.value()` is the group's object or the\n array's item list.\n- `page`, `row` and `container` do NOT. They flatten into their parent and have\n no schema path, so a validator on them would have nothing to run against.\n\nLeaf-level validators such as `email`, `min` and `max` belong on the field that\nholds the value, on any container.\n\n### Common Mistakes to Avoid\n\n1. **Using `expressions` on fields** - This property is NOT supported on standard fields. Use `derivation` for computed values or `logic` for conditional behavior.\n\n2. **Adding `label` to container fields** - Pages, groups, and rows don't have labels:\n ```typescript\n // \u274C WRONG\n { key: 'address', type: 'group', label: 'Address', fields: [...] }\n\n // \u2705 CORRECT - Use a text field for the heading\n { key: 'addressTitle', type: 'text', label: 'Address', props: { elementType: 'h3' } }\n { key: 'address', type: 'group', fields: [...] }\n ```\n\n3. **Adding `id` to FormConfig** - The root FormConfig does not accept an `id` property.\n\n4. **Using `min`/`max` for a slider's range** - A slider's range is\n `minValue`, `maxValue` and `step`, at field level. `min` and `max` are\n validation shorthands: they typecheck, they validate clean, and the slider\n still renders its default 0-100 range, so nothing catches the mistake.\n ```typescript\n // \u274C WRONG - validates clean, renders the wrong range\n { key: 'volume', type: 'slider', label: 'Volume', props: { min: 0, max: 100 } }\n { key: 'volume', type: 'slider', label: 'Volume', min: 0, max: 100 }\n\n // \u2705 CORRECT\n { key: 'volume', type: 'slider', label: 'Volume', minValue: 0, maxValue: 100, step: 5 }\n ```\n\n5. **Using unsupported field types** - Each UI library supports specific field types. Use `ngforge_lookup` with `depth: \"schema\"` to see available types.\n\n## Validation Before Use\n\nNever hand back a config you have not checked. Two routes, depending on what is\navailable:\n\n- With the MCP server: `ngforge_validate({ uiIntegration: 'material', config })`\n- Without it: `npx --yes @ng-forge/dynamic-forms-cli@latest \"path/to/your.form.ts\" --ui material`\n\nBoth run the same schemas, so they agree.\n\n### What a clean run does and does not mean\n\nA clean run means the structure is right: the field types exist, required\nproperties are present, options are in the right place, and containers are not\ncarrying properties they cannot have.\n\nIt does NOT mean every property you wrote is real. The schemas strip unknown keys\nrather than rejecting them, so an invented or misplaced property passes\nvalidation and is silently dropped at runtime. Measured examples that pass today:\n`hideWhen` on a field and `targetProperty` inside a derivation.\n\n`as const satisfies FormConfig` is what catches those, because TypeScript checks\nthe property names. Use both: the type annotation for what exists, the validator\nfor what is structurally correct.\n\nTo see which properties a field type supports, use `ngforge_lookup` with\n`depth: \"schema\"`, or read the field-types reference in the skill.\n";
|
|
3
3
|
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/registry/instructions.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAE9B,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/registry/instructions.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAE9B,eAAO,MAAM,YAAY,urxBA2nBxB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-adapters.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/registry/ui-adapters.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,eAAO,MAAM,WAAW,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"ui-adapters.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/registry/ui-adapters.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,eAAO,MAAM,WAAW,EAAE,aAAa,EAuWtC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lookup-topics.d.ts","sourceRoot":"","sources":["../../../../../../packages/dynamic-form-mcp/src/tools/data/lookup-topics.ts"],"names":[],"mappings":"AAAA,yBAAyB;AAEzB,4CAA4C;AAC5C,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"lookup-topics.d.ts","sourceRoot":"","sources":["../../../../../../packages/dynamic-form-mcp/src/tools/data/lookup-topics.ts"],"names":[],"mappings":"AAAA,yBAAyB;AAEzB,4CAA4C;AAC5C,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA+hFlE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkDrD,CAAC;AAEF,yCAAyC;AACzC,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuJhD,CAAC"}
|