angular-form
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular 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:
| Application | Design System Location |
|---|---|
| WebV2 Apps | |
| TextSnippetClient | |
Key docs to read:
- - Component overview, base classes, library summary
README.md - - Form validation, modes, error handling patterns
03-form-patterns.md - - Available form components and usage examples
02-component-catalog.md - - Colors, typography, spacing tokens
01-design-tokens.md
在创建任何表单之前,请阅读目标应用的设计系统文档:
| 应用程序 | 设计系统位置 |
|---|---|
| WebV2 Apps | |
| TextSnippetClient | |
需要重点阅读的文档:
- - 组件概述、基类、库摘要
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.scsssrc/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
└── {feature}/
├── {feature}-form.component.ts
├── {feature}-form.component.html
└── {feature}-form.component.scssForm Base Class Selection
表单基类选择
| Base Class | Use When |
|---|---|
| Basic form without auth |
| Form with auth context |
| 基类名称 | 使用场景 |
|---|---|
| 无权限验证的基础表单 |
| 带权限上下文的表单 |
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
内置验证器
| Validator | Import | Usage |
|---|---|---|
| | No empty strings |
| | Date/number range |
| | Conditional async |
| | Custom validator factory |
| 验证器名称 | 导入路径 | 用途 |
|---|---|---|
| | 不允许空字符串 |
| | 日期/数值范围验证 |
| | 条件异步验证 |
| | 自定义验证器工厂 |
Key Form APIs
核心表单API
| Method | Purpose | Example |
|---|---|---|
| Validate and mark touched | |
| Get form control | |
| Get current view model | |
| Update view model | |
| Form mode | |
| Check view mode | |
| 方法名称 | 用途 | 示例 |
|---|---|---|
| 验证并标记字段为已触碰 | |
| 获取表单控件 | |
| 获取当前视图模型 | |
| 更新视图模型 | |
| 表单模式 | |
| 检查是否为查看模式 | |
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
验证清单
- returns form configuration
initialFormConfig - loads data for edit mode
initOrReloadVm - called before submit
validateForm() - Async validators use for conditional execution
ifAsyncValidator - configured for cross-field validation
dependentValidations - FormArrays use and
modelItemsitemControl - Error messages displayed for all validation rules
- Loading states shown during async operations
- 返回表单配置
initialFormConfig - 在编辑模式下加载数据
initOrReloadVm - 提交前调用了
validateForm() - 异步验证器使用实现条件执行
ifAsyncValidator - 为跨字段验证配置了
dependentValidations - FormArray使用和
modelItemsitemControl - 所有验证规则都显示了错误提示
- 异步操作期间显示加载状态