angular-store

Angular Store Development Workflow

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "angular-store" with this command: npx skills add congdon1207/agents.md/congdon1207-agents-md-angular-store

Angular Store Development Workflow

When to Use This Skill

  • List components with CRUD operations

  • Complex state with multiple data sources

  • Shared state between components

  • Caching and reloading patterns

Pre-Flight Checklist

  • Identify state shape (what data is needed)

  • Read the design system docs for the target application (see below)

  • Identify side effects (API calls, etc.)

  • Search similar stores: grep "{Feature}Store" --include="*.ts"

  • Determine caching requirements

🎨 Design System Documentation (MANDATORY)

Before creating any store, 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 summary

  • 06-state-management.md

  • State management patterns (NgRx, PlatformVmStore)

  • 07-technical-guide.md

  • Implementation checklist, best practices

File Location

src/PlatformExampleAppWeb/apps/{app-name}/src/app/ └── features/ └── {feature}/ ├── {feature}.store.ts └── {feature}.component.ts

Store Architecture

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

Pattern 1: Basic CRUD Store

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

// ═══════════════════════════════════════════════════════════════════════════ // STATE INTERFACE // ═══════════════════════════════════════════════════════════════════════════

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

export interface FeatureFilters { searchText?: string; status?: FeatureStatus[]; dateRange?: DateRange; }

export interface PaginationState { pageIndex: number; pageSize: number; totalCount: number; }

// ═══════════════════════════════════════════════════════════════════════════ // STORE IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════════════

@Injectable() export class FeatureListStore extends PlatformVmStore<FeatureListState> { // ───────────────────────────────────────────────────────────────────────── // CONFIGURATION // ─────────────────────────────────────────────────────────────────────────

// State factory
protected override vmConstructor = (data?: Partial&#x3C;FeatureListState>) =>
    ({
        items: [],
        filters: {},
        pagination: { pageIndex: 0, pageSize: 20, totalCount: 0 },
        ...data
    }) as FeatureListState;

// Optional: Enable caching
protected override get enableCaching() {
    return true;
}
protected override cachedStateKeyName = () => 'FeatureListStore';

// ─────────────────────────────────────────────────────────────────────────
// SELECTORS (Reactive Signals)
// ─────────────────────────────────────────────────────────────────────────

public readonly items$ = this.select(state => state.items);
public readonly selectedItem$ = this.select(state => state.selectedItem);
public readonly filters$ = this.select(state => state.filters);
public readonly pagination$ = this.select(state => state.pagination);

// Derived selectors
public readonly hasItems$ = this.select(state => state.items.length > 0);
public readonly isEmpty$ = this.select(state => state.items.length === 0);

// ─────────────────────────────────────────────────────────────────────────
// EFFECTS (Side Effects)
// ─────────────────────────────────────────────────────────────────────────

// Load items with current filters
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');

// Save item (create or update)
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'
);

// Delete item
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'
);

// ─────────────────────────────────────────────────────────────────────────
// STATE UPDATERS
// ─────────────────────────────────────────────────────────────────────────

public setFilters(filters: Partial&#x3C;FeatureFilters>): void {
    this.updateState(state => ({
        filters: { ...state.filters, ...filters },
        pagination: { ...state.pagination, pageIndex: 0 } // Reset to first page
    }));
}

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

public selectItem(item?: FeatureDto): void {
    this.updateState({ selectedItem: item });
}

public clearFilters(): void {
    this.updateState({
        filters: {},
        pagination: { ...this.currentVm().pagination, pageIndex: 0 }
    });
}

// ─────────────────────────────────────────────────────────────────────────
// CONSTRUCTOR
// ─────────────────────────────────────────────────────────────────────────

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

}

Pattern 2: Store with Dependent Data

@Injectable() export class EmployeeFormStore extends PlatformVmStore<EmployeeFormState> { protected override vmConstructor = (data?: Partial<EmployeeFormState>) => ({ employee: null, departments: [], positions: [], managers: [], ...data }) as EmployeeFormState;

// Load all dependent data in parallel
public loadFormData = this.effectSimple(
    (employeeId?: string) =>
        forkJoin({
            employee: employeeId ? this.employeeApi.getById(employeeId) : of(this.createNewEmployee()),
            departments: this.departmentApi.getActive(),
            positions: this.positionApi.getAll(),
            managers: this.employeeApi.getManagers()
        }).pipe(
            this.tapResponse(result => {
                this.updateState({
                    employee: result.employee,
                    departments: result.departments,
                    positions: result.positions,
                    managers: result.managers
                });
            })
        ),
    'loadFormData'
);

// Dependent selector - filter managers by department
public managersForDepartment$ = (departmentId: string) => this.select(state => state.managers.filter(m => m.departmentId === departmentId));

}

Pattern 3: 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';

// Cache timeout (optional)
protected override get cacheExpirationMs() {
    return 5 * 60 * 1000;
} // 5 minutes

// Load with cache check
public loadCountries = this.effectSimple(() => {
    if (this.currentVm().countries.length > 0) {
        return EMPTY; // Already loaded, skip
    }

    return this.lookupApi.getCountries().pipe(
        this.tapResponse(countries => {
            this.updateState({ countries });
        })
    );
}, 'loadCountries');

}

Component Integration

@Component({ selector: 'app-feature-list', templateUrl: './feature-list.component.html', providers: [FeatureListStore] // Component-scoped store }) export class FeatureListComponent extends AppBaseVmStoreComponent<FeatureListState, FeatureListStore> implements OnInit { constructor(store: FeatureListStore) { super(store); }

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

onSearch(text: string): void {
    this.store.setFilters({ searchText: text });
    this.store.loadItems();
}

onPageChange(pageIndex: number): void {
    this.store.setPage(pageIndex);
    this.store.loadItems();
}

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

onRefresh(): void {
    this.reload(); // Inherited - reloads all store effects
}

// Loading states
get isLoading$() {
    return this.store.isLoading$('loadItems');
}
get isSaving$() {
    return this.store.isLoading$('saveItem');
}
get isDeleting$() {
    return this.store.isLoading$('deleteItem');
}

}

Template Usage

<app-loading-and-error-indicator [target]="this"> @if (vm(); as vm) { <!-- Filters --> <div class="filters"> <input [value]="vm.filters.searchText ?? ''" (input)="onSearch($event.target.value)" placeholder="Search..." /> </div>

&#x3C;!-- List -->
@for (item of vm.items; track item.id) {
&#x3C;div class="item" [class.selected]="vm.selectedItem?.id === item.id">
    {{ item.name }}
    &#x3C;button (click)="onDelete(item)" [disabled]="isDeleting$()">Delete&#x3C;/button>
&#x3C;/div>
} @empty {
&#x3C;div class="empty">No items found&#x3C;/div>
}

&#x3C;!-- Pagination -->
&#x3C;app-pagination
    [pageIndex]="vm.pagination.pageIndex"
    [pageSize]="vm.pagination.pageSize"
    [totalCount]="vm.pagination.totalCount"
    (pageChange)="onPageChange($event)"
/>
}

</app-loading-and-error-indicator>

Key Store APIs

Method Purpose Example

select()

Create selector this.select(s => s.items)

updateState()

Update state this.updateState({ items })

effectSimple()

Create effect this.effectSimple(() => api.call(), 'requestKey')

currentVm()

Get current state const state = this.currentVm()

observerLoadingErrorState()

Track loading/error Use outside effectSimple only (effectSimple handles this)

tapResponse()

Handle success/error .pipe(this.tapResponse(success, error))

isLoading$()

Loading signal this.store.isLoading$('loadItems')

Anti-Patterns to AVOID

:x: Calling effects without tracking

// WRONG - no loading state this.api.getItems().subscribe(items => this.updateState({ items }));

// CORRECT - effectSimple auto-tracks loading state via second parameter public loadItems = this.effectSimple( () => this.api.getItems().pipe( this.tapResponse(items => this.updateState({ items })) ), 'loadItems' );

:x: Mutating state directly

// WRONG - direct mutation this.currentVm().items.push(newItem);

// CORRECT - immutable update this.updateState(state => ({ items: [...state.items, newItem] }));

:x: Using store without provider

// WRONG - no provider export class MyComponent { constructor(private store: FeatureStore) { } // Error: No provider }

// CORRECT - provide at component level @Component({ providers: [FeatureStore] })

Verification Checklist

  • State interface defines all required properties

  • vmConstructor provides default state

  • Effects use effectSimple() with request key as second parameter

  • Effects use tapResponse() for handling

  • Selectors are memoized with select()

  • State updates are immutable

  • Store provided at correct level (component vs root)

  • Caching configured if needed

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

clean-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops

No summary provided by upstream source.

Repository SourceNeeds Review