# GBMetaForm System — Technical Reference

**Location:** `features/gbmetaform/` · `features/gbmetaformrenderer/` · `libs/common/src/lib/gbformgroup/gbmetaformgroup.ts`  
**Purpose:** Metadata-driven dynamic form engine — form structure, validation, layout and CRUD are all driven by a JSON configuration, not hardcoded templates.

---

## Table of Contents

1. [Overview](#1-overview)
2. [Architecture](#2-architecture)
3. [File Reference](#3-file-reference)
4. [Metadata JSON Specification](#4-metadata-json-specification)
5. [Step-by-Step Data Flow](#5-step-by-step-data-flow)
6. [MetaBaseFormGroup API](#6-metabaseformgroup-api)
7. [Field Types Reference](#7-field-types-reference)
8. [Grid (FormArray) Usage](#8-grid-formarray-usage)
9. [Server Validation](#9-server-validation)
10. [Lookup / DependsOn](#10-lookup--dependson)
11. [Aspect Panel (Visibility Toggle)](#11-aspect-panel-visibility-toggle)
12. [Quick-Action Panel](#12-quick-action-panel)
13. [CRUD Operations](#13-crud-operations)
14. [Adding a New MetaForm](#14-adding-a-new-metaform)
15. [How Existing GB Directives Wire Up](#15-how-existing-gb-directives-wire-up)
16. [Constraints and Conventions](#16-constraints-and-conventions)

---

## 1. Overview

A normal GB form has hardcoded HTML (`gb-input`, `gb-newpicklist`, etc.) and a hand-written `GBBaseFormGroup` constructor call.  
A **MetaForm** has none of that. The only thing you write is a JSON file. The engine reads the JSON and:

- builds a fully typed `FormGroup` with validators
- decides which GB directive to render per field
- wires picklist lookups, dependent selects, and server validations
- exposes Save / Update / Delete tied to dot-separated API codes
- lets users toggle field visibility at runtime via the Aspect panel

**When to use MetaForm instead of a standard GB form:**

| Scenario | Standard form | MetaForm |
|---|---|---|
| Fixed, well-known form structure | ✔ preferred | works |
| Form structure varies per tenant/config | not practical | ✔ preferred |
| Rapid prototyping without HTML | not practical | ✔ preferred |
| Very complex conditional templates | ✔ preferred | partial support |

---

## 2. Architecture

```
┌────────────────────────────────────────────────────┐
│                   gbmetaform                       │
│                                                    │
│  MetaFormComponent  (gb-metaform)                  │
│  ├── loads JSON via MetaFormConverterService       │
│  ├── builds form via MetaBaseFormGroup.buildForm() │
│  ├── owns aspect, serverErrors, quickAction state  │
│  ├── registers server-validation subscriptions     │
│  └── calls MetaFormDbService for CRUD              │
│                                                    │
│  MetaFormRendererComponent  (gb-metaformrenderer)  │
│  ├── receives [metadata] [form] [aspect]           │
│  │   [serverErrors]                                │
│  ├── renders page tabs, sections, field grid       │
│  ├── switches on getFieldType() → GB directive     │
│  ├── renders FormArray grids (line items)          │
│  └── loads API lookups via MetaFormLookupService   │
└────────────────────────────────────────────────────┘

Support layer
  MetaBaseFormGroup  (libs/common)
  MetaFormDbService  (GBHttpService wrapper)
  MetaFormLookupService  (HttpClient + Map cache)
  MetaFormConverterService  (GB JSON → MetaForm metadata)
```

**Data direction:**

```
JSON → converter → metadata
metadata → MetaBaseFormGroup → FormGroup
FormGroup + metadata → [host] → [renderer] → DOM
user input → FormGroup.value → collectData() → API
```

---

## 3. File Reference

| File | Role |
|---|---|
| [gbmetaform.component.ts](../../features/gbmetaform/gbmetaform.component.ts) | Host shell: state, CRUD, server validations, quick-action panel |
| [gbmetaform.component.html](../../features/gbmetaform/gbmetaform.component.html) | Host template: aspect panel, action buttons, quick-action overlay |
| [gbmetaform.component.scss](../../features/gbmetaform/gbmetaform.component.scss) | Host styles: layout, buttons, quick-action panel |
| [gbmetaformrenderer.component.ts](../../features/gbmetaformrenderer/gbmetaformrenderer.component.ts) | Renderer: page nav, field type resolution, grid helpers, lookup loading |
| [gbmetaformrenderer.component.html](../../features/gbmetaformrenderer/gbmetaformrenderer.component.html) | Renderer template: page tabs, field grid, grids, server errors |
| [gbmetaformrenderer.component.scss](../../features/gbmetaformrenderer/gbmetaformrenderer.component.scss) | Renderer styles |
| [gbmetaformgroup.ts](../../libs/common/src/lib/gbformgroup/gbmetaformgroup.ts) | `MetaBaseFormGroup`: builds FormGroup/FormArray from metadata |
| [metaform.service.ts](../../features/gbmetaform/service/metaform.service.ts) | `MetaFormConverterService`: converts GB ObjectFields JSON to MetaForm metadata |
| [metaform.db.service.ts](../../features/gbmetaform/service/metaform.db.service.ts) | `MetaFormDbService`: HTTP calls via GBHttpService (post / get / delete) |
| [metaform.lookup.service.ts](../../features/gbmetaform/service/metaform.lookup.service.ts) | `MetaFormLookupService`: loads + caches API-driven select options |

---

## 4. Metadata JSON Specification

This is the schema the entire engine reads. All properties are optional unless marked **required**.

```jsonc
{
  // ── Top level ───────────────────────────────────────────────────────
  "formId":  "POForm",           // unique ID (string)
  "entity":  "PurchaseOrder",    // used as payload wrapper key on save
  "title":   "Purchase Order",   // display title (currently unused by renderer)

  // ── Pages (wizard steps) ────────────────────────────────────────────
  "pages": [                     // REQUIRED — at least one page
    {
      "pageId":   "Header",      // unique within form
      "pageName": "PO Header",   // shown in page-tab button

      "sections": [              // REQUIRED — at least one section
        {
          "sectionId":   "GeneralInfo",
          "sectionName": "General Information",  // section heading
          "layout": { "columns": 12 },           // CSS grid columns (default 12)

          // ── Normal fields ──────────────────────────────────────────
          "fields": [
            {
              "fieldId":   "PONumber",   // REQUIRED — FormControl name
              "fieldName": "PONumber",   // used in collectData key
              "label":     "PO Number",  // shown as GB directive label
              "type":      "text",       // see §7 Field Types
              "mandatory": true,         // adds Validators.required
              "pattern":   "^PO\\d+$",  // adds Validators.pattern (optional)
              "DefaultValue": "",        // FormControl initial value
              "PostData": true,          // false → excluded from collectData()
              "ReadOnly": false,
              "Width": 276,              // GB directive width
              "LabelWidth": 115,
              "layout": { "colSpan": 4 } // CSS grid column span (1–12)
            },
            {
              // Picklist field (uses existing GB newpicklist)
              "fieldId":      "ECMRoleCode",
              "label":        "Common.code",
              "PicklistJSON": "ECMRole",     // drives gb-newpicklist
              "PicklistTitle": "Code",
              "LinkId":        "ECMRoleId",  // foreign-key field populated on select
              "Multiple":      false,
              "AddNew":        true,
              "IsFirstField":  true,
              "Required":      true
            },
            {
              // API-driven select (metadata lookup)
              "fieldId": "SupplierId",
              "label":   "Supplier",
              "type":    "select",
              "lookup": {
                "api":          "Inventory.Supplier.GetAll",  // dot-separated code
                "valueField":   "id",
                "displayField": "name",
                "dependsOn":    ["WarehouseId"]  // re-fetches when these change
              },
              "mandatory": true
            }
          ],

          // ── Grid (FormArray line items) ────────────────────────────
          "grid": {
            "fieldId":    "Lines",          // REQUIRED — FormArray control name
            "entity":     "POLine",
            "allowAdd":   true,             // shows "+ Add Row" button
            "allowDelete": true,            // shows "✕" per row
            "columns": [
              {
                "fieldId": "ProductId",
                "label":   "Product",
                "type":    "select",
                "lookup": {
                  "api":          "Inventory.Product.GetAll",
                  "dependsOn":    ["WarehouseId"],
                  "valueField":   "id",
                  "displayField": "name"
                }
              },
              { "fieldId": "Quantity",  "label": "Qty",        "type": "number" },
              { "fieldId": "Price",     "label": "Unit Price",  "type": "number" },
              { "fieldId": "LineTotal", "label": "Total",       "type": "number", "ReadOnly": true }
            ]
          }

        }
      ]
    }
  ],

  // ── Rules (developer + implementation tiers) ─────────────────────────
  "rules": [
    {
      "id":        "line-total",
      "scope":     "implementation",
      "type":      "calculate",
      "trigger":   ["Quantity", "Price"],
      "formula":   "{Quantity} * {Price}",
      "target":    "LineTotal"
    },
    {
      "id":        "import-customs-fields",
      "scope":     "implementation",
      "type":      "visibility",           // 'visibility' | 'enable'
      "trigger":   ["POType"],
      "condition": "{POType} == \"Import\"",
      "targets":   ["CustomsDutyCode", "PortOfEntry"],
      "action":    "show"                   // 'show' | 'hide' | 'enable' | 'disable'
    }
  ],
  // NOTE: expressions in `formula`/`condition` (here and in serverValidations.condition,
  // quickActions[].visibleWhen) are evaluated by a restricted parser (arithmetic, comparison,
  // boolean logic over numbers/strings/booleans/null only) — no identifiers, no function calls,
  // no property access. This is deliberate: rules may be authored through a visual designer by
  // non-developers, so arbitrary code execution is not on the table.

  // ── Quick actions (slide-in micro-forms) ────────────────────────────
  "quickActions": [
    {
      "actionId":    "AddSupplierNote",
      "label":       "Add Supplier Note",
      "api":         "Po.SupplierNote.Add",   // dot-separated code called on Apply
      "visibleWhen": "{Status} != \"Delivered\"",  // optional — hides the action when false
      "fields": [
        { "fieldId": "Note", "fieldName": "Note", "type": "textarea" }
      ]
    }
  ],

  // ── Server validations (debounced 300 ms) ───────────────────────────
  "serverValidations": [
    {
      "id":         "CreditCheck",
      "trigger":    ["onChange"],
      "fields":     ["SupplierId"],          // watches these FormControls
      "api":        "Po.CreditCheck.Validate",
      "condition":  "{POType} != \"Service\"",  // optional — skips the call entirely when false;
                                                  // a network-call optimization only, the backend
                                                  // endpoint remains the authority
      "params": {
        "supplierId":    "SupplierId",       // key: param name, value: fieldId
        "orderAmount":   "TotalAmount"
      },
      "errorField":  "CreditLimitExceeded", // key in serverErrors signal
      "severity":    "error"
    }
  ],

  // ── CRUD API codes ───────────────────────────────────────────────────
  "api": {
    "save":   "Po.Purchase.Save",
    "update": "Po.Purchase.Update",
    "delete": "Po.Purchase.Delete"
  }
}
```

> **Important:** All `api` and `lookup.api` values must use the **dot-separated code pattern** (`Module.Entity.Action`) mapped in `projects/gbhost/public/api/gb5api/*.ts`. Do not put raw `.svc/` paths here.

---

## 5. Step-by-Step Data Flow

### Phase A — Initialization

```
1. ngOnInit() in MetaFormComponent
   │
   ├─ require('ecmrole.json')                ← loads raw GB ObjectFields JSON
   │
   ├─ converter.convert(json)                ← MetaFormConverterService
   │   maps ObjectFields[] → pages/sections/fields structure
   │   result stored as this.metadata
   │
   ├─ MetaBaseFormGroup.buildForm(metadata)
   │   for each field  → new FormControl(DefaultValue, validators)
   │                   → (ctrl as any).field = IField { Label, Type, PicklistJSON, ... }
   │                   → form.addControl(fieldId, ctrl)
   │   for each grid   → form.addControl(grid.fieldId, new FormArray([]))
   │   this._ready = true  → form.bFormReady = true
   │
   ├─ initAspect()
   │   aspect.visible = { ECMRoleCode: true, ECMRoleName: true, ... }
   │
   └─ registerServerValidations()
       for each sv in metadata.serverValidations:
         form.get(fieldName).valueChanges
           .pipe(debounceTime(300))
           .subscribe(() => _runServerValidation(sv))
```

### Phase B — Rendering

```
2. Template binds [metadata] [form] [aspect] [serverErrors] to renderer
   │
   ├─ form.bFormReady = true → <ng-container> shown
   │
   ├─ page tabs rendered (only if pages.length > 1)
   │
   ├─ currentPage.sections looped
   │   │
   │   ├─ visibleFields(sec.fields)
   │   │   filters aspect.visible[fieldId] !== false
   │   │
   │   ├─ for each visible field:
   │   │   getFieldType(field) → "picklist" / "input" / "date" / "select" ...
   │   │   [ngSwitch] renders matching GB directive or native element
   │   │   gb-* directive reads (control as any).field → self-configures
   │   │
   │   └─ if sec.grid:
   │       [formArrayName]="grid.fieldId"
   │       *ngFor row → [formGroupName]="i"
   │       each column → [formControlName]="col.fieldId"
   │
   └─ ngOnInit preloads lookups for fields with lookup.api (no dependsOn)
       MetaFormLookupService.load(api, {}) → HTTP GET → cache → markForCheck()
```

### Phase C — User Interaction

```
3. User edits a field
   │
   ├─ FormControl.value updates reactively
   │
   ├─ If field is in serverValidations.fields:
   │   debounceTime(300ms) fires
   │   POST to sv.api with mapped params
   │   response.errors → serverErrors signal updated
   │   renderer shows inline .server-error div
   │
   └─ If field is a dependsOn source for a lookup:
       user focuses the dependent select
       refreshDependentLookup() called
       MetaFormLookupService.load(api, { WarehouseId: currentValue })
       new cache entry → dropdown options updated
```

### Phase D — Save

```
4. User clicks Save
   │
   ├─ save() reads metadata.api.save
   │   if absent → returns immediately (no API configured)
   │
   ├─ collectData(metadata) builds payload:
   │   { ECMRoleCode: "ADMIN", ECMRoleName: "Administrator", ECMRoleId: 5 }
   │   (fields with PostData: false are excluded)
   │   (grid FormArrays are included as arrays)
   │
   ├─ MetaFormDbService.post(urlCode, payload)
   │   → GBHttpService.gbhttppost(urlCode, payload, false)
   │   → GbHttpFormingUrlService resolves dot-code → actual URL
   │   → HTTP POST → backend
   │
   └─ Success → GbDialogBoxComponent "Operation successful"
      Error   → GbDialogBoxComponent with error message
```

---

## 6. MetaBaseFormGroup API

**Class:** `MetaBaseFormGroup extends FormGroup`  
**Location:** [gbmetaformgroup.ts](../../libs/common/src/lib/gbformgroup/gbmetaformgroup.ts)

```typescript
// Factory (preferred)
const form = MetaBaseFormGroup.buildForm(metadata);

// Constructor (same result)
const form = new MetaBaseFormGroup(metadata);
```

| Method | Signature | Description |
|---|---|---|
| `buildForm` | `static buildForm(metadata): MetaBaseFormGroup` | Factory — builds and returns a ready FormGroup |
| `addGridRow` | `addGridRow(gridFieldId, grid): void` | Appends a new FormGroup row to the named FormArray |
| `removeGridRow` | `removeGridRow(gridFieldId, index): void` | Removes row at index from the named FormArray |
| `getGridArray` | `getGridArray(fieldId): FormArray` | Typed accessor for a grid's FormArray |
| `collectData` | `collectData(metadata): Record<string, any>` | Flattens form + grids to plain object for API |
| `bFormReady` | `get bFormReady(): boolean` | True after `_build()` completes — gate for template rendering |

**The `.field` contract:**  
Every `FormControl` created by `buildFromMeta` has `(ctrl as any).field` set to an `IField`-shaped object. All GB directive components (`gb-input`, `gb-newpicklist`, `gb-combobox`, `gb-date`, etc.) read `this.ngControl.control.field` on init to self-configure their label, width, picklist setup, required state, etc.

---

## 7. Field Types Reference

`getFieldType(field)` in the renderer resolves the following priority chain:

```
1. field.PicklistJSON exists          → "picklist"  → <gb-newpicklist>
2. field.FieldType or field.type:
   "picklist"                         → "picklist"  → <gb-newpicklist>
   "combobox"                         → "combobox"  → <gb-combobox>
   "select"                           → "select"    → native <select> + lookup
   "checkbox"                         → "checkbox"  → <gb-checkbox>
   "radio"                            → "radio"     → <gb-radiobutton>
   "textarea"                         → "textarea"  → <gb-textarea>
   "date"                             → "date"      → <gb-date>
   "number"                           → "number"    → <gb-input> (type on IField.Type)
   anything else / absent             → "input"     → <gb-input>
```

**GB directive types** (`picklist`, `combobox`, `checkbox`, `radio`, `textarea`, `date`, `input`)  
→ driven entirely by `(control as any).field` — no extra `@Input()` bindings needed in the template.

**Metadata select type** (`select` with `lookup.api`)  
→ rendered as a native `<select>`, options populated by `MetaFormLookupService`.

---

## 8. Grid (FormArray) Usage

A grid is defined in the metadata under `sec.grid` (not inside `sec.fields`).

```jsonc
"grid": {
  "fieldId":    "Lines",
  "allowAdd":   true,
  "allowDelete": true,
  "columns": [
    { "fieldId": "ItemCode", "label": "Item", "type": "input" },
    { "fieldId": "Qty",      "label": "Qty",  "type": "number" }
  ]
}
```

**FormGroup structure after two rows are added:**

```
FormGroup (MetaBaseFormGroup)
 └── Lines: FormArray
      ├── [0]: FormGroup
      │    ├── ItemCode: FormControl  (.field = { Label: "Item", ... })
      │    └── Qty:      FormControl  (.field = { Label: "Qty", Type: "number" })
      └── [1]: FormGroup
           ├── ItemCode: FormControl
           └── Qty:      FormControl
```

**Template binding chain:**

```html
<form [formGroup]="form">
  <ng-container formArrayName="Lines">
    <tr *ngFor="let row of getGridArray('Lines').controls; let i = index"
        [formGroupName]="i">
      <td><input type="text"   formControlName="ItemCode" /></td>
      <td><input type="number" formControlName="Qty"      /></td>
    </tr>
  </ng-container>
</form>
```

**`collectData` output with two rows:**

```typescript
{
  Lines: [
    { ItemCode: "BOLT-M8", Qty: 100 },
    { ItemCode: "NUT-M8",  Qty: 100 }
  ]
}
```

---

## 9. Server Validation

Server validations fire **on field value change, debounced 300 ms**, and map response errors to the `serverErrors` signal which flows down to the renderer as `[serverErrors]`.

**Metadata definition:**

```jsonc
"serverValidations": [
  {
    "id":        "CreditCheck",
    "fields":    ["SupplierId"],
    "api":       "Po.CreditCheck.Validate",
    "params": {
      "supplierId":  "SupplierId",
      "amount":      "TotalAmount"
    },
    "errorField": "SupplierId",
    "severity":   "error"
  }
]
```

**Flow:**

```
user changes SupplierId
  → 300ms debounce
  → POST Po.CreditCheck.Validate { supplierId: 3, amount: 50000 }
  → response: { errors: { SupplierId: "Credit limit exceeded" } }
  → serverErrors.set({ SupplierId: "Credit limit exceeded" })
  → renderer: <div class="server-error">Credit limit exceeded</div>

user changes supplier to a valid one
  → POST → response has no errors
  → serverErrors key "SupplierId" deleted
  → error div disappears
```

**`params` mapping** — keys are POST body parameter names, values are `fieldId`s read from the current FormGroup:

```typescript
// metadata: { "params": { "supplierId": "SupplierId" } }
params["supplierId"] = form.get("SupplierId")?.value;
```

---

## 10. Lookup / DependsOn

**Static lookup** (no `dependsOn`) — loaded once on renderer `ngOnInit`:

```jsonc
{
  "fieldId": "CurrencyId",
  "type":    "select",
  "lookup":  { "api": "Finance.Currency.GetAll", "valueField": "id", "displayField": "code" }
}
```

**Dependent lookup** (re-fetches when upstream field changes):

```jsonc
{
  "fieldId": "ProductId",
  "type":    "select",
  "lookup": {
    "api":        "Inventory.Product.GetForWarehouse",
    "dependsOn":  ["WarehouseId"],
    "valueField": "id",
    "displayField": "name"
  }
}
```

The select renders with `(focus)="refreshDependentLookup(field)"`.  
When focused, `MetaFormLookupService.load(api, { WarehouseId: currentValue })` is called.

**Cache key:** `api_url + JSON.stringify(params)`  
Identical requests within the session are served from the Map without a network call.

```typescript
// MetaFormLookupService internal cache
Map {
  "Finance.Currency.GetAll{}"              → [{ id:1, code:"USD" }, ...]
  "Inventory.Product.GetForWarehouse{\"WarehouseId\":10}" → [{ id:100, name:"Widget A" }]
  "Inventory.Product.GetForWarehouse{\"WarehouseId\":11}" → [{ id:200, name:"Widget X" }]
}
```

---

## 11. Aspect Panel (Visibility Toggle)

The right-side panel shows a checkbox per field, letting the user **hide/show individual fields at runtime** without touching the form value or validators.

**Initialisation:**

```typescript
// aspect.visible is populated from metadata on ngOnInit
aspect.visible = {
  ECMRoleCode: true,
  ECMRoleName: true,
  ECMRoleId:   true,   // ← LinkId fields are hidden from the panel (isAspectVisibleField)
}
```

**Hiding a field** (user unchecks the checkbox):

```
[(ngModel)]="aspect.visible['ECMRoleName']"  sets it to false
  → renderer: visibleFields(sec.fields) filters out ECMRoleName
  → field disappears from the grid
  → FormControl still exists and still holds its value
  → collectData() still includes ECMRoleName in payload (PostData: true)
```

**`isAspectVisibleField(f)`** — a field is hidden from the aspect panel if:
1. It has no `Label` / `label` — it is a technical/system field.
2. It is a `LinkId` target of another field — it is a hidden foreign-key field (e.g. `ECMRoleId` is the FK for `ECMRoleCode`).

**Precedence vs. rule-driven visibility (§4 `visibility`/`enable` rules, §10-style `DependendFieldName`):**
This panel is a manual, per-session personalization toggle — it's about a user reducing clutter
for themselves, not a business rule. **A `visibility`/`enable` rule always wins** when both apply
to the same field: `MetaFormRendererComponent.visibleFields()` checks `ruleVisibility[fieldId]`
first and only falls back to `aspect.visible[fieldId]` when no rule controls that field. The
checkbox itself is disabled (not removed) for rule-controlled fields, so it's visibly not a live
control rather than silently overridden.

---

## 12. Quick-Action Panel

A quick action is a lightweight **slide-in micro-form** for updating a single property on existing records without navigating away.

**Metadata:**

```jsonc
"quickActions": [
  {
    "actionId": "ChangeDelivery",
    "label":    "Change Delivery Date",
    "api":      "Po.Delivery.ChangeDate",
    "fields":   [
      { "fieldId": "DeliveryDate", "type": "date" }
    ]
  }
]
```

**Flow:**

```
user clicks "Change Delivery Date" button (rendered below the form, filtered by
  visibleQuickActions() — hidden if visibleWhen evaluates false)
  → openQuickAction(action) → isQuickActionOpen.set(true)
  → slide-in panel appears (z-index 901), owned by GbMetaQuickActionComponent
  → backdrop click → closeQuickAction()
  → Apply button → GbMetaQuickActionComponent.apply() merges its own small FormGroup
    with parentFormValue and POSTs directly to action.api via MetaFormDbService —
    this already happens, no extra wiring needed in MetaFormComponent
  → on success: actionComplete emitted → MetaFormComponent.closeQuickAction()
```

Optional `visibleWhen` (evaluated against the parent form's current value) hides the action
button entirely when false — e.g. `"visibleWhen": "{Status} != \"Delivered\""`.

---

## 13. CRUD Operations

All three operations follow the same pattern:

```
method()                         save() / update() / delete()
  │
  ├─ read metadata.api.save/update/delete
  │   if absent → return (no-op, no error)
  │
  ├─ form.collectData(metadata)
  │   → { entity: "PurchaseOrder", data: { PONumber: "PO1001", Lines: [...] } }
  │
  ├─ MetaFormDbService.post(urlCode, payload)
  │   → GBHttpService.gbhttppost(urlCode, payload, false)
  │
  ├─ success → GbDialogBoxComponent { heading: "Success" }
  └─ error   → GbDialogBoxComponent { heading: "Error" }
```

**`collectData` rules:**

| Condition | Included in payload? |
|---|---|
| `PostData: true` (default) | Yes |
| `PostData: false` | No (audit fields, display-only fields) |
| Grid FormArray | Always included as array |

---

## 14. Adding a New MetaForm

### Option A — Using the GB ObjectFields converter (existing JSON format)

**Not currently wired.** `MetaFormConverterService` (`features/gbmetaform/service/metaform.service.ts`)
implements the ObjectFields → MetaForm mapping this option describes, but nothing in the app
imports it today — `MetaFormComponent.ngOnInit()` only loads via Option B's registry path below.
Reactivating this converter (importing it, calling it from `ngOnInit()` when no full `.meta.json`
exists for an entity) is tracked as follow-up work, not something you can rely on yet.

### Option B — Full MetaForm JSON (multi-page, grids, server validation) — the only path that works today

1. Write the metadata JSON following §4, saved as `projects/{module}/public/metaform/{formId}.meta.json`.
2. Add an entry to `projects/gbhost/public/metaform-registry.json`: `{ "entity": "...", "roleId": 0, "formId": "..." }`.
3. Register API codes in `projects/gbhost/public/api/gb5api/{module}.ts`.
4. Register primary key in `projects/gbhost/public/formactionurls/idfield.ts` if needed.
5. Set `@Input() formId` (or route data `entity`) on `<gb-metaform>` — `ngOnInit()` resolves the
   registry entry via `MetaFormLoaderService.resolve()` and fetches the `.meta.json` itself; no
   `require()` call to wire up.

### Checklist

```
□ JSON file created (ObjectFields or full MetaForm format)
□ API codes registered in gb5api mapping
□ All lookup apis use dot-separated codes (no raw .svc/ paths)
□ PicklistJSON entries exist in allpicklist.json for picklist fields
□ PostData: false set on display-only / FK fields
□ Required: true set for mandatory fields
□ IdField entry added if the form supports Delete
□ Server validation endpoints return { errors: { fieldId: "message" } } or {}
```

---

## 15. How Existing GB Directives Wire Up

This is the **core mechanism** that makes the MetaForm work without any extra `@Input()` bindings.

**Step 1:** `MetaBaseFormGroup._toIField(f)` maps field metadata to the `IField` interface.

**Step 2:** The `IField` object is stored on the `FormControl`:
```typescript
(ctrl as any).field = iFieldObject;
```

**Step 3:** Every GB directive (`gb-input`, `gb-newpicklist`, `gb-combobox`, `gb-checkbox`, `gb-date`, `gb-radiobutton`, `gb-textarea`) reads this in `ngOnInit`:
```typescript
// Inside every GB directive
const controlField = (this.ngControl?.control as any)?.field as IField;
if (controlField) {
  this.Field.set(controlField);
  this.ComboValue.set(controlField.PickLists ?? []);
  // ... label, width, required, etc. all come from here
}
```

**Result:** The renderer template only needs `[formControlName]="field.fieldId"`. Everything else — label, type, width, picklist data, placeholder, readonly state — is resolved automatically by the directive.

---

## 16. Constraints and Conventions

| Rule | Reason |
|---|---|
| All API URLs in metadata must be dot-separated codes | Resolved by `GbHttpFormingUrlService`; raw paths bypass encryption, deduplication, and loading indicators |
| `PicklistJSON` fields render as `gb-newpicklist` regardless of `type` | GB picklist has richer behaviour (IndexedDB cache, search, add-new) than a native select |
| `PostData: false` for FK/LinkId fields | These are populated by picklist selection, not user input; sending them causes duplicate-key issues |
| `aspect.visible` does not affect `collectData` | Hiding a field is a UI concern only; the value is still submitted. Set `PostData: false` to exclude from payload |
| `MetaFormLookupService` uses `HttpClient`, not `GBHttpService` | Lookup URLs from metadata may be raw paths from external systems; they bypass the GB URL resolver intentionally |
| `serverValidations` fire on `valueChanges` only | They do not fire on form init; call `_runServerValidation` manually in `ngOnInit` if initial validation is needed |
| Grid columns use native inputs, not GB directives | GB directives inside nested `formGroupName` can cause `MenuId` conflicts; plain inputs are safe |
| `formula`/`condition`/`visibleWhen` expressions are parsed by a restricted evaluator (`metaform-expression-evaluator.ts`), never `eval`/`new Function` | Rules may be authored through a visual designer by non-developers; arbitrary JS execution from JSON is not acceptable |
| `DependendFieldName`/`DependendFieldValue` on a field are compiled into an equivalent `visibility` rule at load time (`compileLegacyVisibilityRules`) | Keeps existing JSON (e.g. `codedefine.meta.json`) working without a rewrite, now that this previously-parsed-but-ignored shape is actually evaluated |
| `visibility`/`enable` rules always override the aspect panel for the same field | The aspect panel is a manual personalization toggle, not a business rule — see §11 |
