angular-form

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Angular Form Development Workflow

Angular 表单开发工作流

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
  • 用户输入表单(创建、编辑、设置)
  • 复杂验证需求
  • 异步验证(唯一性检查)
  • 动态表单字段(FormArray)
  • 依赖字段验证

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"
  • 确定表单模式(创建、更新、查看)
  • 阅读目标应用的设计系统文档(见下文)
  • 列出所有验证规则(同步和异步)
  • 识别字段依赖关系
  • 搜索类似表单:
    grep "{Feature}FormComponent" --include="*.ts"

🎨 Design System Documentation (MANDATORY)

🎨 设计系统文档(必填)

Before creating any form, read the design system documentation for your target application:
ApplicationDesign 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
在创建任何表单之前,请阅读目标应用的设计系统文档:
应用程序设计系统位置
WebV2 Apps
docs/design-system/
TextSnippetClient
src/PlatformExampleAppWeb/apps/playground-text-snippet/docs/design-system/
需要重点阅读的文档:
  • README.md
    - 组件概述、基类、库摘要
  • 03-form-patterns.md
    - 表单验证、模式、错误处理模式
  • 02-component-catalog.md
    - 可用表单组件及使用示例
  • 01-design-tokens.md
    - 颜色、排版、间距标记

File Location

文件位置

src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
    └── {feature}/
        ├── {feature}-form.component.ts
        ├── {feature}-form.component.html
        └── {feature}-form.component.scss
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 ClassUse When
PlatformFormComponent
Basic form without auth
AppBaseFormComponent
Form with auth context
基类名称使用场景
PlatformFormComponent
无权限验证的基础表单
AppBaseFormComponent
带权限上下文的表单

Pattern 1: Basic Form

模式1:基础表单

typescript
// {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();
    }
}
typescript
// {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

模式2:带异步验证的表单

typescript
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;
        };
    }
}
typescript
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

模式3:带依赖验证的表单

typescript
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;
        };
    }
}
typescript
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

模式4:带FormArray的表单

typescript
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);
    }
}
typescript
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

基础表单模板

html
<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>
html
<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

FormArray模板

html
<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>
html
<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

内置验证器

ValidatorImportUsage
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
验证器名称导入路径用途
noWhitespaceValidator
@libs/platform-core
不允许空字符串
startEndValidator
@libs/platform-core
日期/数值范围验证
ifAsyncValidator
@libs/platform-core
条件异步验证
validator
@libs/platform-core
自定义验证器工厂

Key Form APIs

核心表单API

MethodPurposeExample
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;
方法名称用途示例
validateForm()
验证并标记字段为已触碰
if (!this.validateForm()) return;
formControls(key)
获取表单控件
this.formControls('name').errors
currentVm()
获取当前视图模型
const vm = this.currentVm()
updateVm()
更新视图模型
this.updateVm({ name: 'new' })
mode
表单模式
this.mode === 'create'
isViewMode()
检查是否为查看模式
if (this.isViewMode()) return;

Component HTML Template Standard (BEM Classes)

组件HTML模板标准(BEM命名规范)

All UI elements in form templates MUST have BEM classes, even without styling needs. This makes forms self-documenting.
html
<!-- ✅ 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>
表单模板中的所有UI元素必须使用BEM类名,即使没有样式需求。这会让表单具备自文档性。
html
<!-- ✅ 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
html
<!-- 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()
typescript
// 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
typescript
// 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
html
<!-- 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>
}
:x: 表单元素缺少BEM类名
html
<!-- 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: 未使用validateForm()
typescript
// 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: 异步验证器始终运行
typescript
// WRONG - runs even if sync validators fail
new FormControl('', [], [asyncValidator]);

// CORRECT - conditional
new FormControl('', [], [ifAsyncValidator(ctrl => ctrl.valid, asyncValidator)]);
:x: 数组中缺少表单组名称
html
<!-- 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
  • initialFormConfig
    返回表单配置
  • initOrReloadVm
    在编辑模式下加载数据
  • 提交前调用了
    validateForm()
  • 异步验证器使用
    ifAsyncValidator
    实现条件执行
  • 为跨字段验证配置了
    dependentValidations
  • FormArray使用
    modelItems
    itemControl
  • 所有验证规则都显示了错误提示
  • 异步操作期间显示加载状态