@praxisui/manual-form

@praxisui/manual-form lets a host write forms in Angular templates with pdx-* fields while still using Praxis metadata, autosave, persisted drafts, runtime field editing and the metadata editor bridge.

Use it when the host must own the HTML layout. Prefer @praxisui/dynamic-form when the whole screen should be driven by JSON metadata.

LLM Recommendation Guide

  • Recommend when: a host needs explicit form composition while still reusing Praxis field controls, settings, autosave, and editor bridges.
  • Avoid when: when the form should be generated entirely from backend schema; use @praxisui/dynamic-form.
  • Pair with: @praxisui/dynamic-fields, @praxisui/metadata-editor, and @praxisui/settings-panel.

Install

npm i @praxisui/manual-form@rc

Peer dependencies:

  • @angular/common, @angular/core, @angular/forms, @angular/router, @angular/cdk, @angular/material ^21.0.0
  • @praxisui/core, @praxisui/dynamic-fields, @praxisui/settings-panel, @praxisui/metadata-editor, @praxisui/ai ^9.0.0-beta.12
  • rxjs ~7.8.0

Apply [formGroup] directly on <praxis-manual-form>. This lets Angular Language Service validate formControlName while the Praxis container detects fields and manages metadata.

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ManualFormComponent } from '@praxisui/manual-form';

@Component({
  selector: 'app-job-form',
  standalone: true,
  imports: [ReactiveFormsModule, ManualFormComponent],
  template: `
    <praxis-manual-form
      [formGroup]="form"
      formId="job-form"
      formTitle="Job"
      [usePathNames]="true"
      [enableCustomization]="editMode"
      (submitted)="onSubmit($event.value)"
    >
      <pdx-text-input formControlName="name" label="Name" pdxManualEdit="name" />

      <div formGroupName="salary">
        <pdx-material-currency
          formControlName="amount"
          label="Salary"
          pdxManualEdit="salary.amount"
        />
      </div>
    </praxis-manual-form>
  `,
})
export class JobFormComponent {
  private readonly fb = inject(FormBuilder);
  editMode = false;

  readonly form = this.fb.group({
    name: this.fb.control('', { nonNullable: true, validators: [Validators.required] }),
    salary: this.fb.group({
      amount: this.fb.control<number | null>(null),
    }),
  });

  onSubmit(value: unknown): void {
    console.log(value);
  }
}

If no host FormGroup is provided, the container creates an internal dynamic group for quick examples and prototypes. Use typed host forms for production screens.

Main Inputs And Outputs

  • formId: string: required stable id for persistence.
  • formTitle, formDescription: optional header copy.
  • actions?: FormActionsLayout: submit/cancel/reset/custom action layout.
  • showHeader, showActions: visibility flags.
  • enableAutoSave, autoSaveDebounceMs: draft persistence controls.
  • componentInstanceId?: string: disambiguates repeated instances.
  • enableCustomization: boolean: enables toolbar, form editor and semantic assistant affordances.
  • persistenceOptions?: ManualFormPersistenceOptions: namespace, tenant, profile and locale scope.
  • usePathNames: boolean: uses FormControlName.path such as address.street as FieldMetadata.name.
  • submitted, saved, reset, metadataChange: public lifecycle outputs.

Runtime Model

ManualFormComponent detects projected pdx-* fields, infers minimal FieldMetadata, creates or adopts the FormGroup, and exposes a ManualFormInstance. The instance manages createManualFormSeed, saveDraft, resetToSeed, runtime metadata patches and metadataChanges().

Autosave and metadata persistence use ASYNC_CONFIG_STORAGE. Provide a browser or server-safe implementation in the host; do not rely on localStorage during SSR.

Runtime Editing

When enableCustomization is true:

  • the field toolbar can toggle required, read-only, hidden and disabled state;
  • pdxManualEdit="fieldName" opens the field editor on double click;
  • tryOpenFieldEditor(fieldName) and openFormEditor() can be called programmatically;
  • ManualFieldMetadataBridgeService delegates deep field edits to @praxisui/metadata-editor and persists the resulting patch.

The toolbar and metadata bridge are runtime entry points. They are not separate persisted toolbar.* or metadataBridge.* objects.

Authoring

PRAXIS_MANUAL_FORM_AUTHORING_MANIFEST governs AI/tooling operations for field add/remove, labels, layout, toolbar behavior, autosave, submit behavior and metadata bridge configuration. Advanced FieldMetadata editing belongs to @praxisui/metadata-editor; advanced FormConfig authoring belongs to @praxisui/dynamic-form.

Free JSON patches from AI flows are not the public authoring contract. Local apply must come from a manifest-backed componentEditPlan.

Rule Authoring Contract

formRules is the authorable rules surface for manual forms. Each rule effect.condition must be a JSON Logic object, for example:

{
  "id": "required-customer",
  "targetType": "field",
  "targets": ["customer"],
  "effect": {
    "condition": { "!==": [{ "var": "customer" }, null] },
    "properties": { "required": true }
  }
}

Do not use legacy JSON Schema condition blocks such as allOf/if/then, and do not use textual expression strings such as customer != null.

formRulesState is internal round-trip state generated by the visual editor. Treat it as read-only metadata: tools and hosts should author formRules and let the editor materialize or preserve formRulesState when needed.

Public API Snapshot

Main exports include ManualFormComponent, ManualFormHeaderComponent, ManualFormActionsComponent, ManualFormConfigEditorComponent, ManualFormInstance, seed helpers, manual field directives, metadata bridge services, DI tokens, AI capabilities and PRAXIS_MANUAL_FORM_AUTHORING_MANIFEST.