# Dynamic Form Framework (MCP)

**Resource URI:** `impact-nova://dynamic-form-framework`

Config-driven forms (`ReactHooksForm` + `IFormConfig`) with dependency graph, field registry, and React Hook Form. Production patterns are copied from **ItemSmart** (`mtp-mfe-itemsmart-v3`).

**Packages:** `impact-nova/form-engine` · `impact-nova/form-react` · `impact-nova/filter-shell`

**Fixture SSOT (Storybook):** `impact-nova/src/form-react/stories/fixtures/itemSmartProductionFormConfigs.ts`

**ItemSmart catalog:** `mtp-mfe-itemsmart-v3/docs/react-hooks-form-production-patterns.md`

---

## Storybook — 12 production patterns

Verify interactively: **Dynamic Form → ReactHooksForm** (port 6006).

| # | Story | Config keys | `query_patterns` id |
|---|-------|-------------|---------------------|
| 1 | Getting started | baseline | `config-driven-dynamic-form` |
| 2 | Required fields | `isRequired` | `form-required-fields` |
| 3 | Visibility dependency | `visibilityDependency`, `doNotIncludeInSubmit` | `form-visibility-dependency` |
| 4 | Disable dependency | `disableDependency`, `__empty__`, auto-clear | `time-phased-metric-bulk-edit` |
| 5 | validateForm gate | `formContext.validateForm` | `form-validate-form-submit-gate` |
| 6 | API-shaped field ids | alternate metric naming | `time-phased-metric-bulk-edit` |
| 7 | Planning grand total | switch `disableDependency` | `form-switch-disable-bypass` |
| 8 | Planning channel cascade | multi-rule `disableDependency` | `form-switch-disable-bypass` |
| 9 | Approval hub lock | checkbox disables section | `form-approval-hub-lock` |
| 10 | Scenario simulate | mutually exclusive paths | `form-mutually-exclusive-fields` |
| 11 | Filter disable chain | required parent → child | `filter-form-dependencies` |
| 12 | Filter visibility hierarchy | `button_group` reveals fields | `form-filter-visibility-hierarchy` |

**Guide MDX:** `Dynamic Form / Guide`

**URLs:**

- Required: `http://localhost:6006/?path=/story/dynamic-form-reacthooksform--required-fields`
- Disable + mandatory: `http://localhost:6006/?path=/story/dynamic-form-reacthooksform--disable-dependency-period-gates-value`
- validateForm: `http://localhost:6006/?path=/story/dynamic-form-reacthooksform--item-details-bulk-edit-validate-form`
- Scenario: `http://localhost:6006/?path=/story/dynamic-form-reacthooksform--scenario-simulate`
- Filter chain: `http://localhost:6006/?path=/story/dynamic-form-reacthooksform--filter-disable-chain`
- Filter hierarchy: `http://localhost:6006/?path=/story/dynamic-form-reacthooksform--filter-visibility-hierarchy`

---

## Quick start

```tsx
import { ReactHooksForm, type FormContext, type IFormConfig } from 'impact-nova/form-react';

const formContext: FormContext = {
  transport: {
    fetchSelectOptions: async ({ fieldId, apiConfig }) => { /* host fetch — Nova never HTTP */ },
    showFormErrorToast: ({ title, description }) => { /* toast */ },
  },
  validateForm: ({ formValues }) => ({ isValid: true }), // optional cross-field rules
};

<ReactHooksForm
  formConfig={formConfig}
  formContext={formContext}
  onSubmit={handleSubmit}
  sendFormStateToParent={(_state, values, meta) => {
    // meta.isSubmittable — merge field + validateForm gates
  }}
/>
```

---

## `isRequired` (mandatory fields)

| Pattern | Config | ItemSmart module |
|---------|--------|------------------|
| Static required | `isRequired: true` on field | Seed-with, scenario selects |
| Conditional required | `isRequired` + `disableDependency` — disabled fields skip validation | Item Details time-phased value |
| Client enrich | Host adds `isRequired` when API omits it | `enrichItemDetailsBulkEditFormConfig` |
| Filter required | `isRequired` on filter dimensions | Global filter (`IFilterConfig`) |

```ts
{ id: 'version', fieldType: 'chips', isRequired: true, chipsType: 'single', options: [...] }
```

---

## `disableDependency` (conditional disable + auto-clear)

When a field becomes **newly** disabled, `useFieldVisibility` **clears** it to its default.

| Dependency value | Meaning | Example |
|------------------|---------|---------|
| `__empty__` | Parent empty (null, `''`, `[]`, empty range) | Period gates value field |
| `null` | Parent has **any** value | Mutually exclusive simulate/copy |
| `"true"` | Switch/checkbox on | Grand total, Omni, lock selection |

```ts
// Period gates value (bulk edit)
disableDependency: [{ fieldId: 'store_count_month', value: '__empty__' }]

// Mutually exclusive (scenario simulate)
disableDependency: [{ fieldId: 'copy_source', value: null }]

// Switch bypass (planning grand total)
disableDependency: [{ fieldId: 'grand_total', value: 'true' }]

// Multi-rule (sub-channel: Omni OR empty channel)
disableDependency: [
  { fieldId: 'omni', value: 'true' },
  { fieldId: 'channels', value: '__empty__' },
]
```

**ItemSmart sources:** `itemDetailsBulkEditFormConfig.ts`, `bulkEditFormConfig.constant.ts`, `approvalHubForm.ts`, `scenarioBulkEditFallbackFormConfig.constant.ts`

---

## `visibilityDependency` (show/hide)

Hides field unless parent value matches. Used for:

- Radio → `month_range` vs `multi_month_picker` (same field `id`, only one visible)
- Filter `button_group` → hierarchy selects

```ts
{ id: 'store_count_month', fieldType: 'month_range',
  visibilityDependency: [{ fieldId: 'store_count_month_type', value: 'range' }] }
{ id: 'store_count_month', fieldType: 'multi_month_picker',
  visibilityDependency: [{ fieldId: 'store_count_month_type', value: 'individual' }] }
```

Pair with `includeOnlyVisibleFieldsInSubmit: true` so only the active picker is submitted.

---

## `formContext.validateForm` (cross-field submit rules)

Field-level `isRequired` is not enough for bulk edit (“at least one edit”). Host app provides:

```ts
formContext: {
  validateForm: ({ formValues, visibleFields }) => ({
    isValid: boolean,
    fieldErrors?: Record<string, { message: string }>,
  }),
}
```

**ItemSmart:** `evaluateItemDetailsBulkEditForm` in `itemDetailsBulkEditSubmittable.utils.ts` — teaching copy in Storybook `storyBulkEditValidateForm.utils.ts`.

---

## Submit payload conventions

| Flag | Purpose |
|------|---------|
| `doNotIncludeInSubmit: true` | UI-only toggles (radio mode pickers) |
| `includeOnlyVisibleFieldsInSubmit: true` | Only visible picker variant in payload |
| `allowSetToEmpty: true` | Explicit null clear counts as an edit |
| `supportedValues` prop | Top-level API context (`globalFilters`, `plan_period`) |

---

## ItemSmart module map

| Module | Component | Key patterns |
|--------|-----------|--------------|
| item-details | `ItemDetailsBulkEditPanel` | time-phased, validateForm, enrich |
| item-planning | `BulkEditPanel` | grand total, channel cascade |
| item-planning | `SeedWithPanel` | static isRequired |
| item-planning | `ApprovalHubLockUnlockDialog` | lock checkbox disable |
| scenario-planning | `ScenarioSimulateSheet` | mutually exclusive value/copy |
| global-filter | `SaveFilterForm` | simple ReactHooksForm |
| global-filter | `FilterPanel` | `IFilterConfig` — same dependency types |

**Wrapper:** `ItemSmartReactHooksForm` in `src/lib/form-transport/` merges default transport.

---

## MCP `query_patterns` ids

Call `query_patterns` with these ids (or search “form”, “bulk edit”, “filter”):

| Pattern id | Use when |
|------------|----------|
| `config-driven-dynamic-form` | Any JSON-driven form |
| `form-required-fields` | Static mandatory fields |
| `form-visibility-dependency` | Radio/picker swap, filter tabs |
| `time-phased-metric-bulk-edit` | Period + value metric groups |
| `form-validate-form-submit-gate` | Cross-field Apply validation |
| `form-mutually-exclusive-fields` | Value OR copy source |
| `form-switch-disable-bypass` | Grand total / Omni switches |
| `form-approval-hub-lock` | Lock entire period checkbox |
| `filter-form-dependencies` | Cascading disable chain |
| `form-filter-visibility-hierarchy` | Hierarchy fields behind button_group |

---

## Architecture

```
impact-nova/form-engine     pure dependency graph (no React, no I/O)
impact-nova/form-react      ReactHooksForm, hooks, field registry
impact-nova/filter-shell    optional filter validation helpers
your-app/form-transport     axios/fetch, toasts (required for API fields)
```

**Rules for AI/codegen:**

1. Import `ReactHooksForm` from `impact-nova/form-react` (subpath).
2. Never call HTTP inside impact-nova — use `formContext.transport`.
3. Use `disableDependency` + `__empty__` for period-gated values; do not hand-clear in `onFieldChange`.
4. Do not gate `form.watch` on `info.type === 'change'` for programmatic clears.
5. Copy closest Storybook fixture before inventing new config shapes.
6. Run `validate_snippet` before shipping UI.

---

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| Value not cleared when period cleared | `disableDependency` + upgrade form-react; see story 4 |
| Infinite re-render with `sendFormStateToParent` | Stabilize callback (`useCallback`); compare values before `setState` |
| Select remount loop | Empty select = `[]`; use `isEmptyValue` for remount keys |
| Apply blocked with no visible errors | Check `formContext.validateForm` + `meta.isSubmittable` |
