angular-component

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Angular Component Development Workflow

Angular组件开发工作流

Pre-Flight Checklist

前置检查清单

  • Identify correct app:
    playground-text-snippet
    ,
    employee
    , etc.
  • Read the design system docs for the target application (see below)
  • Search for similar components:
    grep "{FeatureName}Component" --include="*.ts"
  • Determine component type (list, form, detail, dialog)
  • Check if store is needed (complex state)
  • 确定所属应用:
    playground-text-snippet
    employee
  • 阅读目标应用的设计系统文档(见下文)
  • 检索是否存在相似组件:
    grep "{FeatureName}Component" --include="*.ts"
  • 确定组件类型(列表、表单、详情、弹窗)
  • 判断是否需要Store(复杂状态场景)

🎨 Design System Documentation (MANDATORY)

🎨 设计系统文档(强制要求)

Before creating any component, 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
  • 02-component-catalog.md
    - Available components and usage examples
  • 01-design-tokens.md
    - Colors, typography, spacing tokens
  • 07-technical-guide.md
    - Implementation checklist
创建任何组件前,请先阅读目标应用的设计系统文档:
应用设计系统地址
WebV2应用
docs/design-system/
TextSnippetClient
src/PlatformExampleAppWeb/apps/playground-text-snippet/docs/design-system/
必读核心文档:
  • README.md
    - 组件概述、基类、库摘要
  • 02-component-catalog.md
    - 可用组件及用法示例
  • 01-design-tokens.md
    - 颜色、排版、间距token
  • 07-technical-guide.md
    - 实现检查清单

Component Hierarchy

组件层级

PlatformComponent                    # Base: lifecycle, subscriptions, signals
├── PlatformVmComponent             # + ViewModel injection
├── PlatformFormComponent           # + Reactive forms integration
└── PlatformVmStoreComponent        # + ComponentStore state management

AppBaseComponent                     # + Auth, roles, company context
├── AppBaseVmComponent              # + ViewModel + auth context
├── AppBaseFormComponent            # + Forms + auth + validation
└── AppBaseVmStoreComponent         # + Store + auth + loading/error
PlatformComponent                    # 基类:生命周期、订阅、signals
├── PlatformVmComponent             # + ViewModel注入
├── PlatformFormComponent           # + Reactive forms集成
└── PlatformVmStoreComponent        # + ComponentStore状态管理

AppBaseComponent                     # + 权限、角色、企业上下文
├── AppBaseVmComponent              # + ViewModel + 权限上下文
├── AppBaseFormComponent            # + 表单 + 权限 + 校验
└── AppBaseVmStoreComponent         # + Store + 权限 + 加载/错误状态

Component Type Decision

组件类型选择

ScenarioBase ClassUse When
Simple display
AppBaseComponent
Static content, no state
With ViewModel
AppBaseVmComponent
Needs mutable view model
Form with validation
AppBaseFormComponent
User input forms
Complex state/CRUD
AppBaseVmStoreComponent
Lists, dashboards, multi-step
场景基类适用场景
简单展示
AppBaseComponent
静态内容、无状态
带ViewModel
AppBaseVmComponent
需要可变视图模型
带校验的表单
AppBaseFormComponent
用户输入表单
复杂状态/CRUD
AppBaseVmStoreComponent
列表、仪表盘、多步骤流程

File Location

文件位置

src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
    └── {feature}/
        ├── {feature}.component.ts
        ├── {feature}.component.html
        ├── {feature}.component.scss
        └── {feature}.store.ts (if using store)
src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
    └── {feature}/
        ├── {feature}.component.ts
        ├── {feature}.component.html
        ├── {feature}.component.scss
        └── {feature}.store.ts (使用Store时新增)

Component HTML Template Standard (BEM Classes)

组件HTML模板规范(BEM类名)

All UI elements in component templates MUST have BEM classes, even without styling needs. This makes HTML self-documenting like OOP class hierarchy.
html
<!-- ✅ CORRECT: All elements have BEM classes for structure clarity -->
<div class="feature-list">
    <div class="feature-list__header">
        <h1 class="feature-list__title">Features</h1>
        <button class="feature-list__btn --add" (click)="onAdd()">Add New</button>
    </div>
    <div class="feature-list__content">
        @for (item of vm.items; track trackByItem) {
        <div class="feature-list__item">
            <span class="feature-list__item-name">{{ item.name }}</span>
            <div class="feature-list__item-actions">
                <button class="feature-list__item-btn" (click)="onDelete(item)">Delete</button>
            </div>
        </div>
        } @empty {
        <div class="feature-list__empty">No items found</div>
        }
    </div>
</div>

<!-- ❌ WRONG: Elements without classes - structure unclear -->
<div class="feature-list">
    <div>
        <h1>Features</h1>
        <button (click)="onAdd()">Add New</button>
    </div>
    <div>
        @for (item of vm.items; track trackByItem) {
        <div>
            <span>{{ item.name }}</span>
            <div>
                <button (click)="onDelete(item)">Delete</button>
            </div>
        </div>
        }
    </div>
</div>
BEM Naming Convention:
  • Block: Component name (e.g.,
    feature-list
    )
  • Element: Child using
    block__element
    (e.g.,
    feature-list__header
    )
  • Modifier: Separate class with
    --
    prefix (e.g.,
    feature-list__btn --add --large
    )
组件模板中所有UI元素必须添加BEM类名,即使没有样式需求也需要添加。 这可以让HTML像OOP类层级一样具备自说明性。
html
<!-- ✅ 正确:所有元素都有BEM类名,结构清晰 -->
<div class="feature-list">
    <div class="feature-list__header">
        <h1 class="feature-list__title">Features</h1>
        <button class="feature-list__btn --add" (click)="onAdd()">Add New</button>
    </div>
    <div class="feature-list__content">
        @for (item of vm.items; track trackByItem) {
        <div class="feature-list__item">
            <span class="feature-list__item-name">{{ item.name }}</span>
            <div class="feature-list__item-actions">
                <button class="feature-list__item-btn" (click)="onDelete(item)">Delete</button>
            </div>
        </div>
        } @empty {
        <div class="feature-list__empty">No items found</div>
        }
    </div>
</div>

<!-- ❌ 错误:元素没有类名,结构不清晰 -->
<div class="feature-list">
    <div>
        <h1>Features</h1>
        <button (click)="onAdd()">Add New</button>
    </div>
    <div>
        @for (item of vm.items; track trackByItem) {
        <div>
            <span>{{ item.name }}</span>
            <div>
                <button (click)="onDelete(item)">Delete</button>
            </div>
        </div>
        }
    </div>
</div>
BEM命名约定:
  • Block:组件名称(例如:
    feature-list
  • Element:子元素使用
    block__element
    格式(例如:
    feature-list__header
  • Modifier:修饰类使用
    --
    前缀(例如:
    feature-list__btn --add --large

Component SCSS Standard

组件SCSS规范

Always style both the host element (Angular selector) and the main wrapper class:
scss
@import '~assets/scss/variables';

// Host element styling - ensures Angular element is a proper block container
my-component {
    display: flex;
    flex-direction: column;
}

// Main wrapper class with full styling
.my-component {
    display: flex;
    flex-direction: column;
    width: 100%;
    flex-grow: 1;

    &__header {
        // BEM child elements...
    }

    &__content {
        flex: 1;
        overflow-y: auto;
    }
}
Why both?
  • Host element: Makes the Angular element a real layout element (not an unknown element without display)
  • Main class: Contains the full styling, matches the wrapper div in HTML
必须同时为宿主元素(Angular选择器)和主容器类添加样式:
scss
@import '~assets/scss/variables';

// 宿主元素样式 - 确保Angular元素是合法的块级容器
my-component {
    display: flex;
    flex-direction: column;
}

// 主容器类,包含完整样式
.my-component {
    display: flex;
    flex-direction: column;
    width: 100%;
    flex-grow: 1;

    &__header {
        // BEM子元素样式...
    }

    &__content {
        flex: 1;
        overflow-y: auto;
    }
}
为什么需要同时设置两者?
  • 宿主元素:让Angular元素成为真实的布局元素(而非无display属性的未知元素)
  • 主容器类:包含完整样式,与HTML中的容器div匹配

Pattern 1: List Component with Store

模式1:带Store的列表组件

Store Definition

Store定义

typescript
// {feature}.store.ts
import { Injectable } from '@angular/core';
import { PlatformVmStore } from '@libs/platform-core';

export interface FeatureListState {
    items: FeatureDto[];
    selectedItem?: FeatureDto;
    filters: FeatureFilters;
}

@Injectable()
export class FeatureListStore extends PlatformVmStore<FeatureListState> {
    // Initial state
    protected override vmConstructor = (data?: Partial<FeatureListState>) => ({ items: [], filters: {}, ...data }) as FeatureListState;

    // Selectors
    public readonly items$ = this.select(state => state.items);
    public readonly selectedItem$ = this.select(state => state.selectedItem);

    // Effects
    public loadItems = this.effectSimple(() =>
        this.featureApi.getList(this.currentVm().filters).pipe(
            this.observerLoadingErrorState('loadItems'),
            this.tapResponse(items => this.updateState({ items }))
        )
    );

    public saveItem = this.effectSimple((item: FeatureDto) =>
        this.featureApi.save(item).pipe(
            this.observerLoadingErrorState('saveItem'),
            this.tapResponse(saved => {
                this.updateState(state => ({
                    items: state.items.upsertBy(x => x.id, [saved])
                }));
            })
        )
    );

    public deleteItem = this.effectSimple((id: string) =>
        this.featureApi.delete(id).pipe(
            this.observerLoadingErrorState('deleteItem'),
            this.tapResponse(() => {
                this.updateState(state => ({
                    items: state.items.filter(x => x.id !== id)
                }));
            })
        )
    );

    constructor(private featureApi: FeatureApiService) {
        super();
    }
}
typescript
// {feature}.store.ts
import { Injectable } from '@angular/core';
import { PlatformVmStore } from '@libs/platform-core';

export interface FeatureListState {
    items: FeatureDto[];
    selectedItem?: FeatureDto;
    filters: FeatureFilters;
}

@Injectable()
export class FeatureListStore extends PlatformVmStore<FeatureListState> {
    // Initial state
    protected override vmConstructor = (data?: Partial<FeatureListState>) => ({ items: [], filters: {}, ...data }) as FeatureListState;

    // Selectors
    public readonly items$ = this.select(state => state.items);
    public readonly selectedItem$ = this.select(state => state.selectedItem);

    // Effects
    public loadItems = this.effectSimple(() =>
        this.featureApi.getList(this.currentVm().filters).pipe(
            this.observerLoadingErrorState('loadItems'),
            this.tapResponse(items => this.updateState({ items }))
        )
    );

    public saveItem = this.effectSimple((item: FeatureDto) =>
        this.featureApi.save(item).pipe(
            this.observerLoadingErrorState('saveItem'),
            this.tapResponse(saved => {
                this.updateState(state => ({
                    items: state.items.upsertBy(x => x.id, [saved])
                }));
            })
        )
    );

    public deleteItem = this.effectSimple((id: string) =>
        this.featureApi.delete(id).pipe(
            this.observerLoadingErrorState('deleteItem'),
            this.tapResponse(() => {
                this.updateState(state => ({
                    items: state.items.filter(x => x.id !== id)
                }));
            })
        )
    );

    constructor(private featureApi: FeatureApiService) {
        super();
    }
}

List Component

列表组件

typescript
// {feature}-list.component.ts
import { Component, OnInit } from '@angular/core';
import { AppBaseVmStoreComponent } from '@libs/apps-domains';
import { FeatureListStore, FeatureListState } from './feature-list.store';

@Component({
    selector: 'app-feature-list',
    templateUrl: './feature-list.component.html',
    styleUrls: ['./feature-list.component.scss'],
    providers: [FeatureListStore] // Provide store at component level
})
export class FeatureListComponent extends AppBaseVmStoreComponent<FeatureListState, FeatureListStore> implements OnInit {
    // Track-by for performance
    trackByItem = this.ngForTrackByItemProp<FeatureDto>('id');

    constructor(store: FeatureListStore) {
        super(store);
    }

    ngOnInit(): void {
        this.store.loadItems();
    }

    onRefresh(): void {
        this.reload(); // Reloads all store data
    }

    onDelete(item: FeatureDto): void {
        this.store.deleteItem(item.id);
    }

    // Check loading state for specific request
    get isDeleting$() {
        return this.store.isLoading$('deleteItem');
    }
}
typescript
// {feature}-list.component.ts
import { Component, OnInit } from '@angular/core';
import { AppBaseVmStoreComponent } from '@libs/apps-domains';
import { FeatureListStore, FeatureListState } from './feature-list.store';

@Component({
    selector: 'app-feature-list',
    templateUrl: './feature-list.component.html',
    styleUrls: ['./feature-list.component.scss'],
    providers: [FeatureListStore] // 在组件层级提供Store
})
export class FeatureListComponent extends AppBaseVmStoreComponent<FeatureListState, FeatureListStore> implements OnInit {
    // Track-by优化性能
    trackByItem = this.ngForTrackByItemProp<FeatureDto>('id');

    constructor(store: FeatureListStore) {
        super(store);
    }

    ngOnInit(): void {
        this.store.loadItems();
    }

    onRefresh(): void {
        this.reload(); // 重载所有Store数据
    }

    onDelete(item: FeatureDto): void {
        this.store.deleteItem(item.id);
    }

    // 获取特定请求的加载状态
    get isDeleting$() {
        return this.store.isLoading$('deleteItem');
    }
}

List Template

列表模板

html
<!-- {feature}-list.component.html -->
<app-loading-and-error-indicator [target]="this">
    @if (vm(); as vm) {
    <div class="feature-list">
        <!-- Header with actions -->
        <div class="header">
            <h1>Features</h1>
            <button (click)="onRefresh()" [disabled]="isStateLoading()()">Refresh</button>
        </div>

        <!-- List items -->
        @for (item of vm.items; track trackByItem) {
        <div class="item">
            <span>{{ item.name }}</span>
            <button (click)="onDelete(item)" [disabled]="isDeleting$() === true">Delete</button>
        </div>
        } @empty {
        <div class="empty">No items found</div>
        }
    </div>
    }
</app-loading-and-error-indicator>
html
<!-- {feature}-list.component.html -->
<app-loading-and-error-indicator [target]="this">
    @if (vm(); as vm) {
    <div class="feature-list">
        <!-- 带操作的头部 -->
        <div class="header">
            <h1>Features</h1>
            <button (click)="onRefresh()" [disabled]="isStateLoading()()">Refresh</button>
        </div>

        <!-- 列表项 -->
        @for (item of vm.items; track trackByItem) {
        <div class="item">
            <span>{{ item.name }}</span>
            <button (click)="onDelete(item)" [disabled]="isDeleting$() === true">Delete</button>
        </div>
        } @empty {
        <div class="empty">No items found</div>
        }
    </div>
    }
</app-loading-and-error-indicator>

Pattern 2: Form Component

模式2:表单组件

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

export interface FeatureFormVm {
    id?: string;
    name: string;
    code: string;
    status: FeatureStatus;
    effectiveDate?: Date;
}

@Component({
    selector: 'app-feature-form',
    templateUrl: './feature-form.component.html'
})
export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
    // 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-]+$/)],
                [
                    // Async validator only runs if sync validators pass
                    ifAsyncValidator(() => !this.isViewMode(), this.checkCodeUniqueValidator())
                ]
            ),
            status: new FormControl(this.currentVm().status, [Validators.required]),
            effectiveDate: new FormControl(this.currentVm().effectiveDate)
        },
        // Re-validate code when status changes
        dependentValidations: {
            code: ['status']
        }
    });

    // Initialize or reload view model
    protected initOrReloadVm = (isReload: boolean) => {
        if (this.mode === 'create') {
            return of<FeatureFormVm>({
                name: '',
                code: '',
                status: FeatureStatus.Draft
            });
        }
        return this.featureApi.getById(this.featureId);
    };

    // Custom async validator
    private checkCodeUniqueValidator() {
        return async (control: AbstractControl) => {
            const exists = await firstValueFrom(this.featureApi.checkCodeExists(control.value, this.currentVm().id));
            return exists ? { codeExists: true } : null;
        };
    }

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

        const vm = this.currentVm();
        this.featureApi
            .save(vm)
            .pipe(
                this.observerLoadingErrorState('save'),
                this.tapResponse(
                    saved => this.onSaveSuccess(saved),
                    error => this.onSaveError(error)
                ),
                this.untilDestroyed()
            )
            .subscribe();
    }

    constructor(private featureApi: FeatureApiService) {
        super();
    }
}
typescript
// {feature}-form.component.ts
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { AppBaseFormComponent } from '@libs/apps-domains';
import { ifAsyncValidator, noWhitespaceValidator } from '@libs/platform-core';

export interface FeatureFormVm {
    id?: string;
    name: string;
    code: string;
    status: FeatureStatus;
    effectiveDate?: Date;
}

@Component({
    selector: 'app-feature-form',
    templateUrl: './feature-form.component.html'
})
export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
    // 表单配置
    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-]+$/)],
                [
                    // 仅同步校验通过后才运行异步校验
                    ifAsyncValidator(() => !this.isViewMode(), this.checkCodeUniqueValidator())
                ]
            ),
            status: new FormControl(this.currentVm().status, [Validators.required]),
            effectiveDate: new FormControl(this.currentVm().effectiveDate)
        },
        // 状态变更时重新校验code
        dependentValidations: {
            code: ['status']
        }
    });

    // 初始化或重载ViewModel
    protected initOrReloadVm = (isReload: boolean) => {
        if (this.mode === 'create') {
            return of<FeatureFormVm>({
                name: '',
                code: '',
                status: FeatureStatus.Draft
            });
        }
        return this.featureApi.getById(this.featureId);
    };

    // 自定义异步校验器
    private checkCodeUniqueValidator() {
        return async (control: AbstractControl) => {
            const exists = await firstValueFrom(this.featureApi.checkCodeExists(control.value, this.currentVm().id));
            return exists ? { codeExists: true } : null;
        };
    }

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

        const vm = this.currentVm();
        this.featureApi
            .save(vm)
            .pipe(
                this.observerLoadingErrorState('save'),
                this.tapResponse(
                    saved => this.onSaveSuccess(saved),
                    error => this.onSaveError(error)
                ),
                this.untilDestroyed()
            )
            .subscribe();
    }

    constructor(private featureApi: FeatureApiService) {
        super();
    }
}

Form Template

表单模板

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

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

    <!-- Status dropdown -->
    <div class="form-field">
        <label for="status">Status *</label>
        <select id="status" formControlName="status">
            @for (status of statusOptions; track status.value) {
            <option [value]="status.value">{{ status.label }}</option>
            }
        </select>
    </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
<!-- {feature}-form.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <!-- 名称字段 -->
    <div class="form-field">
        <label for="name">Name *</label>
        <input id="name" formControlName="name" />
        @if (formControls('name').errors?.['required']) {
        <span class="error">Name is required</span>
        }
    </div>

    <!-- 带异步校验的编码字段 -->
    <div class="form-field">
        <label for="code">Code *</label>
        <input id="code" formControlName="code" />
        @if (formControls('code').errors?.['codeExists']) {
        <span class="error">Code already exists</span>
        } @if (formControls('code').pending) {
        <span class="info">Checking...</span>
        }
    </div>

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

    <!-- 操作按钮 -->
    <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>

Pattern 3: Simple Component

模式3:简单组件

typescript
// {feature}-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { AppBaseComponent } from '@libs/apps-domains';

@Component({
    selector: 'app-feature-card',
    template: `
        <div class="card" [class.selected]="isSelected">
            <h3>{{ feature.name }}</h3>
            <p>{{ feature.description }}</p>
            @if (canEdit) {
                <button (click)="onEdit.emit(feature)">Edit</button>
            }
        </div>
    `
})
export class FeatureCardComponent extends AppBaseComponent {
    @Input() feature!: FeatureDto;
    @Input() isSelected = false;
    @Output() onEdit = new EventEmitter<FeatureDto>();

    get canEdit(): boolean {
        return this.hasRole('Admin', 'Manager');
    }
}
typescript
// {feature}-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { AppBaseComponent } from '@libs/apps-domains';

@Component({
    selector: 'app-feature-card',
    template: `
        <div class="card" [class.selected]="isSelected">
            <h3>{{ feature.name }}</h3>
            <p>{{ feature.description }}</p>
            @if (canEdit) {
                <button (click)="onEdit.emit(feature)">Edit</button>
            }
        </div>
    `
})
export class FeatureCardComponent extends AppBaseComponent {
    @Input() feature!: FeatureDto;
    @Input() isSelected = false;
    @Output() onEdit = new EventEmitter<FeatureDto>();

    get canEdit(): boolean {
        return this.hasRole('Admin', 'Manager');
    }
}

Key Platform APIs

核心平台API

Lifecycle & Subscriptions

生命周期与订阅

typescript
// Auto-cleanup subscription
this.data$.pipe(this.untilDestroyed()).subscribe();

// Store named subscriptions
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');
typescript
// 自动清理订阅
this.data$.pipe(this.untilDestroyed()).subscribe();

// Store命名订阅
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');

Loading/Error State

加载/错误状态

typescript
// Track request state
observable.pipe(this.observerLoadingErrorState('requestKey'));

// Check states in template
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();
typescript
// 追踪请求状态
observable.pipe(this.observerLoadingErrorState('requestKey'));

// 在模板中检查状态
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();

Response Handling

响应处理

typescript
// Handle success/error
observable.pipe(
    this.tapResponse(
        result => {
            /* success */
        },
        error => {
            /* error */
        }
    )
);
typescript
// 处理成功/错误
observable.pipe(
    this.tapResponse(
        result => {
            /* success */
        },
        error => {
            /* error */
        }
    )
);

Track-by Functions

Track-by函数

typescript
// For @for loops
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);
typescript
// 用于@for循环
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);

Code Responsibility Hierarchy (CRITICAL)

代码职责分层(关键要求)

Place logic in the LOWEST appropriate layer to enable reuse and prevent duplication:
LayerResponsibility
Entity/ModelDisplay helpers, static factory methods, default values, dropdown options
ServiceAPI calls, command factories, data transformation
ComponentUI event handling ONLY - delegates all logic to lower layers
typescript
// ❌ WRONG: Logic in component (leads to duplication if another component needs it)
readonly authTypes = [{ value: AuthType.OAuth2, label: 'OAuth2' }, ...];
getDefaultBaseUrl(type) { return this.providerUrls[type] ?? ''; }

// ✅ CORRECT: Logic in entity/model (single source of truth, reusable)
readonly authTypes = AuthConfigurationDisplay.getApiAuthTypeOptions();
getDefaultBaseUrl(type) { return JobBoardProviderConfiguration.getDefaultBaseUrl(type); }
Common Refactoring Patterns:
  • Dropdown options → static method in entity:
    Entity.getOptions()
  • Display logic (CSS class, text) → instance method in entity:
    entity.getStatusCssClass()
  • Default values → static method in entity:
    Entity.getDefaultValue()
  • Command building → factory class in service:
    CommandFactory.buildSaveCommand(formValues)
将逻辑放在最低层级的合适位置,以实现复用并避免重复:
层级职责
实体/模型展示辅助方法、静态工厂方法、默认值、下拉选项
ServiceAPI调用、命令工厂、数据转换
组件仅处理UI事件 - 所有逻辑委托给下层实现
typescript
// ❌ 错误:逻辑放在组件中(如果其他组件需要会导致重复)
readonly authTypes = [{ value: AuthType.OAuth2, label: 'OAuth2' }, ...];
getDefaultBaseUrl(type) { return this.providerUrls[type] ?? ''; }

// ✅ 正确:逻辑放在实体/模型中(单一数据源,可复用)
readonly authTypes = AuthConfigurationDisplay.getApiAuthTypeOptions();
getDefaultBaseUrl(type) { return JobBoardProviderConfiguration.getDefaultBaseUrl(type); }
常见重构模式:
  • 下拉选项 → 实体中的静态方法:
    Entity.getOptions()
  • 展示逻辑(CSS类、文本) → 实体中的实例方法:
    entity.getStatusCssClass()
  • 默认值 → 实体中的静态方法:
    Entity.getDefaultValue()
  • 命令构建 → Service中的工厂类:
    CommandFactory.buildSaveCommand(formValues)

Anti-Patterns to AVOID

需要避免的反模式

:x: Using wrong base class
typescript
// WRONG - using PlatformComponent when auth needed
export class MyComponent extends PlatformComponent {}

// CORRECT - using AppBaseComponent for auth context
export class MyComponent extends AppBaseComponent {}
:x: Manual subscription management
typescript
// WRONG
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }

// CORRECT
this.data$.pipe(this.untilDestroyed()).subscribe();
:x: Direct HTTP calls
typescript
// WRONG
constructor(private http: HttpClient) { }

// CORRECT
constructor(private featureApi: FeatureApiService) { }
:x: Missing loading states
html
<!-- WRONG - no loading indicator -->
<div>{{ items }}</div>

<!-- CORRECT - with loading wrapper -->
<app-loading-and-error-indicator [target]="this">
    <div>{{ items }}</div>
</app-loading-and-error-indicator>
:x: 使用错误的基类
typescript
// 错误 - 需要权限时使用PlatformComponent
export class MyComponent extends PlatformComponent {}

// 正确 - 使用AppBaseComponent获取权限上下文
export class MyComponent extends AppBaseComponent {}
:x: 手动管理订阅
typescript
// 错误
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }

// 正确
this.data$.pipe(this.untilDestroyed()).subscribe();
:x: 直接调用HTTP接口
typescript
// 错误
constructor(private http: HttpClient) { }

// 正确
constructor(private featureApi: FeatureApiService) { }
:x: 缺失加载状态
html
<!-- 错误 - 无加载指示器 -->
<div>{{ items }}</div>

<!-- 正确 - 带加载包裹组件 -->
<app-loading-and-error-indicator [target]="this">
    <div>{{ items }}</div>
</app-loading-and-error-indicator>

Verification Checklist

验证清单

  • Correct base class selected for use case
  • Store provided at component level (if using store)
  • Loading/error states handled with
    app-loading-and-error-indicator
  • Subscriptions use
    untilDestroyed()
  • Track-by functions used in
    @for
    loops
  • Form validation configured properly
  • Auth checks use
    hasRole()
    from base class
  • API calls use service extending
    PlatformApiService
  • 为当前场景选择了正确的基类
  • Store在组件层级提供(使用Store时)
  • 使用
    app-loading-and-error-indicator
    处理加载/错误状态
  • 订阅使用
    untilDestroyed()
    自动清理
  • @for
    循环使用了track-by函数
  • 表单校验配置正确
  • 权限校验使用基类提供的
    hasRole()
    方法
  • API调用使用继承自
    PlatformApiService
    的服务