Build Form: $ARGUMENTS

SkillDev tools

Build a reactive form with @fundamental-ngx/platform form components, FormGroup wiring, validation, and error states

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Build Form: $ARGUMENTS skill

What this skill tells your AI

The instructions your AI receives, as published by sap/fundamental-ngx in .claude/skills/build-form/SKILL.md and read by ahel’s review.

If $ARGUMENTS is empty, ask the user: (1) what the form is for, (2) which fields it needs, (3) whether validation is required.

Phase 1: Determine Scope

Parse from $ARGUMENTS or ask:

  • Form name (PascalCase, e.g., UserProfile, ShippingAddress)
  • Fields: name, type (text | number | select | date | checkbox | textarea), required (default) or optional (suffix with ?, e.g., birthDate:date?)
  • Layout: 1 column | 2 columns (default) | 3 columns
  • Submit target: inline handler, emitted event to parent, or none

Phase 2: Gather Component Context

Call the @fundamental-ngx/mcp MCP server:

  1. get_usage_guide('form') — composition order, fdp-form-group vs fd-form-item decision tree, pitfalls
  2. get_component_api('fdp-form-group') — layout inputs, (onSubmit) output
  3. get_component_api('fdp-form-field')[id], [label], [required], [columns], hint/error slots
  4. For each non-text field: get_component_api for its control (e.g., fdp-select, fdp-date-picker, fdp-checkbox)

If MCP is unavailable, read libs/mcp-server/src/data/usage-guides.ts.

Phase 3: Present Plan

Output this table before writing any code:

## Form Plan: [FormName]

**Layout:** 2 columns  |  **Validation trigger:** on submit + on touched

| Field | Control | Required | Validators |
|-------|---------|----------|------------|
| firstName | fdp-input | yes | minLength(2) |
| role | fdp-select | yes | required |
| birthDate | fdp-date-picker | no | — |

Stop here and wait for approval before generating code.

Phase 4: Generate Component

Create three files in the target path (ask the user if not already known).

Also generate the fields interface at the top of the .component.ts file:

export interface [Name]Fields {
    // one property per form field, e.g.:
    firstName: string;
    role: string;
}

TypeScript (.component.ts)

Import each platform form component directly — they are all standalone. There is no barrel PlatformFormModule to import.

import { ChangeDetectionStrategy, Component, inject, output } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from '@fundamental-ngx/core/button';
import {
    FormFieldComponent,
    FormFieldErrorDirective,
    FormGroupComponent,
    InputComponent
    // Add per-field: SelectComponent, TextAreaComponent, etc.
} from '@fundamental-ngx/platform/form';
// Import OptionItem only when using fdp-select:
// import { OptionItem } from '@fundamental-ngx/platform/shared';

@Component({
    selector: 'app-[kebab-name]-form',
    templateUrl: './[kebab-name]-form.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
    imports: [
        ReactiveFormsModule,
        FormGroupComponent,
        FormFieldComponent,
        FormFieldErrorDirective,  // always required when any field has validators
        InputComponent,
        // SelectComponent, TextAreaComponent, etc.
        ButtonModule
    ]
})
export class [Name]FormComponent {
    readonly submitted = output<[Name]Fields>();

    private readonly _fb = inject(FormBuilder);

    form: FormGroup = this._fb.group({
        // fieldName: [defaultValue, [Validators.required, ...]]
    });

    onSubmit(): void {
        if (this.form.valid) {
            this.submitted.emit(this.form.getRawValue() as [Name]Fields);
        }
    }
}

Standalone component imports reference:

Template elementImport from @fundamental-ngx/platform/form
fdp-form-groupFormGroupComponent
fdp-form-fieldFormFieldComponent
fdp-inputInputComponent
fdp-selectSelectComponent
fdp-textareaTextAreaComponent
fdp-date-pickerPlatformDatePickerComponent
fdp-checkboxCheckboxComponent
fdpFormFieldError (template directive)FormFieldErrorDirective

HTML Template (.component.html)

Wrap everything in a native <form>FormGroupComponent does NOT render a <form> tag by default (useForm defaults to false) and has no footer slot. Submit/reset buttons must be placed outside <fdp-form-group> but inside the <form>.

Use columnLayout="XL2-L2-M2-S1" for a 2-column layout (not [layout]).

<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <fdp-form-group [formGroup]="form" columnLayout="XL2-L2-M2-S1">
        <!--
          Error message templates — shared across ALL fields in this group.
          Required whenever any fdp-form-field has [required]="true" or its
          FormControl carries validators. Omitting these causes a runtime error:
          "Validation strings are required for the any provided validations."
          Add one <ng-template> per Angular error key your validators produce.
        -->
        <ng-template fdpFormFieldError="required">This field is required</ng-template>
        <ng-template fdpFormFieldError="minlength" let-error
            >Minimum {{ error.requiredLength }} characters required</ng-template
        >
        <ng-template fdpFormFieldError="email">Please enter a valid email address</ng-template>
        <!-- add more for maxlength, pattern, custom validators, etc. -->

        <fdp-form-field id="firstName" label="First Name" [required]="true" [column]="1">
            <fdp-input name="firstName" [formControlName]="'firstName'"></fdp-input>
        </fdp-form-field>

        <!-- repeat fdp-form-field blocks for each field -->
    </fdp-form-group>

    <!-- Buttons go here, outside fdp-form-group but inside <form> -->
    <div style="display: flex; gap: 0.5rem; margin-top: 1rem">
        <button fd-button fdType="emphasized" type="submit">Submit</button>
        <button fd-button fdType="transparent" type="button" (click)="form.reset()">Reset</button>
    </div>
</form>

fdp-select usage — use [list] with OptionItem[] from @fundamental-ngx/platform/shared. Each item's .value is what gets written to the FormControl:

<fdp-form-field id="role" label="Role" [required]="true" [column]="2">
    <fdp-select name="role" [formControlName]="'role'" [list]="roleOptions"> </fdp-select>
</fdp-form-field>
import { OptionItem } from '@fundamental-ngx/platform/shared';

readonly roleOptions: OptionItem[] = [
    { label: 'Developer', value: 'developer' },
    { label: 'Designer', value: 'designer' },
];

SCSS (.component.scss)

Leave empty — form spacing and layout are handled by fundamental-styles. Only add custom rules if product requirements demand it.

Critical Rules

  • fdpFormFieldError templates are mandatory when any field has validators — if [required]="true" is set on any fdp-form-field, or its FormControl carries validators, the component throws at runtime: "Validation strings are required for the any provided validations." Fix: add <ng-template fdpFormFieldError="required">...</ng-template> (and one per additional error key) as direct children of fdp-form-group. They are shared automatically by all fields. Import FormFieldErrorDirective from @fundamental-ngx/platform/form.
  • One <ng-template fdpFormFieldError> per Angular error key — add templates for every key your validators can produce: required, minlength, maxlength, email, pattern, and any custom keys. The template context (let-error) exposes the error value (e.g. error.requiredLength for minlength).
  • Import standalone components directly — there is no PlatformFormModule. Import each component individually: FormGroupComponent, FormFieldComponent, FormFieldErrorDirective, InputComponent, SelectComponent, TextAreaComponent, etc.
  • fdp-form-field not fdp-form-itemfdp-form-item is the core fd- API; platform forms use fdp-form-field
  • Wrap in <form (ngSubmit)>, NOT (fdSubmit) on fdp-form-groupfdp-form-group does not emit fdSubmit. Use a standard <form [formGroup]="form" (ngSubmit)="onSubmit()"> wrapper. The component's own (onSubmit) output fires only when [useForm]="true" is set.
  • columnLayout (string), NOT [layout] (object)FormGroupComponent accepts columnLayout="XL2-L2-M2-S1", not [layout]="{ columns: 2 }". The format is XLn-Ln-Mn-Sn.
  • No [fdpFormFooter] directive — it does not exist. Place buttons outside <fdp-form-group> but inside your <form> wrapper.
  • [list] (not [options]) on fdp-select — pass OptionItem[] to [list]. Do NOT use [options], displayKey, or valueKey — these inputs do not exist on SelectComponent.
  • [id] on fdp-form-field is required — it sets the for on the label and the id on the inner control via content projection; omitting it breaks label–control association and accessibility
  • name attribute on every control — must be unique within the form; matches the FormControlName
  • [column] (singular) not [columns]fdp-form-field uses [column] to set which column it occupies in the grid; [columns] is the span count
  • Do NOT add standalone: true — default since Angular 19
  • Do NOT use ngClass or ngStyle — use direct class/style bindings

Phase 5: Validate

nx run <project>:build   # or: ng build

Fix any TypeScript type errors or missing import errors before reporting done.

Output

## Build Form: [FormName]

**Files generated:**
- src/app/.../[kebab-name]-form.component.ts
- src/app/.../[kebab-name]-form.component.html
- src/app/.../[kebab-name]-form.component.scss

**Imports required in parent:**
- `import { [Name]FormComponent } from './[kebab-name]-form/[kebab-name]-form.component'`

**Next steps:**
- [ ] Add [Name]FormComponent to the parent's imports array
- [ ] Handle the (submitted) output event in the parent
- [ ] Customize per-field error messages with fdp-form-message if default messages are insufficient
- [ ] For multi-step forms, wrap multiple fdp-form-group instances in a wizard container

Signals

GitHub stars
294
Forks
147
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
build-form
Source
github.com/sap/fundamental-ngx