angular-form

📁 congdon1207/agents.md 📅 9 days ago
1
总安装量
1
周安装量
#52939
全站排名
安装命令
npx skills add https://github.com/congdon1207/agents.md --skill angular-form

Agent 安装分布

opencode 1
codex 1
claude-code 1

Skill 文档

Angular Form Development Workflow

When to Use This Skill

  • User input forms (create, edit, settings)
  • Complex validation requirements
  • Async validation (uniqueness checks)
  • Dynamic form fields (FormArrays)
  • Dependent field validation

Pre-Flight Checklist

  • Identify form mode (create, update, view)
  • Read the design system docs for the target application (see below)
  • List all validation rules (sync and async)
  • Identify field dependencies
  • Search similar forms: grep "{Feature}FormComponent" --include="*.ts"

🎨 Design System Documentation (MANDATORY)

Before creating any form, read the design system documentation for your target application:

Application Design System Location
WebV2 Apps docs/design-system/
TextSnippetClient src/PlatformExampleAppWeb/apps/playground-text-snippet/docs/design-system/

Key docs to read:

  • README.md – Component overview, base classes, library summary
  • 03-form-patterns.md – Form validation, modes, error handling patterns
  • 02-component-catalog.md – Available form components and usage examples
  • 01-design-tokens.md – Colors, typography, spacing tokens

File Location

src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
    └── {feature}/
        ├── {feature}-form.component.ts
        ├── {feature}-form.component.html
        └── {feature}-form.component.scss

Form Base Class Selection

Base Class Use When
PlatformFormComponent Basic form without auth
AppBaseFormComponent Form with auth context

Pattern 1: Basic Form

// {feature}-form.component.ts
import { Component, Input } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { AppBaseFormComponent } from '@libs/apps-domains';
import { noWhitespaceValidator } from '@libs/platform-core';

// ═══════════════════════════════════════════════════════════════════════════
// VIEW MODEL
// ═══════════════════════════════════════════════════════════════════════════

export interface FeatureFormVm {
    id?: string;
    name: string;
    code: string;
    description?: string;
    status: FeatureStatus;
    isActive: boolean;
}

// ═══════════════════════════════════════════════════════════════════════════
// COMPONENT
// ═══════════════════════════════════════════════════════════════════════════

@Component({
    selector: 'app-feature-form',
    templateUrl: './feature-form.component.html'
})
export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
    @Input() featureId?: string;

    // ─────────────────────────────────────────────────────────────────────────
    // FORM CONFIGURATION
    // ─────────────────────────────────────────────────────────────────────────

    protected initialFormConfig = () => ({
        controls: {
            name: new FormControl(this.currentVm().name, [Validators.required, Validators.maxLength(200), noWhitespaceValidator]),
            code: new FormControl(this.currentVm().code, [Validators.required, Validators.pattern(/^[A-Z0-9_-]+$/), Validators.maxLength(50)]),
            description: new FormControl(this.currentVm().description, [Validators.maxLength(2000)]),
            status: new FormControl(this.currentVm().status, [Validators.required]),
            isActive: new FormControl(this.currentVm().isActive)
        }
    });

    // ─────────────────────────────────────────────────────────────────────────
    // INIT/RELOAD VIEW MODEL
    // ─────────────────────────────────────────────────────────────────────────

    protected initOrReloadVm = (isReload: boolean) => {
        if (!this.featureId) {
            // Create mode - return empty view model
            return of<FeatureFormVm>({
                name: '',
                code: '',
                status: FeatureStatus.Draft,
                isActive: true
            });
        }

        // Edit mode - load from API
        return this.featureApi.getById(this.featureId);
    };

    // ─────────────────────────────────────────────────────────────────────────
    // ACTIONS
    // ─────────────────────────────────────────────────────────────────────────

    onSubmit(): void {
        if (!this.validateForm()) return;

        const vm = this.currentVm();

        this.featureApi
            .save(vm)
            .pipe(
                this.observerLoadingErrorState('save'),
                this.tapResponse(
                    saved => this.onSuccess(saved),
                    error => this.onError(error)
                ),
                this.untilDestroyed()
            )
            .subscribe();
    }

    onCancel(): void {
        this.router.navigate(['/features']);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CONSTRUCTOR
    // ─────────────────────────────────────────────────────────────────────────

    constructor(
        private featureApi: FeatureApiService,
        private router: Router
    ) {
        super();
    }
}

Pattern 2: Form with Async Validation

export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
    protected initialFormConfig = () => ({
        controls: {
            code: new FormControl(
                this.currentVm().code,
                // Sync validators
                [Validators.required, Validators.pattern(/^[A-Z0-9_-]+$/)],
                // Async validators (only run if sync pass)
                [
                    ifAsyncValidator(
                        ctrl => ctrl.valid, // Condition to run
                        this.checkCodeUniqueValidator()
                    )
                ]
            ),
            email: new FormControl(
                this.currentVm().email,
                [Validators.required, Validators.email],
                [
                    ifAsyncValidator(
                        () => !this.isViewMode(), // Skip in view mode
                        this.checkEmailUniqueValidator()
                    )
                ]
            )
        }
    });

    // ─────────────────────────────────────────────────────────────────────────
    // ASYNC VALIDATORS
    // ─────────────────────────────────────────────────────────────────────────

    private checkCodeUniqueValidator(): AsyncValidatorFn {
        return async (control: AbstractControl): Promise<ValidationErrors | null> => {
            if (!control.value) return null;

            const exists = await firstValueFrom(
                this.featureApi.checkCodeExists(control.value, this.currentVm().id).pipe(debounceTime(300)) // Debounce API calls
            );

            return exists ? { codeExists: 'Code already exists' } : null;
        };
    }

    private checkEmailUniqueValidator(): AsyncValidatorFn {
        return async (control: AbstractControl): Promise<ValidationErrors | null> => {
            if (!control.value) return null;

            const exists = await firstValueFrom(this.employeeApi.checkEmailExists(control.value, this.currentVm().id));

            return exists ? { emailExists: 'Email already in use' } : null;
        };
    }
}

Pattern 3: Form with Dependent Validation

export class DateRangeFormComponent extends AppBaseFormComponent<DateRangeVm> {
    protected initialFormConfig = () => ({
        controls: {
            startDate: new FormControl(this.currentVm().startDate, [Validators.required]),
            endDate: new FormControl(this.currentVm().endDate, [
                Validators.required,
                // Cross-field validation
                startEndValidator(
                    'invalidRange',
                    ctrl => ctrl.parent?.get('startDate')?.value,
                    ctrl => ctrl.value,
                    { allowEqual: true }
                )
            ]),
            category: new FormControl(this.currentVm().category, [Validators.required]),
            subcategory: new FormControl(this.currentVm().subcategory, [Validators.required])
        },
        // Re-validate these fields when dependencies change
        dependentValidations: {
            endDate: ['startDate'], // Re-validate endDate when startDate changes
            subcategory: ['category'] // Re-validate subcategory when category changes
        }
    });

    // Custom cross-field validator
    private dateRangeValidator(): ValidatorFn {
        return (control: AbstractControl): ValidationErrors | null => {
            const form = control.parent;
            if (!form) return null;

            const start = form.get('startDate')?.value;
            const end = control.value;

            if (start && end && new Date(end) < new Date(start)) {
                return { invalidRange: 'End date must be after start date' };
            }

            return null;
        };
    }
}

Pattern 4: Form with FormArray

export interface ProductFormVm {
    name: string;
    price: number;
    specifications: Specification[];
    tags: string[];
}

export interface Specification {
    name: string;
    value: string;
}

export class ProductFormComponent extends AppBaseFormComponent<ProductFormVm> {
    protected initialFormConfig = () => ({
        controls: {
            name: new FormControl(this.currentVm().name, [Validators.required]),
            price: new FormControl(this.currentVm().price, [Validators.required, Validators.min(0)]),

            // FormArray configuration
            specifications: {
                // Model items to create controls from
                modelItems: () => this.currentVm().specifications,

                // How to create control for each item
                itemControl: (spec: Specification, index: number) =>
                    new FormGroup({
                        name: new FormControl(spec.name, [Validators.required]),
                        value: new FormControl(spec.value, [Validators.required])
                    })
            },

            // Simple array of primitives
            tags: {
                modelItems: () => this.currentVm().tags,
                itemControl: (tag: string) => new FormControl(tag, [Validators.required])
            }
        }
    });

    // ─────────────────────────────────────────────────────────────────────────
    // FORM ARRAY HELPERS
    // ─────────────────────────────────────────────────────────────────────────

    get specificationsArray(): FormArray {
        return this.form.get('specifications') as FormArray;
    }

    addSpecification(): void {
        const newSpec: Specification = { name: '', value: '' };

        // Update view model
        this.updateVm(vm => ({
            specifications: [...vm.specifications, newSpec]
        }));

        // Add form control
        this.specificationsArray.push(
            new FormGroup({
                name: new FormControl('', [Validators.required]),
                value: new FormControl('', [Validators.required])
            })
        );
    }

    removeSpecification(index: number): void {
        // Update view model
        this.updateVm(vm => ({
            specifications: vm.specifications.filter((_, i) => i !== index)
        }));

        // Remove form control
        this.specificationsArray.removeAt(index);
    }
}

Template Patterns

Basic Form Template

<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <!-- Text input -->
    <div class="form-field">
        <label for="name">Name *</label>
        <input id="name" formControlName="name" />
        @if (formControls('name').errors?.['required'] && formControls('name').touched) {
        <span class="error">Name is required</span>
        } @if (formControls('name').errors?.['maxlength']) {
        <span class="error">Name is too long</span>
        }
    </div>

    <!-- Async validation feedback -->
    <div class="form-field">
        <label for="code">Code *</label>
        <input id="code" formControlName="code" />
        @if (formControls('code').pending) {
        <span class="info">Checking availability...</span>
        } @if (formControls('code').errors?.['codeExists']) {
        <span class="error">{{ formControls('code').errors?.['codeExists'] }}</span>
        }
    </div>

    <!-- Select dropdown -->
    <div class="form-field">
        <label for="status">Status *</label>
        <select id="status" formControlName="status">
            @for (option of statusOptions; track option.value) {
            <option [value]="option.value">{{ option.label }}</option>
            }
        </select>
    </div>

    <!-- Checkbox -->
    <div class="form-field">
        <label>
            <input type="checkbox" formControlName="isActive" />
            Active
        </label>
    </div>

    <!-- Actions -->
    <div class="actions">
        <button type="button" (click)="onCancel()">Cancel</button>
        <button type="submit" [disabled]="!form.valid || isLoading$('save')()">{{ isLoading$('save')() ? 'Saving...' : 'Save' }}</button>
    </div>
</form>

FormArray Template

<div formArrayName="specifications">
    @for (spec of specificationsArray.controls; track $index; let i = $index) {
    <div [formGroupName]="i" class="specification-row">
        <input formControlName="name" placeholder="Name" />
        <input formControlName="value" placeholder="Value" />
        <button type="button" (click)="removeSpecification(i)">Remove</button>
    </div>
    }
    <button type="button" (click)="addSpecification()">Add Specification</button>
</div>

Built-in Validators

Validator Import Usage
noWhitespaceValidator @libs/platform-core No empty strings
startEndValidator @libs/platform-core Date/number range
ifAsyncValidator @libs/platform-core Conditional async
validator @libs/platform-core Custom validator factory

Key Form APIs

Method Purpose Example
validateForm() Validate and mark touched if (!this.validateForm()) return;
formControls(key) Get form control this.formControls('name').errors
currentVm() Get current view model const vm = this.currentVm()
updateVm() Update view model this.updateVm({ name: 'new' })
mode Form mode this.mode === 'create'
isViewMode() Check view mode if (this.isViewMode()) return;

Component HTML Template Standard (BEM Classes)

All UI elements in form templates MUST have BEM classes, even without styling needs. This makes forms self-documenting.

<!-- ✅ CORRECT: All form elements have BEM classes -->
<form class="feature-form" [formGroup]="form" (ngSubmit)="onSubmit()">
    <div class="feature-form__section">
        <div class="feature-form__field">
            <label class="feature-form__label" for="name">Name *</label>
            <input class="feature-form__input" id="name" formControlName="name" />
            @if (formControls('name').errors?.['required'] && formControls('name').touched) {
            <span class="feature-form__error">Name is required</span>
            }
        </div>
        <div class="feature-form__field">
            <label class="feature-form__label" for="code">Code *</label>
            <input class="feature-form__input" id="code" formControlName="code" />
            @if (formControls('code').pending) {
            <span class="feature-form__info">Checking availability...</span>
            }
        </div>
    </div>
    <div class="feature-form__actions">
        <button class="feature-form__btn --cancel" type="button" (click)="onCancel()">Cancel</button>
        <button class="feature-form__btn --submit" type="submit">Save</button>
    </div>
</form>

<!-- ❌ WRONG: Missing BEM classes -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <div>
        <div>
            <label for="name">Name *</label>
            <input id="name" formControlName="name" />
        </div>
    </div>
    <div>
        <button type="button">Cancel</button>
        <button type="submit">Save</button>
    </div>
</form>

Anti-Patterns to AVOID

:x: Missing BEM classes on form elements

<!-- WRONG -->
<div><label>Name</label><input formControlName="name" /></div>

<!-- CORRECT -->
<div class="form__field">
    <label class="form__label">Name</label>
    <input class="form__input" formControlName="name" />
</div>

:x: Not using validateForm()

// WRONG - form may be invalid
onSubmit() {
  this.api.save(this.currentVm());
}

// CORRECT - validate first
onSubmit() {
  if (!this.validateForm()) return;
  this.api.save(this.currentVm());
}

:x: Async validator always runs

// WRONG - runs even if sync validators fail
new FormControl('', [], [asyncValidator]);

// CORRECT - conditional
new FormControl('', [], [ifAsyncValidator(ctrl => ctrl.valid, asyncValidator)]);

:x: Missing form group name in array

<!-- WRONG -->
@for (item of formArray.controls; track $index) {
<input formControlName="name" />
}

<!-- CORRECT -->
@for (item of formArray.controls; track $index; let i = $index) {
<div [formGroupName]="i">
    <input formControlName="name" />
</div>
}

Verification Checklist

  • initialFormConfig returns form configuration
  • initOrReloadVm loads data for edit mode
  • validateForm() called before submit
  • Async validators use ifAsyncValidator for conditional execution
  • dependentValidations configured for cross-field validation
  • FormArrays use modelItems and itemControl
  • Error messages displayed for all validation rules
  • Loading states shown during async operations