xiaohongshu
xiaohongshu
angular-component
Use when creating or modifying Angular components in WebV2 (Angular 19) with proper base class inheritance, state management, and platform patterns.
Full skill instructions
playground-text-snippet, employee, etc.grep "{FeatureName}Component" --include="*.ts"Before creating any component, 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 summary02-component-catalog.md - Available components and usage examples01-design-tokens.md - Colors, typography, spacing tokens07-technical-guide.md - Implementation checklistPlatformComponent # 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
| Scenario | Base Class | Use 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 |
src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
└── {feature}/
├── {feature}.component.ts
├── {feature}.component.html
├── {feature}.component.scss
└── {feature}.store.ts (if using store)
All UI elements in component templates MUST have BEM classes, even without styling needs. This makes HTML self-documenting like OOP class hierarchy.
<!-- ✅ 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:
feature-list)block__element (e.g., feature-list__header)-- prefix (e.g., feature-list__btn --add --large)Always style both the host element (Angular selector) and the main wrapper class:
@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?
// {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();
}
}
// {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');
}
}
<!-- {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>
// {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();
}
}
<!-- {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>
// {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');
}
}
// Auto-cleanup subscription
this.data$.pipe(this.untilDestroyed()).subscribe();
// Store named subscriptions
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');
// Track request state
observable.pipe(this.observerLoadingErrorState('requestKey'));
// Check states in template
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();
// Handle success/error
observable.pipe(
this.tapResponse(
result => {
/* success */
},
error => {
/* error */
}
)
);
// For @for loops
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);
Place logic in the LOWEST appropriate layer to enable reuse and prevent duplication:
| Layer | Responsibility |
|---|---|
| Entity/Model | Display helpers, static factory methods, default values, dropdown options |
| Service | API calls, command factories, data transformation |
| Component | UI event handling ONLY - delegates all logic to lower layers |
// ❌ 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:
Entity.getOptions()entity.getStatusCssClass()Entity.getDefaultValue()CommandFactory.buildSaveCommand(formValues):x: Using wrong base class
// 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
// WRONG
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }
// CORRECT
this.data$.pipe(this.untilDestroyed()).subscribe();
:x: Direct HTTP calls
// WRONG
constructor(private http: HttpClient) { }
// CORRECT
constructor(private featureApi: FeatureApiService) { }
:x: Missing loading states
<!-- 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>
app-loading-and-error-indicatoruntilDestroyed()@for loopshasRole() from base classPlatformApiServicexiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience