Form

The <smart-form> component renders a reactive form driven by @Field() model decorators. It is a wrapper that delegates to FormStandardComponent by default and can be replaced via FORM_STANDARD_COMPONENT_TOKEN.


Usage

<smart-form
  [options]="options"
  (valueChange)="value.set($event)"
  (invokeSubmit)="onSubmit()"
/>

Components

FormComponent (<smart-form>)

Main wrapper. Holds all reactive-form logic: builds the form via FormFactory, tracks loading$, and emits valueChange, valuePartialChange, and validChange outputs. Renders FormStandardComponent by default. When FORM_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet, passing all inputs and outputs.

FormStandardComponent (<smart-form-standard>)

Default concrete implementation. Generic Tailwind-styled container that iterates over fields and renders each via <smart-input> per control.

FormBaseComponent (abstract)

Abstract base directive. Exposes form (UntypedFormGroup), options, fields (computed from form.controls), model, mode, possibilities, inputComponents, cssClass, and lifecycle hooks submit(), afterSetForm(), afterSetOptions(). Extend it to build custom form implementations.

API

Inputs

InputTypeDefaultDescription
optionsInputSignal<IFormOptions<T>>requiredForm configuration (model, mode, control, loading$, …)
classInputSignal<string> (alias cssClass)''External CSS classes on the container

Outputs

OutputTypeDescription
invokeSubmitOutputEmitterRef<T>Emits the form value when the form is submitted
valueChangeOutputEmitterRef<T>Emits the full form value on every change
valuePartialChangeOutputEmitterRef<Partial<T>>Emits only the changed portion of the form value
validChangeOutputEmitterRef<boolean>Emits form validity on every change

FORM_STANDARD_COMPONENT_TOKEN

InjectionToken that allows replacing the default FormStandardComponent with a custom implementation. Provide a Type<FormBaseComponent<T>>.

Extending the base class

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  signal,
  ViewEncapsulation,
} from '@angular/core';

import {
  FORM_STANDARD_COMPONENT_TOKEN,
  FormBaseComponent,
  FormComponent,
  IFormOptions,
  InputComponent,
} from '@smartsoft001/angular';
import { Field, FieldType, Model } from '@smartsoft001/models';

@Model({ titleKey: 'name' })
export class DocsAccount {
  @Field({ type: FieldType.text, create: true, required: true })
  name = '';

  @Field({ type: FieldType.email, create: true })
  email = '';
}

@Component({
  selector: 'docs-custom-form',
  template: `
    <div [class]="containerClasses()">
      <p class="docs-form__hint">All fields marked with * are required.</p>

      @for (field of fields; track field) {
        <div class="docs-form__row">
          <smart-input
            [options]="{
              treeLevel: treeLevel ?? 0,
              fieldKey: field,
              control: getUntypedFormControl(field),
              model: model,
              mode: mode,
            }"
          />
        </div>
      }

      <!--
        NgComponentOutlet does not forward outputs, so invokeSubmit declared
        here never reaches the caller. It does not have to: smart-form already
        wraps this template in a <form>, so a plain submit button makes the
        wrapper emit its own (invokeSubmit).
      -->
      <button type="submit" class="docs-form__submit">Create account</button>
    </div>
  `,
  imports: [InputComponent],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomFormComponent extends FormBaseComponent<DocsAccount> {
  // FormComponent passes the external class under its aliased name, so the
  // inherited `cssClass` input is used as is - do not redeclare it here.
  containerClasses = computed(() => {
    const classes = ['docs-form'];
    const extra = this.cssClass();
    if (extra) classes.push(extra);
    return classes.join(' ');
  });
}

@Component({
  selector: 'docs-form-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FormComponent],
  // The token swaps the standard form shell for the custom one everywhere
  // below this component, so consumers keep writing `<smart-form>`.
  providers: [
    { provide: FORM_STANDARD_COMPONENT_TOKEN, useValue: CustomFormComponent },
  ],
  template: `
    <smart-form
      [options]="options"
      (valueChange)="value.set($event)"
      (invokeSubmit)="submitted.set(true)"
    />
  `,
})
export class FormCustomExampleComponent {
  value = signal<DocsAccount | null>(null);
  submitted = signal(false);

  // `<smart-form>` builds the FormGroup from the @Field metadata through
  // FormFactory; the custom shell only decides how the fields are laid out.
  options: IFormOptions<DocsAccount> = {
    model: new DocsAccount(),
    show: true,
    mode: 'create',
  };
}

Field Rendering

Each field is rendered via <smart-input>. Per-field dispatch by FieldType is handled inside <smart-input> itself. See the angular-components-input skill for per-field details.

Preset

FormPresetComponent (<smart-form-preset>) is a styled, drop-in replacement for FormStandardComponent. It extends the standard component and reuses all of its logic (field iteration, statuses, submit-on-enter, and the value/valid/partial outputs on the wrapper). The preset only restyles the shell: the form root gets a vertical rhythm (smart:space-y-5) and each field row is wrapped in a data-role="field" element (with the field key on data-key); the root carries data-role="form".

The form preset does NOT restyle field internals. Each field is still rendered by <smart-input>, so to get the full styled look, register the input presets alongside it. The recommended duet provides both tokens:

Providing only FORM_STANDARD_COMPONENT_TOKEN restyles the form layout but leaves the inputs in their default look. The preset keeps the inherited class alias, so <smart-form class="…"> still lands external classes on the form root.

Reference implementation

The example application under docs/examples/app never places <smart-form> by hand: the CRUD item page renders it from the @Field metadata, and the pieces below are what the form reads in any application.

  • docs/examples/app/libs/model/src/lib/note.model.ts: the @Field decorators the form is generated from, with create and update flags per mode, required repeated per mode and focused on the first input.
  • docs/examples/app/apps/web/src/app/app.config.ts: MODEL_VALIDATORS_PROVIDER registered even though the app adds no validators of its own, because the form factory injects it without a default and the form does not render without it.
  • docs/examples/app/apps/web/src/app/app.config.spec.ts: the test that the provider hands the base validators implied by the metadata back to the factory.
  • docs/examples/app/apps/web/src/app/translations.ts: the MODEL.<key> labels the rendered inputs show.

Source

The component lives in packages/shared/angular/src/lib/components/form and is documented for Claude Code by the angular-components-form skill.