Claude-skill-registry angular-store
Use when implementing state management with PlatformVmStore for complex components requiring reactive state, effects, and selectors.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-angular-store" ~/.claude/skills/majiayu000-claude-skill-registry-angular-store && rm -rf "$T"
manifest:
skills/data/frontend-angular-store/SKILL.mdsource content
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 | |
| TextSnippetClient | |
Key docs to read:
- Component overview, base classes, library summaryREADME.md
- State management patterns (NgRx, PlatformVmStore)06-state-management.md
- Implementation checklist, best practices07-technical-guide.md
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<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<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> <!-- List --> @for (item of vm.items; track item.id) { <div class="item" [class.selected]="vm.selectedItem?.id === item.id"> {{ item.name }} <button (click)="onDelete(item)" [disabled]="isDeleting$()">Delete</button> </div> } @empty { <div class="empty">No items found</div> } <!-- Pagination --> <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 |
|---|---|---|
| Create selector | |
| Update state | |
| Create effect | |
| Get current state | |
| Track loading/error | Use outside effectSimple only (effectSimple handles this) |
| Handle success/error | |
| Loading signal | |
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
-
provides default statevmConstructor - Effects use
with request key as second parametereffectSimple() - Effects use
for handlingtapResponse() - Selectors are memoized with
select() - State updates are immutable
- Store provided at correct level (component vs root)
- Caching configured if needed