Skip to content
angular-component logo

Angular Component Development Workflow

angular-component

Use when creating or modifying Angular components in WebV2 (Angular 19) with proper base class inheritance, state management, and platform patterns.

SKILL.md

Full skill instructions

Angular Component Development Workflow

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)

🎨 Design System Documentation (MANDATORY)

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

ApplicationDesign System Location
WebV2 Appsdocs/​design-system/
TextSnippetClientsrc/​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

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

Component Type Decision

ScenarioBase ClassUse When
Simple displayAppBaseComponentStatic content, no state
With ViewModelAppBaseVmComponentNeeds mutable view model
Form with validationAppBaseFormComponentUser input forms
Complex state/​CRUDAppBaseVmStoreComponentLists, dashboards, multi-step

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)

Component HTML Template Standard (BEM Classes)

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:

  • 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)

Component SCSS Standard

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?

  • 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

Pattern 1: List Component with Store

Store Definition

// {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

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

List Template

<!-- {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>

Pattern 2: Form Component

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

Form Template

<!-- {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>

Pattern 3: Simple Component

// {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

Lifecycle & Subscriptions

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

// Store named subscriptions
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');

Loading/​Error State

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

// Check states in template
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();

Response Handling

// Handle success/​error
observable.pipe(
    this.tapResponse(
        result => {
            /​* success */
        },
        error => {
            /​* error */
        }
    )
);

Track-by Functions

// For @for loops
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
// ❌ 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)

Anti-Patterns to AVOID

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

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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

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

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

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

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

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.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View