Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
duc01226 avatar

Frontend Angular

  • 10 installs
  • 7 repo stars
  • Updated June 18, 2026
  • duc01226/easyplatform

Creates or modifies Angular components, forms, stores, and API services with proper base-class and state patterns.

About

Builds and modifies Angular components, forms, stores, and API services using base-class inheritance, state management, and platform patterns. A developer uses it when working in an Angular codebase.

  • Follows base-class inheritance and state patterns
  • Covers components, forms, stores, and API services

Frontend Angular by the numbers

  • 10 all-time installs (skills.sh)
  • Ranked #1,691 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/duc01226/easyplatform --skill frontend-angular

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs10
repo stars7
Last updatedJune 18, 2026
Repositoryduc01226/easyplatform

What it does

Creates or modifies Angular components, forms, stores, and API services with proper base-class and state patterns.

Files

SKILL.mdMarkdownGitHub ↗

Prerequisites: MUST READ before executing:

  • .claude/skills/shared/understand-code-first-protocol.md
  • .claude/skills/shared/evidence-based-reasoning-protocol.md

Quick Summary

Goal: Create or modify Angular frontend code -- components, forms, stores, and API services -- following platform patterns.

Project Pattern Discovery

Before implementation, search your codebase for project-specific patterns:

  • Search for: BaseComponent, VmStore, FormComponent, ApiService, untilDestroyed
  • Look for: project base component classes, shared component libraries, state store conventions
MANDATORY IMPORTANT MUST Plan ToDo Task to READ frontend-patterns-reference.md for project-specific patterns and code examples.
If file not found, continue with search-based discovery above.

Workflow:

1. Pre-Flight -- Identify app, search similar code, determine type (component/form/store/service) 2. Select Pattern -- Choose from Component, Form, Store, or API Service patterns below 3. Implement -- Follow BEM template standard, SCSS host+wrapper pattern, platform base classes 4. Wire Up -- Route config, module imports, API service integration

Key Rules:

  • Never extend Platform directly; always use AppBase intermediaries
  • All HTML elements MUST have BEM classes (block__element --modifier)
  • Use .pipe(this.untilDestroyed()) for all subscriptions
  • Always extend PlatformApiService for HTTP calls, never use HttpClient directly
  • Always use PlatformVmStore for state management, never manual signals
  • MUST READ .ai/docs/frontend-code-patterns.md and design system docs before implementation

Prerequisites: MUST READ .claude/skills/shared/design-system-check.md for mandatory design system checks.

Pre-Flight Checklist

  • [ ] Identify correct app: growth-for-company, employee, etc.
  • [ ] Read the design system docs for the target application
  • [ ] Search for similar code: grep "{FeatureName}" --include="*.ts"
  • [ ] Determine what you need: component, form, store, API service, or combination
  • [ ] Read .ai/docs/frontend-code-patterns.md

File Locations

Search your project to find the frontend directory structure. Common pattern:

{frontend-apps-dir}/{app-name}/src/app/
  features/
    {feature}/
      {feature}.component.ts          # Component
      {feature}.component.html         # Template
      {feature}.component.scss         # Styles
      {feature}.store.ts               # Store (if complex state)
      {feature}-form.component.ts      # Form component

{frontend-libs-dir}/{domain-lib}/src/lib/
  {domain}/
    services/
      {feature}-api.service.ts         # API Service

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
// BAD: Logic in component (leads to duplication)
readonly authTypes = [{ value: AuthType.OAuth2, label: 'OAuth2' }, ...];

// GOOD: Logic in entity/model (single source of truth, reusable)
readonly authTypes = AuthConfigurationDisplay.getApiAuthTypeOptions();

---

Part 1: Components

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

Component HTML Template Standard (BEM Classes)

All UI elements MUST have BEM classes, even without styling needs. This makes HTML self-documenting.

<!-- GOOD: All elements have BEM classes -->
<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>
        } @empty {
        <div class="feature-list__empty">No items found</div>
        }
    </div>
</div>

BEM Naming: Block = component name, Element = block__element, Modifier = separate --modifier class.

Component SCSS Standard

Always style both the host element and the main wrapper class:

@import '~assets/scss/variables';

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

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

    &__header {
        /* ... */
    }
    &__content {
        flex: 1;
        overflow-y: auto;
    }
}

Pattern: List Component with Store

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

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

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

    onRefresh(): void {
        this.reload();
    }

    onDelete(item: FeatureDto): void {
        this.store.deleteItem(item.id);
    }
}
<app-loading-and-error-indicator [target]="this">
    @if (vm(); as vm) {
    <div class="feature-list">
        <div class="feature-list__header">
            <h1 class="feature-list__title">Features</h1>
            <button class="feature-list__btn --refresh" (click)="onRefresh()" [disabled]="isStateLoading()()">Refresh</button>
        </div>
        @for (item of vm.items; track trackByItem) {
        <div class="feature-list__item">
            <span class="feature-list__item-name">{{ item.name }}</span>
            <button class="feature-list__item-btn --delete" (click)="onDelete(item)">Delete</button>
        </div>
        } @empty {
        <div class="feature-list__empty">No items found</div>
        }
    </div>
    }
</app-loading-and-error-indicator>

Pattern: Simple Component

@Component({
    selector: 'app-feature-card',
    template: `
        <div class="feature-card" [class.--selected]="isSelected">
            <h3 class="feature-card__title">{{ feature.name }}</h3>
            <p class="feature-card__desc">{{ feature.description }}</p>
            @if (canEdit) {
                <button class="feature-card__btn --edit" (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 (Components)

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

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

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

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

// Track-by for @for loops
trackByItem = this.ngForTrackByItemProp<Item>('id');

---

Part 2: Forms

Form Base Class Selection

Base ClassUse When
PlatformFormComponentBasic form without auth
AppBaseFormComponentForm with auth context

Pattern: Basic Form

export interface FeatureFormVm {
    id?: string;
    name: string;
    code: string;
    status: FeatureStatus;
    isActive: boolean;
}

@Component({
    selector: 'app-feature-form',
    templateUrl: './feature-form.component.html'
})
export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
    @Input() featureId?: string;

    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_-]+$/)]),
            status: new FormControl(this.currentVm().status, [Validators.required]),
            isActive: new FormControl(this.currentVm().isActive)
        }
    });

    protected initOrReloadVm = (isReload: boolean) => {
        if (!this.featureId) {
            return of<FeatureFormVm>({ name: '', code: '', status: FeatureStatus.Draft, isActive: true });
        }
        return this.featureApi.getById(this.featureId);
    };

    onSubmit(): void {
        if (!this.validateForm()) return;
        this.featureApi
            .save(this.currentVm())
            .pipe(
                this.observerLoadingErrorState('save'),
                this.tapResponse(
                    saved => this.onSuccess(saved),
                    error => this.onError(error)
                ),
                this.untilDestroyed()
            )
            .subscribe();
    }

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

Pattern: Async Validation

protected initialFormConfig = () => ({
    controls: {
        code: new FormControl(
            this.currentVm().code,
            [Validators.required, Validators.pattern(/^[A-Z0-9_-]+$/)],
            [ifAsyncValidator(ctrl => ctrl.valid, this.checkCodeUniqueValidator())]
        )
    }
});

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))
        );
        return exists ? { codeExists: 'Code already exists' } : null;
    };
}

Pattern: Dependent Validation

protected initialFormConfig = () => ({
    controls: {
        startDate: new FormControl(this.currentVm().startDate, [Validators.required]),
        endDate: new FormControl(this.currentVm().endDate, [
            Validators.required,
            startEndValidator('invalidRange', ctrl => ctrl.parent?.get('startDate')?.value, ctrl => ctrl.value, { allowEqual: true })
        ])
    },
    dependentValidations: {
        endDate: ['startDate']  // Re-validate endDate when startDate changes
    }
});

Pattern: FormArray

protected initialFormConfig = () => ({
    controls: {
        name: new FormControl(this.currentVm().name, [Validators.required]),
        specifications: {
            modelItems: () => this.currentVm().specifications,
            itemControl: (spec: Specification) => new FormGroup({
                name: new FormControl(spec.name, [Validators.required]),
                value: new FormControl(spec.value, [Validators.required])
            })
        }
    }
});

get specificationsArray(): FormArray { return this.form.get('specifications') as FormArray; }

addSpecification(): void {
    this.updateVm(vm => ({ specifications: [...vm.specifications, { name: '', value: '' }] }));
    this.specificationsArray.push(new FormGroup({
        name: new FormControl('', [Validators.required]),
        value: new FormControl('', [Validators.required])
    }));
}

removeSpecification(index: number): void {
    this.updateVm(vm => ({ specifications: vm.specifications.filter((_, i) => i !== index) }));
    this.specificationsArray.removeAt(index);
}

Form Template

<form class="feature-form" [formGroup]="form" (ngSubmit)="onSubmit()">
    <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>
        } @if (formControls('code').errors?.['codeExists']) {
        <span class="feature-form__error">{{ formControls('code').errors?.['codeExists'] }}</span>
        }
    </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" [disabled]="!form.valid || isLoading$('save')()">
            {{ isLoading$('save')() ? 'Saving...' : 'Save' }}
        </button>
    </div>
</form>

Built-in Validators

ValidatorImportUsage
noWhitespaceValidator@libs/platform-coreNo empty strings
startEndValidator@libs/platform-coreDate/number range
ifAsyncValidator@libs/platform-coreConditional async
validator@libs/platform-coreCustom validator factory

Key Form APIs

MethodPurposeExample
validateForm()Validate and mark touchedif (!this.validateForm()) return;
formControls(key)Get form controlthis.formControls('name').errors
currentVm()Get current view modelconst vm = this.currentVm()
updateVm()Update view modelthis.updateVm({ name: 'new' })
modeForm modethis.mode === 'create'
isViewMode()Check view modeif (this.isViewMode()) return;

---

Part 3: Stores (State Management)

Store Architecture

PlatformVmStore<TState>
  State: TState (reactive signal)
  Selectors: select() -> Signal<T>
  Effects: effectSimple() -> side effects
  Updaters: updateState() -> mutations
  Loading/Error: observerLoadingErrorState()

Pattern: Basic CRUD Store

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

@Injectable()
export class FeatureListStore extends PlatformVmStore<FeatureListState> {
    protected override vmConstructor = (data?: Partial<FeatureListState>) =>
        ({ items: [], filters: {}, pagination: { pageIndex: 0, pageSize: 20, totalCount: 0 }, ...data }) as FeatureListState;

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

    // Effects
    public loadItems = this.effectSimple(() => {
        const state = this.currentVm();
        return this.featureApi
            .getList({
                ...state.filters,
                skipCount: state.pagination.pageIndex * state.pagination.pageSize,
                maxResultCount: state.pagination.pageSize
            })
            .pipe(
                this.tapResponse(result => {
                    this.updateState({ items: result.items, pagination: { ...state.pagination, totalCount: result.totalCount } });
                })
            );
    }, 'loadItems');

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

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

    // Updaters
    public setFilters(filters: Partial<FeatureFilters>): void {
        this.updateState(state => ({ filters: { ...state.filters, ...filters }, pagination: { ...state.pagination, pageIndex: 0 } }));
    }

    public setPage(pageIndex: number): void {
        this.updateState(state => ({ pagination: { ...state.pagination, pageIndex } }));
    }

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

Pattern: Store with Dependent Data

@Injectable()
export class EmployeeFormStore extends PlatformVmStore<EmployeeFormState> {
    public loadFormData = this.effectSimple(
        (employeeId?: string) =>
            forkJoin({
                employee: employeeId ? this.employeeApi.getById(employeeId) : of(this.createNewEmployee()),
                departments: this.departmentApi.getActive(),
                positions: this.positionApi.getAll()
            }).pipe(this.tapResponse(result => this.updateState(result))),
        'loadFormData'
    );
}

Pattern: Store with Caching

@Injectable({ providedIn: 'root' }) // Singleton for caching
export class LookupDataStore extends PlatformVmStore<LookupDataState> {
    protected override get enableCaching() {
        return true;
    }
    protected override cachedStateKeyName = () => 'LookupDataStore';
    protected override get cacheExpirationMs() {
        return 5 * 60 * 1000;
    }

    public loadCountries = this.effectSimple(() => {
        if (this.currentVm().countries.length > 0) return EMPTY;
        return this.lookupApi.getCountries().pipe(this.tapResponse(countries => this.updateState({ countries })));
    }, 'loadCountries');
}

Key Store APIs

MethodPurposeExample
select()Create selectorthis.select(s => s.items)
updateState()Update statethis.updateState({ items })
effectSimple()Create effectthis.effectSimple(() => api.call(), 'requestKey')
currentVm()Get current stateconst state = this.currentVm()
observerLoadingErrorState()Track loading/errorUse outside effectSimple only
tapResponse()Handle success/error.pipe(this.tapResponse(success, error))
isLoading$()Loading signalthis.store.isLoading$('loadItems')

---

Part 4: API Services

Pattern: Basic CRUD API Service

@Injectable({ providedIn: 'root' })
export class FeatureApiService extends PlatformApiService {
    protected get apiUrl(): string {
        return environment.apiUrl + '/api/Feature';
    }

    getList(query?: FeatureListQuery): Observable<PagedResult<FeatureDto>> {
        return this.get<PagedResult<FeatureDto>>('', query);
    }

    getById(id: string): Observable<FeatureDto> {
        return this.get<FeatureDto>(`/${id}`);
    }

    save(command: SaveFeatureCommand): Observable<FeatureDto> {
        return this.post<FeatureDto>('', command);
    }

    update(id: string, command: Partial<SaveFeatureCommand>): Observable<FeatureDto> {
        return this.put<FeatureDto>(`/${id}`, command);
    }

    delete(id: string): Observable<void> {
        return this.deleteRequest<void>(`/${id}`);
    }

    checkCodeExists(code: string, excludeId?: string): Observable<boolean> {
        return this.get<boolean>('/check-code-exists', { code, excludeId });
    }
}

Pattern: API Service with Caching

@Injectable({ providedIn: 'root' })
export class LookupApiService extends PlatformApiService {
    protected get apiUrl(): string {
        return environment.apiUrl + '/api/Lookup';
    }

    getCountries(): Observable<CountryDto[]> {
        return this.get<CountryDto[]>('/countries', null, { enableCache: true, cacheKey: 'countries', cacheDurationMs: 60 * 60 * 1000 });
    }

    invalidateCountriesCache(): void {
        this.clearCache('countries');
    }
    invalidateAllCache(): void {
        this.clearAllCache();
    }
}

Pattern: File Upload/Download

@Injectable({ providedIn: 'root' })
export class DocumentApiService extends PlatformApiService {
    protected get apiUrl(): string {
        return environment.apiUrl + '/api/Document';
    }

    upload(file: File, metadata?: DocumentMetadata): Observable<DocumentDto> {
        const formData = new FormData();
        formData.append('file', file, file.name);
        if (metadata) formData.append('metadata', JSON.stringify(metadata));
        return this.postFormData<DocumentDto>('/upload', formData);
    }

    download(id: string): Observable<Blob> {
        return this.getBlob(`/${id}/download`);
    }

    downloadAndSave(id: string, fileName: string): Observable<void> {
        return this.download(id).pipe(
            tap(blob => {
                const url = window.URL.createObjectURL(blob);
                const link = document.createElement('a');
                link.href = url;
                link.download = fileName;
                link.click();
                window.URL.revokeObjectURL(url);
            }),
            map(() => void 0)
        );
    }
}

Pattern: Search/Autocomplete API

@Injectable({ providedIn: 'root' })
export class EmployeeApiService extends PlatformApiService {
    protected get apiUrl(): string {
        return environment.apiUrl + '/api/Employee';
    }

    search(term: string): Observable<EmployeeDto[]> {
        if (!term || term.length < 2) return of([]);
        return this.get<EmployeeDto[]>('/search', { searchText: term, maxResultCount: 10 });
    }

    autocomplete(prefix: string): Observable<AutocompleteItem[]> {
        return this.get<AutocompleteItem[]>(
            '/autocomplete',
            { prefix },
            {
                enableCache: true,
                cacheKey: `autocomplete-${prefix}`,
                cacheDurationMs: 30 * 1000
            }
        );
    }
}

Base PlatformApiService Methods

MethodPurposeExample
get<T>()GET requestthis.get<User>('/users/1')
post<T>()POST requestthis.post<User>('/users', data)
put<T>()PUT requestthis.put<User>('/users/1', data)
patch<T>()PATCH requestthis.patch<User>('/users/1', partial)
deleteRequest<T>()DELETE requestthis.deleteRequest('/users/1')
postFormData<T>()POST with FormDatathis.postFormData('/upload', formData)
getBlob()GET binary datathis.getBlob('/file/download')
clearCache()Clear specific cachethis.clearCache('cacheKey')
clearAllCache()Clear all cachethis.clearAllCache()

Request Options

interface RequestOptions {
    enableCache?: boolean;
    cacheKey?: string;
    cacheDurationMs?: number;
    headers?: { [key: string]: string };
    responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
    reportProgress?: boolean;
    observe?: 'body' | 'events' | 'response';
}

---

Anti-Patterns to AVOID

Wrong base class:

// BAD: using PlatformComponent when auth needed
export class MyComponent extends PlatformComponent {}
// GOOD:
export class MyComponent extends AppBaseComponent {}

Manual subscription management:

// BAD
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }
// GOOD
this.data$.pipe(this.untilDestroyed()).subscribe();

Direct HTTP calls:

// BAD
constructor(private http: HttpClient) { }
// GOOD
constructor(private featureApi: FeatureApiService) { }

Missing loading states:

<!-- BAD -->
<div>{{ items }}</div>
<!-- GOOD -->
<app-loading-and-error-indicator [target]="this">
    <div>{{ items }}</div>
</app-loading-and-error-indicator>

Not using validateForm():

// BAD
onSubmit() { this.api.save(this.currentVm()); }
// GOOD
onSubmit() { if (!this.validateForm()) return; this.api.save(this.currentVm()); }

Async validator always runs:

// BAD
new FormControl('', [], [asyncValidator]);
// GOOD
new FormControl('', [], [ifAsyncValidator(ctrl => ctrl.valid, asyncValidator)]);

Mutating state directly:

// BAD
this.currentVm().items.push(newItem);
// GOOD
this.updateState(state => ({ items: [...state.items, newItem] }));

Missing BEM classes:

<!-- BAD -->
<div><label>Name</label><input formControlName="name" /></div>
<!-- GOOD -->
<div class="form__field"><label class="form__label">Name</label><input class="form__input" formControlName="name" /></div>

---

Verification Checklist

Components

  • [ ] 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
  • [ ] Auth checks use hasRole() from base class
  • [ ] API calls use service extending PlatformApiService

Forms

  • [ ] initialFormConfig returns form configuration
  • [ ] initOrReloadVm loads data for edit mode
  • [ ] validateForm() called before submit
  • [ ] Async validators use ifAsyncValidator
  • [ ] dependentValidations configured for cross-field validation
  • [ ] Error messages displayed for all validation rules

Stores

  • [ ] State interface defines all required properties
  • [ ] vmConstructor provides default state
  • [ ] Effects use effectSimple() with request key
  • [ ] Effects use tapResponse() for handling
  • [ ] Selectors memoized with select()
  • [ ] State updates are immutable
  • [ ] Store provided at correct level (component vs root)

API Services

  • [ ] Extends PlatformApiService
  • [ ] apiUrl getter returns correct base URL
  • [ ] All methods have return type annotations
  • [ ] DTOs defined for request/response
  • [ ] @Injectable({ providedIn: 'root' }) for singleton

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.