@praxisui/dynamic-form

Schema-driven and metadata-driven form runtime for Praxis UI.

Use this package to render FormConfig and FieldMetadata, connect forms to backend schema/submit endpoints, host visual configuration, and keep form layout, rules, actions, and submit payloads governed by shared Praxis contracts.

LLM Recommendation Guide

  • Recommend when: users need schema-driven or metadata-driven Angular forms that can bind to Praxis resource paths, local FormConfig, hooks, validation, and governed authoring.
  • Avoid when: for plain static Angular forms with no runtime metadata, backend schema, or configuration requirement.
  • Pair with: @praxisui/dynamic-fields, @praxisui/core, @praxisui/metadata-editor, and praxis-metadata-starter.

Install

npm i @praxisui/dynamic-form@rc

Peer dependencies:

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

Reactive backend determinations

When the exact request schema from /schemas/filtered publishes root-level x-ui.reactiveDeterminations, the form executes the Metadata-compiled backend capability after a valid source changes and applies server-derived draft values. After edit/create hydration, an already populated source is also validated before the draft can be reported as stable; an empty initial source settles as not applicable without an HTTP call. The projection is one closed graph: if any definition is malformed, the complete projection is rejected instead of partially executing. It remains backend-owned, tenant-neutral and transient: it is never copied to FormConfig and cannot be authored as a callback, URL or local write policy.

Triggers, source fields and request/response bindings use JSON Pointer. The runtime supports nested object bindings, accepts at most 64 total input/output bindings and rejects duplicate or hierarchically overlapping pointers. It resolves the capability only against the configured API origin and rejects protocol-relative, encoded-separator or cross-origin targets. Output bindings have one closed meaning: server-derived draft values replace their targets. Metadata cannot select if-pristine, if-empty or another incidental UI policy.

The runtime waits until hydration finishes and invalidates the previous generation as soon as a newer relevant value or validation status arrives, before the new debounce completes. sourcePaths is an intentional trigger subset of inputs: changing a non-trigger input invalidates the current draft, emits input-changed-awaiting-trigger, and blocks submit/navigation until a source trigger recalculates it. Every configured response path is resolved before target controls are written; all nested targets are populated before change observers are notified. Chained determinations receive one logical batch signal after the complete commit, rather than one execution per output control. Their dependency order uses hierarchical input/output JSON Pointer overlap, and the canonical projection rejects cycles and overlapping multi-writers. Downstream waits while upstream is pending and runs only after authoritative outputs or confirmed unchanged inputs. Null/undefined upstream outputs and failed, invalid or cancelled upstream generations settle downstream as skipped instead of invoking it with missing or stale data. If a control rejects a write, the runtime attempts to restore earlier writes and reports a terminal failure. This is a coordinated client-side batch, not a transaction or an ACID guarantee. Each capability remains a separate HTTP request, so an activation may occur between two chained previews; only a host command that resolves and revalidates one aggregate can claim snapshot consistency for the complete business decision.

A determination remains pending through debounce, asynchronous validation and capability execution. Submit attempts wait with a bounded timeout and repeat the stability check after hooks that may mutate controls. Output controls are disabled transiently as server-owned draft values, but remain present in getRawValue(), valueChange and submit payloads; this state is not persisted in FormConfig.

Hosts can observe metadata-only evidence through reactiveDeterminationExecuted and aggregate state through reactiveDeterminationPendingChange. Before navigating or closing a composed surface, inspect hasPendingReactiveDeterminations() and hasUnsatisfiedReactiveDeterminations() and await waitForReactiveDeterminations(timeoutMs?); false means timeout or that the latest generation did not produce a current authoritative draft, and must block the transition. A programmatic change to an owned output invalidates the previous authority and triggers a new determination. Cancelled pending correlations terminate as skipped/superseded. No event or method exposes form or response values.

Use optionSource.dependsOn for dependent option lists and workflow actions for persistent transitions. Recommendations, side-effect refresh and backend validation are separate capabilities. The final backend command must still authorize, recompute or validate fiscal, pricing, eligibility, compliance and approval outcomes.

Minimum Local Runtime

Use a local FormConfig when the host already knows the sections and fields.

import { Component } from '@angular/core';
import { FormConfig } from '@praxisui/core';
import { PraxisDynamicForm } from '@praxisui/dynamic-form';

@Component({
  standalone: true,
  selector: 'app-local-form',
  imports: [PraxisDynamicForm],
  template: `
    <praxis-dynamic-form
      formId="employee-local-form"
      mode="create"
      [config]="config"
      (formSubmit)="save($event.formData)">
    </praxis-dynamic-form>
  `,
})
export class LocalFormComponent {
  config: FormConfig = {
    sections: [
      {
        id: 'main',
        title: 'Employee',
        rows: [
          {
            columns: [
              {
                id: 'name-column',
                items: [{ kind: 'field', id: 'name-field', fieldName: 'name' }],
                fields: ['name'],
              },
            ],
          },
        ],
      },
    ],
    fieldMetadata: [
      { name: 'name', label: 'Name', controlType: 'input', required: true },
    ],
  };

  save(payload: unknown): void {}
}

Minimum Backend Runtime

For schema-driven backend surfaces, prefer explicit schemaUrl, submitUrl, and submitMethod.

<praxis-dynamic-form
  formId="employee-edit"
  mode="edit"
  resourcePath="/api/employees"
  [resourceId]="employeeId"
  schemaUrl="/api/employees/schemas/edit"
  readUrl="/api/employees/42"
  submitUrl="/api/employees/42"
  submitMethod="PUT"
  (formSubmit)="onSubmitted($event)">
</praxis-dynamic-form>

resourcePath can connect the form to backend discovery, but it is not the only or always preferred minimum. Detached dialog/drawer hosts may also need apiEndpointKey or apiUrlEntry so relative runtime URLs resolve against the intended backend.

When an action protects a different aggregate with optimistic concurrency, the core action adapter may also materialize submitResourceVersionTargetResourceKey and submitResourceVersionTargetIdField. Dynamic Form resolves that exact resource and scalar request field only after the final submit payload exists, reads the current representation, and forwards its ETag as If-Match. Missing target metadata, a divergent catalog, a non-scalar id, or a response without ETag blocks the command. Hosts must not derive this target from labels, routes, lookup display objects, or action names.

When using resource, schema, read, or submit flows, the host must provide the effective API/CRUD service wiring for that scope.

Schema-driven layout materialization is opt-in through layoutPolicy. Use it when the backend schema already publishes enough x-ui metadata for the runtime to build the layout without local FormConfig.sections.

<praxis-dynamic-form
  formId="employee-detail"
  mode="view"
  schemaUrl="/schemas/filtered?path=/api/employees/{id}&operation=get&schemaType=response"
  [initialValue]="employee"
  [layoutPolicy]="{
    source: 'schema',
    intent: 'detail',
    preset: 'compactPresentation',
    lifecycle: 'live',
    persistence: 'transient',
    schemaOperation: 'detail',
    schemaType: 'response'
  }">
</praxis-dynamic-form>

For create/edit command forms, the policy must point at a request schema, normally through an explicit schemaUrl containing schemaType=request and operation=post|put|patch. If the declared schemaUrl conflicts with layoutPolicy.schemaType or layoutPolicy.schemaOperation, the runtime fails initialization instead of rendering a form from the wrong business contract.

layoutPolicy.source = "schema" ignores authored or persisted sections, but preserves non-layout host/persisted concerns such as actions, api, messages, rules, hooks and rich blocks. Host-provided non-layout config has precedence over persisted config.

For nested commands, layoutPolicy.groupedCommand.contextFields identifies request fields whose values are supplied by the surrounding surface (for example, the employee id inherited by a related-resource drawer). These fields remain schema-backed controls in the runtime form and are included in the submit payload, but are omitted from the interactive layout. The host must hydrate them through initialValue on create or through the entity read on edit; this policy must not hide values that the user is expected to choose.

For generated command groups, the default layoutPolicy.groupedCommand.partialRowStrategy = "preserve" keeps published spans unchanged. Use "fill-compatible" when eligible controls should absorb the remaining 12-column space at each visual row after responsive visibility, CSS order and wrapping are resolved (for example, 4 + 4 projects to 6 + 6, while 6 + 6 + 6 projects to visual rows 6 + 6 and 12). This is a transient runtime projection: schema x-ui.width, generated sections and authoring documents are not rewritten. Compact boolean/action controls, uploads and unknown control types remain at their canonical span.

When mode="view" and the host does not provide presentationModeGlobal, a schema policy with intent: "detail" or preset: "compactPresentation" also enables presentation rendering. Hosts that need traditional disabled controls for the same schema can pass [presentationModeGlobal]="false". For compact detail screens, explicit schema x-ui.width values are honored as 12-column spans so fieldsets can render multiple read-only fields per row. Fields without an explicit width remain full-width to preserve existing layouts. Generated compact sections also receive semantic header icons derived from their group role, while field icons continue to come from existing schema metadata. In runtime rendering, compact detail containers are kept visible only when at least one referenced field can actually be materialized after metadata visibility, visible=false, hidden=true and field-rule overrides are applied. Boolean detail fields are valid presentation content, including false values; marker/flag groups should render their boolean values, not collapse them as empty strings. If a section has no materialized fields, the runtime hides the section instead of leaving an orphan title. When a migrated detail surface needs the summary density to win over generic schema widths, set detailSummary.widthPrecedence: "summary" together with columns or responsiveColumns. The default is "schema", so existing forms keep honoring x-ui.width unless the summary policy explicitly opts into overriding it for the compact read-only surface. Use "summary" for curated operational summaries. If a field contains long narrative text, legal content, observations, or values that require scan width, keep schema precedence for that surface or exclude the field from the compact summary and expose it in a secondary detail group.

For fields published by the backend schema, server DTO metadata remains authoritative for semantic presentation data such as label, hint, helpText, tooltip, tooltipOnHover, and icon metadata. Local FormConfig should customize layout, grouping, transient fields, actions, and host-specific behavior, but it should not redefine DTO-owned field semantics.

Field-level access metadata follows the same authority boundary. When the backend publishes x-ui.fieldAccess, @praxisui/core preserves it in FieldMetadata.fieldAccess and @praxisui/dynamic-form can materialize it as hidden or read-only UX when the host supplies explicit FormConfig.metadata.fieldAccessAuthorities or enterprise runtime authorities. The runtime does not infer field authorities from capabilities; backend filtering and submit validation remain the security boundary.

Dense operational forms should keep DTO hint/helpText intact and use FormConfig.helpPresentation to choose the visual policy. For drawers or command forms, prefer display: "auto" with preferPopoverForControls for controls such as toggle, checkbox, and select; validation errors remain inline regardless of this policy. Field-level helpDisplay is still authoritative when present, including helpDisplay: "auto" for DTOs that need a local threshold.

Submit failures published in the Praxis RestApiResponse.errors envelope are materialized by the runtime. A safe public message is shown to the user, while an error with a target matching a form control is also attached to that control and marked as touched. Existing client validators are preserved. Each field-owned server error is removed as soon as that field changes, without clearing client validators or errors on other controls; all remaining server errors are cleared before the next submit attempt and after a successful submit. Targets may use Angular dotted paths or JSON Pointer syntax. Unknown or targetless details remain form-level feedback; the client does not guess a field from error text, codes, keywords, or private database details.

Structured details and public backend messages are trusted only for known HTTP 4xx responses. Network failures, 5xx responses, and responses without a trustworthy status fail closed to the localized platform messages under global.submitError.*, even when their payload contains message or errors. For a governed 4xx response with structured details, the backend-owned business message has precedence over generic copy configured in messages. Without structured details, configured create/update/custom submit copy remains authoritative and the normalized platform fallback is used only when no configured copy exists.

Example backend-owned x-ui metadata:

{
  "type": "object",
  "properties": {
    "frequencyType": {
      "type": "string",
      "x-ui": {
        "label": "Frequency type",
        "controlType": "async-select",
        "helpText": "Select the operational class used by payroll and timekeeping rules.",
        "tooltip": "Backend-owned DTO semantics; local FormConfig cannot override it.",
        "tooltipOnHover": true,
        "icon": "calendar_month",
        "iconPosition": "start",
        "iconColor": "primary",
        "endpoint": "/api/frequency-types/options/filter",
        "filterField": "name",
        "sortField": "name",
        "sortOrder": "asc",
        "optionsPageSize": 20
      }
    }
  }
}

In that flow, local FormConfig may place frequencyType in sections, rows, tabs, custom actions, or corporate-only local fields. It should not persist competing label, helpText, tooltip, or icon metadata for frequencyType, because the next schema reconciliation will restore the backend DTO semantics.

ErgonX migration baseline

For ErgonX-style migrations, use schema-driven layout as the preferred target once the DTO/schema is semantically ready. Treat local FormConfig.sections as a temporary bridge, not as the standard for hundreds of screens.

Recommended defaults:

  • detail/read-only: layoutPolicy.source = "schema", intent = "detail", preset = "compactPresentation", persistence = "transient" and a response schema;
  • create/edit: layoutPolicy.source = "schema", intent = "command", preset = "groupedCommand", persistence = "transient" and a request schema;
  • groupedCommand expands a single orphan md/lg field to the full row by default. Set layoutPolicy.groupedCommand.orphanFieldExpansion = "preserve" for intentionally compact rows, or "all" to expand orphan xs/sm fields too;
  • layoutPolicy.groupedCommand.partialRowStrategy = "fill-compatible" projects every eligible visual row, including rows created by responsive visibility, CSS order and wrapping, to the canonical 12-column grid per container breakpoint, while the default "preserve" retains intentional whitespace;
  • generated groupedCommand sections are untitled by default, avoiding a generic Informações heading beneath an already contextual action title. Explicit semantic group titles published by the schema remain authoritative;
  • dense command drawers: helpPresentation.display = "auto" with popover preference for select, checkbox, toggle, date and numeric controls;
  • operational input-heavy forms: fieldIconPolicy = "presentation-only" unless the icon is part of the actual affordance.

layoutPolicy.persistence = "transient" also suppresses automatic UI-storage writes of derived schema metadata and its context index. Schema fields, hashes, context and verification state remain available in memory. Pending metadata work rechecks the policy before sending a write; this does not cancel requests already sent. Existing storage reads and explicit editor Apply/Save behavior are unchanged. configPersistenceStrategy = "input-first" alone controls precedence, not this no-automatic-write policy. The default persistent behavior is unchanged.

On narrow viewports, the standard form action footer stacks validation guidance above the command CTA and keeps the action full width. This is presentation behavior only; submit labels, visibility and confirmation still come from the host or discovered action contract.

Schema-driven command forms do not need to declare empty manual sections. Keep host config focused on behavior, messages, actions and field metadata that is genuinely local:

const commandConfig: FormConfig = {
  metadata: { source: 'schema' },
  behavior: {
    disableMountAnimation: true,
    focusFirstError: true,
    scrollToErrors: true,
  },
  messages: {
    submitSuccess: 'Registro salvo.',
    submitError: 'Não foi possível salvar o registro.',
  },
};

Pair this config with an explicit schema source, such as [schemaUrl], [submitUrl], [submitMethod] and, in detached hosts, [apiEndpointKey] or [apiUrlEntry]. A sectionless config is valid only when the schema/runtime source is the canonical owner of field grouping and controls.

Do not use Angular config to fix weak DTO semantics. Missing grouping, vague helpText, generic labels, raw coded values, wrong control types or absent option metadata should return to backend DTO/schema hardening. The detailed migration checklist lives in docs/schema-driven-layout-materialization-rfc.md.

Runtime Inputs And Outputs

Common inputs:

  • config, formId, componentInstanceId
  • mode: create | edit | view
  • resourcePath, resourceId, initialValue, emptyState
  • schemaUrl, readUrl, submitUrl, submitMethod, responseSchemaUrl
  • apiEndpointKey, apiUrlEntry
  • actions, layout, backConfig, hooks
  • enableCustomization
  • readonlyModeGlobal, disabledModeGlobal, presentationModeGlobal, fieldIconPolicy, visibleGlobal
  • notifyIfOutdated, snoozeMs, autoOpenSettingsOnOutdated
  • domainRules
  • editorialContext

providePraxisDynamicFormMetadata() also registers localized editorial copy through the existing ComponentMetadataRegistry. Shared editors resolve labels and help with resolveEditorial('praxis-dynamic-form'), while raw input names, types and defaults remain authoritative. The form owns the pt-BR/en-US dictionaries. Hosts can override keys such as praxisDynamicForm.editorial.mode.label using the normal Praxis i18n provider/translator; consuming builders need no form-specific map. Advanced inputs without editorial overrides retain their raw metadata copy.

Surface action editors in custom actions, command rules and section headers propagate invalid JSON drafts into the owner's dirty/valid state. Apply/Save stays blocked until all errors are corrected; tab navigation preserves raw text and Reset explicitly discards it. See consumer validation.

In the actions editor, omitted optional text (icons, shortcuts, tooltips and CSS classes) is displayed as an empty input. Opening a tab does not write empty strings into the configuration; existing host text and explicit user edits retain their values.

actions is also a registered Dynamic Page input. A host or surface materializer may use it for presentation affordances such as a contextual submit label and button visibility. It must not use that input to replace the schema, method, endpoint or authorization decision discovered from the backend.

In edit and view, resourceId identifies the remote entity to hydrate. In create, it is contextual only (for example, the employee targeted by an item-scoped command) and never starts an entity read; provide command defaults through initialValue. Component metadata publishes resourceId as a public input port so top-level and nested UiCompositionPlan links are validated by the same registry contract used at runtime.

When neither readUrl nor resourcePath is configured, initialValue is the authoritative local snapshot: replacing it replaces the whole form value and null clears stale fields. This is the Dynamic Page composition contract used by local master-detail/detail surfaces. With a remote read source, the backend entity remains canonical; a later initialValue change never resets hydrated fields or pending user edits. Change resourceId to load another remote entity.

During a remote entity read, the form preserves its layout and mounted controls, but hides the previous values from sight, keyboard interaction and assistive technology. A local status explains the update. Failed or invalid reads keep the body and submit blocked and offer a local retry for the same identity. Successful reads replace the record snapshot, clearing fields omitted by the new response; superseded reads cannot apply to the current selection. This is automatic runtime behavior, not an additional widget binding or editor setting. Creation commands and authoritative local initialValue snapshots retain their existing behavior. Hiding is immediate: an opacity transition must not reveal the previous identity under the new header. Explicit retry focuses a stable detail region; the response does not steal focus after the user navigates elsewhere. See the local feedback contract and evidence.

For local master-detail views, emptyState is the canonical opt-in placeholder while neither resourceId nor a non-empty initialValue identifies a record. The Dynamic Form replaces its field surface with the shared praxis-empty-state-card only in mode="view"; create and edit flows are never hidden. The host or UiCompositionPlan owns the business-specific copy, while the component owns the deterministic transition between "select a record" and the presentation-only form. Do not duplicate that state with a consumer-local card or keep an empty form visible behind it.

The placeholder follows its content height, without reserving a fraction of the viewport. Its existing --pdx-form-context-empty-min-height and --pdx-form-context-empty-padding tokens default to 0; the shared card still provides its internal spacing. Hosts can override those tokens to reserve more space. --pdx-form-context-empty-max-width continues to default to 32rem. Visual choices such as alignment, density and icon container remain in emptyState, including the native editor and JSON document.

<praxis-dynamic-form
  mode="view"
  [initialValue]="selectedRecord"
  [emptyState]="{
    icon: 'touch_app',
    title: 'Selecione um registro',
    description: 'Selecione uma linha na tabela para consultar os detalhes.',
    variant: 'transparent',
    alignment: 'center',
    density: 'compact',
    iconContainer: 'circle'
  }"
/>

Common outputs:

  • formSubmit, formCancel, formReset
  • formReady, valueChange
  • payloadPreviewChange: projects the current persistence payload through the same synchronous normalization and submit-policy pipeline used by submit, without running submit hooks or dispatching HTTP. changedFields contains dirty control paths and remains independent from payload membership.
  • configChange, configPatchChange
  • syncCompleted, schemaStatusChange
  • loadingStateChange, initializationError
  • customAction, actionConfirmation
  • fieldRenderError, ruleDiagnosticsChange

Public untrusted runtime profile

The component metadata publishes public-local-readonly for hosts such as the Dynamic Page Studio. It is intentionally narrower than the full Dynamic Form API: the document must use mode="view", global readonly, configPersistenceStrategy="input-first", local initialValue, and an explicitly allowlisted config shape. It declares no runtime effects.

The profile rejects unknown input/config/field keys, remote controls and option sources, endpoints, rich-content blocks, hooks, rules, commands, actions, customization, AI triggers, and persistence identifiers. This is an execution profile, not an authorization grant. Hosts still evaluate it and remain responsible for their own network policy.

Readonly and presentation modes also fail closed at runtime: onSubmit() does not call create/update and does not emit formSubmit, even when invoked by a host or native browser submission path.

Submit Contract

formSubmit exposes both persistence data and full UI state:

schemaUrl/the request schema is the only source of editable controls and the command payload. responseSchemaUrl is diagnostic response evidence; hosts must not use it to merge response-only properties into edit controls. Record identity belongs in an external read-only context component and never in formData.

During hydration, properties absent from request-schema fieldMetadata do not create Angular form controls, so they are absent from both rawFormData and the prepared PUT/PATCH payload. Nested collection identity remains governed by its own item-schema submit contract.

  • formData: filtered backend payload after submit policies are applied
  • rawFormData: complete form value bag, including UI-only values

Host-owned fields that should participate in UI state but not be persisted by default belong in FormConfig.fieldMetadata with source: 'local' or transient: true.

const config: FormConfig = {
  sections: [],
  fieldMetadata: [
    { name: 'fullName', label: 'Full name', controlType: 'input' },
    {
      name: 'approvalComment',
      label: 'Approval comment',
      controlType: 'textarea',
      source: 'local',
      submitPolicy: 'omit',
    },
    {
      name: 'manualOverrideReason',
      label: 'Manual override reason',
      controlType: 'textarea',
      source: 'local',
      submitPolicy: 'includeWhenDirty',
    },
  ],
};

Do not model backend DTO fields as local/transient fields. If a field belongs to the public backend schema, fix the canonical schema or metadata mapping.

Layout, Rules, And Rich Content

FormConfig owns sections, rows, columns, field placement, visual blocks, rules, hooks, and actions.

  • sections[].rows[].columns[].items[] is the ordered layout contract.
  • kind: "field" references fieldMetadata[].name.
  • kind: "richContent" hosts a RichContentDocument and does not enter fieldMetadata, formData, or HTTP payloads.
  • formRules govern visibility, required state, values, labels, styles, and other whitelisted runtime properties.
  • formRulesState is internal visual-builder state. LLMs and external tools should not write it directly.
  • formCommandRules are used for governed side effects such as global actions.

Presentation formatting of field values is resolved by @praxisui/dynamic-fields from field metadata such as valuePresentation.

Compact Presentation Preset

For a detail form inside a widget card, use sections[].appearance: "plain" to keep a single outer frame. Plain sections retain transparent backgrounds, no shadow and no internal padding in field, presentation, compact and editorial contexts. Card and step sections keep their own surfaces. Configure section spacing and presentation density separately; explicit authored styles still apply.

For dense enterprise read-only screens, DynamicFormLayoutService.generateFormConfigFromMetadata can materialize a compact presentation layout directly from governed field metadata:

const config = layout.generateFormConfigFromMetadata(fields, {
  layoutPreset: 'compactPresentation',
  presentationRoleMap: {
    Identificacao: 'core-attributes',
    Regras: 'technical-scope',
    Marcadores: 'behavior-flags',
  },
});

The preset keeps field semantics in FieldMetadata, creates compact rows from field width/data type, derives semantic section header icons from group roles, marks generated sections with className: "pdx-presentation-section ...", and annotates metadata with generatedLayoutPreset: "compactPresentation" and presentationDensity: "compact". Boolean fields are displayed through the canonical valuePresentation: { type: "boolean" } path, so DTO-normalized booleans and legacy display values such as S/N are formatted by @praxisui/dynamic-fields.

When a compact read-only screen needs operational signals, use formBlocksBefore with a short RichContentDocument strip such as metric and progress nodes. Keep those blocks visual-only: they must not duplicate DTO fields in fieldMetadata, enter formData, or push the first business field below the useful viewport. Larger guidance cards, timelines, and stat groups belong in a dedicated row, side context, or a separate rich-content demonstration tab rather than the compact detail header.

The runtime exposes host-themeable CSS variables for this mode:

  • --pdx-presentation-label-width
  • --pdx-presentation-label-size
  • --pdx-presentation-value-size
  • --pdx-presentation-row-gap
  • --pdx-presentation-section-gap
  • --pdx-presentation-flag-height
  • --pdx-presentation-flag-padding
  • --pdx-presentation-flag-radius
  • --pdx-presentation-flag-font-size

Use this preset for read/detail surfaces. Do not use it to replace backend DTO documentation, business validation, or editable command forms. In table/detail workbenches, keep the table selection and the detail initialValue synchronized in the host; the compact form only renders the selected DTO state.

LLM-Safe Authoring Subset

For AI-generated or tool-generated configuration, prefer canonical FormConfig JSON and keep edits reviewable:

  • Write rule suggestions to formRules[] with metadata: { origin: "llm", reviewStatus: "pending" }.
  • Do not write formRulesState; the editor may derive it after human review when the rule is visually representable.
  • Treat simple field comparisons, scalar literals, field-to-field comparisons, and and/or groups as the stable visual round-trip subset.
  • Treat advanced JsonLogic such as !, xor, implies, between, notIn, custom functions, context operands, and computed expressions as runtime-supported JSON-only unless a focused visual round-trip test exists.
  • Use only properties allowed by the target rule schema. Unsupported or sanitized properties appear in rule diagnostics and should be fixed before publication.
  • Keep side effects in formCommandRules with structured globalAction: { actionId, payload }; do not encode commands inside formRules[].effect.properties.

Visual Editor

Use PraxisDynamicFormConfigEditor directly or enable customization on the runtime.

import { PraxisDynamicFormConfigEditor } from '@praxisui/dynamic-form';

dialog.open(PraxisDynamicFormConfigEditor, {
  width: '1024px',
  data: {
    formConfig: config,
    formId: 'employee-edit',
    mode: 'edit',
  },
});

The editor preserves the same FormConfig contract used by the runtime. Field type names, descriptions, and icons come from the dynamic-fields editorial metadata chain, not from a local map inside Dynamic Form.

Runtime-only rules remain valid JSON when they pass diagnostics, but they may open in JSON review instead of the visual rule builder until their condition and effect shape are covered by the visual-safe subset.

AI Authoring

PRAXIS_DYNAMIC_FORM_AUTHORING_MANIFEST declares executable operations for governed edits to:

  • fields and local fields
  • sections, rows, columns, and layout placement
  • visual blocks
  • form actions
  • form rules and messages
  • schema-backed field edits

The manifest is meant to keep AI-generated patches compatible with the visual editor and the runtime FormConfig contract.

PRAXIS_FILTER_FORM_AUTHORING_MANIFEST is a fail-closed projection of that canonical source under the exact praxis-filter-form identity. It exposes only the field and structural-layout operations executed by Filter Form. Submit policy, visual-block, rule, and configured form-action operations remain owned by praxis-dynamic-form because the reduced filter runtime does not execute those semantics.

Rule operations in the manifest write formRules[], require pending review metadata, and intentionally avoid formRulesState. A valid JsonLogic condition is not automatically a visual round-trip guarantee; LLMs should use the safe subset above unless the operation explicitly targets a JSON-only/runtime-only path.

Public API

Main exports:

  • PraxisDynamicForm
  • PraxisFilterForm
  • PraxisDynamicFormConfigEditor
  • JsonConfigEditorComponent
  • LayoutEditorComponent
  • PraxisFormActionsComponent
  • FormConfigService, FormLayoutService, DynamicFormLayoutService, FormContextService, FormRulesService, DomainRuleFormRulesService
  • widget config editors for dynamic form and filter form
  • metadata providers such as providePraxisDynamicFormMetadata
  • rule utilities, date normalization, settings-panel providers
  • FORM_AI_CAPABILITIES, FORM_COMPONENT_AI_CAPABILITIES
  • PRAXIS_DYNAMIC_FORM_AUTHORING_MANIFEST
  • PRAXIS_FILTER_FORM_AUTHORING_MANIFEST

Notes

  • Separate local-only, backend schema-driven, and authored/customizable usage before deciding the minimum host setup.
  • Keep backend schema fields server-backed; use local/transient metadata only for UI-owned state.
  • Prefer explicit schemaUrl + submitUrl + submitMethod for backend-published surfaces and actions.
  • Use the official documentation for full recipes on cascades, schema reconciliation, domain rules, action layout, and visual editor workflows.

Modo de consulta no Page Builder

Ao conectar um formulário a um recurso e alterar somente mode para view, o editor preserva a ausência de configuração autorada. Assim, os campos continuam derivados do schema após salvar e reabrir a página. Uma configuração de formulário efetivamente editada continua sendo persistida.

configPatchChange publica um inputPatch parcial com config, resourcePath, mode e/ou presentationModeGlobal. Consumidores devem aplicar somente as propriedades presentes: a conexão rápida publica a rota sem fixar uma configuração de campos ainda vazia.

Configurar a exibição inicial no Page Builder

Abra Configurar conteúdo → Geral → Dados do formulário. Para um detalhe de leitura, selecione Modo de dados: view e Modo de exibição: Apresentação. Salve o conteúdo e a página. Campos de formulário preserva controles de leitura; Automático (seguir schema) remove o override e respeita layoutPolicy. A preferência usa bindings.presentationModeGlobal no documento e o input homônimo no widget. Não é necessário criar uma ação rápida para iniciar em apresentação.

O botão criado em Configurar widget → Ações rápidas (inputs) continua sendo uma troca durante a interação. Para tipografia, densidade e alinhamento da apresentação em execução, use Configurar formulário → Modo Apresentação; esse editor persiste contextSnapshot.presentation separadamente da ativação.

Presentation style projection and editor UX

  • contextSnapshot.presentation is the canonical style snapshot. The optional runtime input presentation: DynamicFormPresentationSnapshot | null projects it into page widgets independently of schema-generated config. Explicit styles take precedence over stored style preferences; null restores defaults and blocks stale stored overrides; omission retains normal style resolution.
  • Runtime editor Apply/Save emits presentation with mode and presentationModeGlobal in configPatchChange.inputPatch. Widget save/reopen preserves the same snapshot; omitting the document block clears the widget projection with null.
  • Numeric font sizes and label widths are pixels. Capturing a runtime rem size converts it using the host root font size; opening and saving must not reinterpret 0.78rem as 0.78px.
  • Geral → Dados do formulário separates Criar / Editar / Visualizar from Automático / Campos / Apresentação. Selecting presentation in Create/Edit keeps the operation unchanged and explains the inactive state with an explicit action to switch to View.
  • Estilo da apresentação remains discoverable regardless of the current mode. It previews sample data and does not activate presentation by itself. Filter Form does not expose this tab because it has no presentation runtime.
  • Applying or finishing widget editing updates the page draft. Salvar página persists the composition. Shortening sample text affects the preview only.

Limpeza de seleção em detalhes controlados

Em formulários view/edit ligados a uma identidade remota, mudar resourceId de um registro para null limpa o snapshot anterior e cancela a leitura pendente, inclusive quando ela usa readUrl. Respostas de seleções substituídas não podem hidratar o registro atual. Um rebuild de configuração após essa limpeza não restaura o cache nem a semente initialValue do registro anterior; uma nova identidade inicia sua própria hidratação. Isso não altera snapshots locais authorados por initialValue, nem os defaults de comandos em modo create.

Recoverable schema loading

Dynamic Form consumes Core's SCHEMA_METADATA_CLIENT_OPTIONS transport policy for schema deadlines and cancellation. Timeouts use the existing error/retry surface; superseded results cannot finish a newer initialization. There is no additional widget timeout input. See schema loading recovery for provider configuration, scope, and validation limits.

Orientação antes da seleção de um registro

O editor nativo, em Geral → Sem registro selecionado, materializa o contrato existente PraxisDynamicFormEmptyState em bindings.emptyState. O wrapper Page Builder projeta esse binding para inputs.emptyState; Apply, Save e JSON usam o mesmo documento. Não pertence a FormConfig nem substitui o modo dos dados.

title é obrigatório e não pode conter apenas espaços. description e icon são textos opcionais; a cópia de negócio permanece literal, sem ser interpretada automaticamente como chave i18n. O seletor usa o catálogo canônico de ícones. Restaurar o ícone padrão limpa a escolha explícita. Os controles do editor usam o catálogo i18n Form; a aparência runtime usa os tokens do empty state compartilhado.

As opções de aparência ficam em disclosure avançado: tone (neutral, primary, secondary), variant (card, inline, panel, transparent), alignment (start, center), density (compact, comfortable) e iconContainer (none, circle, soft). Ausência de opção herda o padrão do componente; JSON inválido produz diagnóstico.

A orientação aparece somente em mode=view, sem registro selecionado nem snapshot local. Criar e Editar preservam a configuração sem exibi-la. Desligar grava null, que impede reativação por uma preferência antiga; substituir um documento que omite o binding também limpa a orientação. Em input-first, a página governa o valor. Filter Form não expõe essa seção, pois não possui contexto de seleção equivalente.

Após Aplicar, o dono da página confirma o commit local pelo canal transitório de sessão appliedValues$. O wrapper/nativo usa acceptAppliedValue para comparar o draft com a última prévia aceita; retornar aos valores de abertura volta a habilitar Aplicar quando a prévia diverge. Redefinir continua restaurando a abertura do Form. A confirmação local não comprova persistência HTTP.

O wrapper isola SETTINGS_PANEL_DATA do editor embutido: o envelope do host e seus observables nunca são clonados como configuração. O Core envia o canal de confirmação apenas a editores que declaram o input appliedValues$; o editor nativo consome esse canal somente junto ao documento canônico.

Leitura do editor de layout

Espaço após as seções define o valor proposto para gapBottom, em pixels. Alterar o número não modifica o formulário. Preencher espaços não definidos leva o valor ao rascunho somente nas seções sem espaçamento explícito; valores existentes, inclusive zero, são preservados. O comando não cria uma marca paralela de personalização. Aplicar/Salvar continuam confirmando o rascunho; o controle não altera o espaço entre colunas.

Densidade do editor (Compacta, Confortável e Espaçosa) e Guias do editor (Bordas das seções e Separadores) ajustam somente a visualização de trabalho. Não alteram o documento nem habilitam Aplicar/Salvar. Os controles mantêm nomes acessíveis, estados selecionado/pressionado e dicas explicativas no catálogo praxisDynamicFormLayoutEditor, com português, inglês e substituição pelo tradutor do host. Os valores internos e os tokens visuais existentes permanecem os mesmos.

Renomear seção, no cabeçalho, edita somente o título. Salvar nome ou Enter confirma; Cancelar ou Escape descarta a edição do nome e devolve o foco ao botão. Nomes vazios são recusados. Aparência, descrição, ícone e comportamento continuam no editor completo da seção. Uma seção sem linhas orienta a sequência linha → colunas → campos e oferece Adicionar primeira linha, usando o comando existente. Os textos usam o catálogo i18n da lib e a aparência herda tokens do host. Remover uma seção preserva os metadados dos campos: os campos elegíveis sem outro posicionamento voltam ao catálogo; blocos visuais pertencentes à seção são removidos com sua estrutura. Essas alterações continuam sujeitas a Aplicar/Salvar do editor e Salvar página.

O botão de ações de cada linha mostra seu número e possui nome acessível com linha e seção. O menu mantém os presets, Adicionar coluna e Remover linha existentes, utilizando o comportamento de teclado e foco do menu Material.

Os controles de espaçamento e faixa de tela ficam em Ajustar layout da linha, recolhidos inicialmente; abrir esse grupo não altera o documento. O menu da linha sempre atua na linha, independentemente da seleção anterior. Presets mostram miniaturas proporcionais e nomes traduzidos. A distribuição de larguras afeta somente a faixa exibida (por padrão MD); a estrutura de colunas é compartilhada pelas faixas. Ao reduzir colunas, seus itens são anexados em ordem à última coluna mantida, preservando campos e blocos visuais. As larguras das demais faixas nas colunas mantidas permanecem inalteradas.

Selecionar um campo mostra sua localização usando o título da seção e o rótulo dos metadados. As ações Editar campo e Layout da coluna abrem os editores canônicos existentes; selecionar ou abrir um editor não modifica o documento. Enter e Espaço selecionam o campo focado. As ações são suprimidas quando o alvo não existe mais na estrutura canônica.

Para posicionar um campo disponível sem arrastar, abra Opções da coluna → Adicionar campo da API, navegue com as setas e confirme com Enter. O campo sai do catálogo de disponíveis e recebe foco na coluna; Tab continua para editar seus metadados. Isso posiciona um campo já definido, sem criar ou duplicar metadados. Campos já posicionados não podem ser reinseridos por esse comando. A coluna vazia e o catálogo explicam esse caminho usando o catálogo i18n da lib.

Em colunas mistas, esse comando acrescenta o campo ao final de items[], inclusive depois de blocos visuais. Os itens anteriores conservam identidade e conteúdo. Para escolher outra posição, use Mover campo; a posição considera também os blocos visuais. fields[] permanece uma projeção apenas dos campos, sem incluir conteúdo editorial.

Nas opções de movimentação, blocos visuais mostram a posição original na coluna e um resumo do conteúdo, por exemplo bloco visual (posição 2): Revise as informações…. O número conta todos os itens, inclusive o campo sendo movido; não é um identificador persistido. Textos iguais e blocos vazios são diferenciados pela posição. O resumo reutiliza a leitura textual do editor, normaliza espaços e limita a prévia a 80 caracteres; não avalia bindings nem renderiza HTML. Os valores das opções continuam sendo IDs canônicos.

Na prévia de um bloco visual, Enter abre a edição do conteúdo e Espaço seleciona o bloco. Botões internos conservam sua própria ativação pelo teclado: abrir o menu ou remover o bloco não deve disparar o editor de conteúdo. Ao inserir ou mover um bloco, o foco acompanha a prévia selecionada; ao removê-lo, retorna à coluna preservada. O editor não recupera esse foco se a seleção mudar antes da renderização.

Mover campo abre a escolha de seção, linha, coluna e posição sem depender do arraste. Linhas e colunas mostram um resumo dos primeiros três itens, com a quantidade restante quando necessário. Os rótulos vêm de fieldMetadata, com nome técnico como fallback; blocos visuais e destinos vazios são identificados. Os resumos descrevem a estrutura authorada, incluindo campos ocultos, e não simulam permissões ou visibilidade runtime. A posição considera todos os itens da coluna, incluindo blocos visuais. O serviço de layout é isolado por instância do editor, para que operações do rascunho não sejam transmitidas aos formulários em uso. Abrir ou cancelar não altera o documento; confirmar utiliza DynamicFormLayoutService.moveLayoutItemById, conserva a identidade do item, seus metadados e os contêineres vazios, e seleciona/foca o campo no destino. A aparência passa a ser a da coluna de destino. Se o documento for substituído enquanto o diálogo estiver aberto, a movimentação é recusada com orientação para escolher novamente. Aplicar, Salvar e fechar e Salvar página mantêm suas responsabilidades existentes.

Na aba Estilo da coluna, o alinhamento é vertical na linha (align-self), não alinhamento de texto. O grupo visual Topo/Centro/Base/Preencher mantém os valores canônicos start/center/end/stretch. Espaço interno apresenta o padding numérico existente em pixels; o editor mantém sua faixa de 0 a 32. Classe CSS do host e identificador de teste ficam em Integração com o aplicativo, recolhidos inicialmente. Expandir esse grupo não limpa seus valores nem modifica o documento; os controles usam i18n da lib e tokens Material/Praxis existentes.

O painel de coluna identifica seu alvo por seção, linha e número da coluna no título. Os campos aparecem com os rótulos de fieldMetadata, seguindo a ordem canônica de items; nomes técnicos são fallback quando não existe rótulo. Esse contexto é transitório e não entra no JSON salvo. As faixas XS–XL mantêm seus códigos e ganham nomes traduzidos e explicação da herança. Largura e recuo ficam juntos; Ordem visual da coluna é recolhida inicialmente e aberta quando há erro de ordem. Abrir grupos ou trocar a faixa de prévia não modifica o formulário.

O editor da coluna apresenta uma prévia de 12 partes, derivada da largura e do recuo resolvidos para a faixa selecionada. Ela representa a coluna isolada, não a distribuição completa da linha. Modelos para várias faixas de tela separa os presets responsivos dos ajustes de largura/recuo: o grupo informa quais modelos também afetam visibilidade e estilo. Usar valores atuais em todas as faixas copia largura, recuo, ordem e visibilidade; não restaura valores iniciais. Valores sem definição própria continuam seguindo a herança responsiva canônica.

Largura aceita inteiros de 1 a 12, recuo de 0 a 11 e ordem de -12 a 12. Limpar um valor mantém a herança responsiva; não equivale a zero. Valores inválidos permanecem visíveis para correção, com erro traduzido por propriedade/faixa, e bloqueiam Aplicar/Salvar, a prévia e os resumos numéricos. A edição JSON valida esses mesmos domínios novamente no momento de aplicar. Ocultar em mobile e Somente desktop alteram apenas visibilidade: preservam largura, recuo, ordem e estilo, inclusive em colunas personalizadas.

Os rótulos Largura, Recuo e Faixa de tela apresentam os contratos existentes span, offset e breakpoint; não mudam o documento salvo. Colunas também podem ser reorganizadas pelo menu Mover para esquerda/direita. O resumo de espaçamento distingue valor herdado de zero explícito, exibindo px somente para números.

O indicador “Oculto nos metadados” aparece quando FieldMetadata.hidden ou FieldMetadata.formHidden é explicitamente true. Ele é informativo, usa a tradução do editor e os tokens de texto do tema. Não avalia hiddenCondition, permissões ou visibilidade da coluna, nem altera esses contratos. A ausência desse indicador não é garantia de visibilidade no runtime.

Row spacing guidance in the layout editor

The row inspector treats unused grid space as an intentional authoring choice, not a requirement to fill all 12 units. Guidance includes offsets and respects inherited visibility at the selected preview breakpoint. Columns containing only fields with static hidden: true or formHidden: true do not contribute; rich content, mixed visible content, unknown field references and empty draft columns remain available for planning.

The optional width suggestion uses the same occupancy calculation. Inspecting a row or changing its preview breakpoint does not change its document. This is static authoring guidance, not a simulation of record-dependent rules, schema hydration or permission decisions. Numeric domain validation and Apply/Save validation remain separate and unchanged.

A adaptação de authoring considera a largura do contêiner, inclusive quando o painel é estreito em uma janela desktop. Table e Dynamic Form exibem “Seção do editor” até 768px de largura disponível, mantendo a mesma seleção e os conteúdos das abas. Acima desse limite, reaparece a navegação por abas. O Settings Panel organiza título, expandir e fechar em uma grade até 599px; a prévia, quando suportada, ocupa a linha seguinte. Nenhum documento ou protocolo de Save é alterado.