@malamute/ai-rules 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "libs/**/data-access/**/*.ts"
|
|
4
|
+
- "**/*.store.ts"
|
|
5
|
+
- "**/+state/**/*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# NgRx SignalStore
|
|
9
|
+
|
|
10
|
+
The `@ngrx/signals` package provides a modern, signal-based state management solution.
|
|
11
|
+
|
|
12
|
+
## When to Use SignalStore vs Classic NgRx
|
|
13
|
+
|
|
14
|
+
| Use SignalStore | Use Classic NgRx |
|
|
15
|
+
|-----------------|------------------|
|
|
16
|
+
| New projects | Existing NgRx codebase |
|
|
17
|
+
| Feature-level state | App-wide state with DevTools |
|
|
18
|
+
| Simpler mental model | Complex async orchestration |
|
|
19
|
+
| Less boilerplate | Redux pattern familiarity |
|
|
20
|
+
|
|
21
|
+
## Basic SignalStore
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// stores/counter.store.ts
|
|
25
|
+
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
|
|
26
|
+
|
|
27
|
+
export const CounterStore = signalStore(
|
|
28
|
+
withState({
|
|
29
|
+
count: 0,
|
|
30
|
+
}),
|
|
31
|
+
withMethods((store) => ({
|
|
32
|
+
increment(): void {
|
|
33
|
+
patchState(store, { count: store.count() + 1 });
|
|
34
|
+
},
|
|
35
|
+
decrement(): void {
|
|
36
|
+
patchState(store, { count: store.count() - 1 });
|
|
37
|
+
},
|
|
38
|
+
reset(): void {
|
|
39
|
+
patchState(store, { count: 0 });
|
|
40
|
+
},
|
|
41
|
+
})),
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Usage in component
|
|
47
|
+
@Component({
|
|
48
|
+
selector: 'app-counter',
|
|
49
|
+
providers: [CounterStore], // Provide at component level
|
|
50
|
+
template: `
|
|
51
|
+
<p>Count: {{ store.count() }}</p>
|
|
52
|
+
<button (click)="store.increment()">+</button>
|
|
53
|
+
<button (click)="store.decrement()">-</button>
|
|
54
|
+
`,
|
|
55
|
+
})
|
|
56
|
+
export class CounterComponent {
|
|
57
|
+
protected readonly store = inject(CounterStore);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## SignalStore with Computed
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
|
|
65
|
+
import { computed } from '@angular/core';
|
|
66
|
+
|
|
67
|
+
interface TodoState {
|
|
68
|
+
todos: Todo[];
|
|
69
|
+
filter: 'all' | 'active' | 'completed';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const TodoStore = signalStore(
|
|
73
|
+
withState<TodoState>({
|
|
74
|
+
todos: [],
|
|
75
|
+
filter: 'all',
|
|
76
|
+
}),
|
|
77
|
+
withComputed(({ todos, filter }) => ({
|
|
78
|
+
filteredTodos: computed(() => {
|
|
79
|
+
const allTodos = todos();
|
|
80
|
+
switch (filter()) {
|
|
81
|
+
case 'active':
|
|
82
|
+
return allTodos.filter(t => !t.completed);
|
|
83
|
+
case 'completed':
|
|
84
|
+
return allTodos.filter(t => t.completed);
|
|
85
|
+
default:
|
|
86
|
+
return allTodos;
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
completedCount: computed(() => todos().filter(t => t.completed).length),
|
|
90
|
+
activeCount: computed(() => todos().filter(t => !t.completed).length),
|
|
91
|
+
})),
|
|
92
|
+
withMethods((store) => ({
|
|
93
|
+
addTodo(text: string): void {
|
|
94
|
+
patchState(store, {
|
|
95
|
+
todos: [...store.todos(), { id: crypto.randomUUID(), text, completed: false }],
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
toggleTodo(id: string): void {
|
|
99
|
+
patchState(store, {
|
|
100
|
+
todos: store.todos().map(t =>
|
|
101
|
+
t.id === id ? { ...t, completed: !t.completed } : t
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
setFilter(filter: TodoState['filter']): void {
|
|
106
|
+
patchState(store, { filter });
|
|
107
|
+
},
|
|
108
|
+
})),
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## SignalStore with Entity Adapter
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { signalStore, withMethods, patchState } from '@ngrx/signals';
|
|
116
|
+
import { withEntities, addEntity, updateEntity, removeEntity, setAllEntities } from '@ngrx/signals/entities';
|
|
117
|
+
|
|
118
|
+
interface User {
|
|
119
|
+
id: string;
|
|
120
|
+
name: string;
|
|
121
|
+
email: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const UserStore = signalStore(
|
|
125
|
+
{ providedIn: 'root' }, // Singleton store
|
|
126
|
+
withEntities<User>(),
|
|
127
|
+
withMethods((store) => ({
|
|
128
|
+
setUsers(users: User[]): void {
|
|
129
|
+
patchState(store, setAllEntities(users));
|
|
130
|
+
},
|
|
131
|
+
addUser(user: User): void {
|
|
132
|
+
patchState(store, addEntity(user));
|
|
133
|
+
},
|
|
134
|
+
updateUser(id: string, changes: Partial<User>): void {
|
|
135
|
+
patchState(store, updateEntity({ id, changes }));
|
|
136
|
+
},
|
|
137
|
+
removeUser(id: string): void {
|
|
138
|
+
patchState(store, removeEntity(id));
|
|
139
|
+
},
|
|
140
|
+
})),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Exposes: store.entities(), store.ids(), store.entityMap()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## SignalStore with Async Methods
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
|
|
150
|
+
import { inject } from '@angular/core';
|
|
151
|
+
import { firstValueFrom } from 'rxjs';
|
|
152
|
+
|
|
153
|
+
interface ProductState {
|
|
154
|
+
products: Product[];
|
|
155
|
+
loading: boolean;
|
|
156
|
+
error: string | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const ProductStore = signalStore(
|
|
160
|
+
{ providedIn: 'root' },
|
|
161
|
+
withState<ProductState>({
|
|
162
|
+
products: [],
|
|
163
|
+
loading: false,
|
|
164
|
+
error: null,
|
|
165
|
+
}),
|
|
166
|
+
withMethods((store, productService = inject(ProductService)) => ({
|
|
167
|
+
async loadProducts(): Promise<void> {
|
|
168
|
+
patchState(store, { loading: true, error: null });
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const products = await firstValueFrom(productService.getAll());
|
|
172
|
+
patchState(store, { products, loading: false });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
patchState(store, {
|
|
175
|
+
loading: false,
|
|
176
|
+
error: error instanceof Error ? error.message : 'Failed to load products',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async addProduct(data: CreateProductDto): Promise<void> {
|
|
182
|
+
const product = await firstValueFrom(productService.create(data));
|
|
183
|
+
patchState(store, { products: [...store.products(), product] });
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async deleteProduct(id: string): Promise<void> {
|
|
187
|
+
await firstValueFrom(productService.delete(id));
|
|
188
|
+
patchState(store, {
|
|
189
|
+
products: store.products().filter(p => p.id !== id),
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
})),
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## SignalStore with Hooks
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { signalStore, withState, withMethods, withHooks, patchState } from '@ngrx/signals';
|
|
200
|
+
|
|
201
|
+
export const AuthStore = signalStore(
|
|
202
|
+
{ providedIn: 'root' },
|
|
203
|
+
withState({
|
|
204
|
+
user: null as User | null,
|
|
205
|
+
token: null as string | null,
|
|
206
|
+
initialized: false,
|
|
207
|
+
}),
|
|
208
|
+
withMethods((store, authService = inject(AuthService)) => ({
|
|
209
|
+
async initialize(): Promise<void> {
|
|
210
|
+
const token = localStorage.getItem('token');
|
|
211
|
+
if (token) {
|
|
212
|
+
try {
|
|
213
|
+
const user = await firstValueFrom(authService.validateToken(token));
|
|
214
|
+
patchState(store, { user, token, initialized: true });
|
|
215
|
+
} catch {
|
|
216
|
+
localStorage.removeItem('token');
|
|
217
|
+
patchState(store, { initialized: true });
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
patchState(store, { initialized: true });
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
logout(): void {
|
|
224
|
+
localStorage.removeItem('token');
|
|
225
|
+
patchState(store, { user: null, token: null });
|
|
226
|
+
},
|
|
227
|
+
})),
|
|
228
|
+
withHooks({
|
|
229
|
+
onInit(store) {
|
|
230
|
+
// Called when store is first injected
|
|
231
|
+
store.initialize();
|
|
232
|
+
},
|
|
233
|
+
onDestroy(store) {
|
|
234
|
+
// Called when store is destroyed (component-level stores)
|
|
235
|
+
console.log('Auth store destroyed');
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## SignalStore with Custom Features
|
|
242
|
+
|
|
243
|
+
Create reusable store features:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// features/with-loading.ts
|
|
247
|
+
import { signalStoreFeature, withState, withMethods, patchState } from '@ngrx/signals';
|
|
248
|
+
|
|
249
|
+
export function withLoading() {
|
|
250
|
+
return signalStoreFeature(
|
|
251
|
+
withState({
|
|
252
|
+
loading: false,
|
|
253
|
+
error: null as string | null,
|
|
254
|
+
}),
|
|
255
|
+
withMethods((store) => ({
|
|
256
|
+
setLoading(loading: boolean): void {
|
|
257
|
+
patchState(store, { loading, error: null });
|
|
258
|
+
},
|
|
259
|
+
setError(error: string): void {
|
|
260
|
+
patchState(store, { loading: false, error });
|
|
261
|
+
},
|
|
262
|
+
})),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Usage
|
|
267
|
+
export const ProductStore = signalStore(
|
|
268
|
+
withLoading(), // Adds loading and error state + methods
|
|
269
|
+
withState({ products: [] as Product[] }),
|
|
270
|
+
withMethods((store) => ({
|
|
271
|
+
async loadProducts(): Promise<void> {
|
|
272
|
+
store.setLoading(true);
|
|
273
|
+
try {
|
|
274
|
+
const products = await this.fetch();
|
|
275
|
+
patchState(store, { products });
|
|
276
|
+
store.setLoading(false);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
store.setError('Failed to load');
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
})),
|
|
282
|
+
);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## SignalStore with RxJS
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { signalStore, withMethods } from '@ngrx/signals';
|
|
289
|
+
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
|
290
|
+
import { pipe, switchMap, tap } from 'rxjs';
|
|
291
|
+
import { tapResponse } from '@ngrx/operators';
|
|
292
|
+
|
|
293
|
+
export const SearchStore = signalStore(
|
|
294
|
+
withState({
|
|
295
|
+
results: [] as SearchResult[],
|
|
296
|
+
loading: false,
|
|
297
|
+
}),
|
|
298
|
+
withMethods((store, searchService = inject(SearchService)) => ({
|
|
299
|
+
search: rxMethod<string>(
|
|
300
|
+
pipe(
|
|
301
|
+
debounceTime(300),
|
|
302
|
+
distinctUntilChanged(),
|
|
303
|
+
tap(() => patchState(store, { loading: true })),
|
|
304
|
+
switchMap((query) =>
|
|
305
|
+
searchService.search(query).pipe(
|
|
306
|
+
tapResponse({
|
|
307
|
+
next: (results) => patchState(store, { results, loading: false }),
|
|
308
|
+
error: () => patchState(store, { results: [], loading: false }),
|
|
309
|
+
}),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
})),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Usage
|
|
318
|
+
store.search(this.query); // Can pass signal or value
|
|
319
|
+
store.search(this.query$); // Can pass observable
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## File Structure
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
libs/[domain]/data-access/
|
|
326
|
+
src/lib/
|
|
327
|
+
stores/
|
|
328
|
+
[feature].store.ts # SignalStore definition
|
|
329
|
+
[feature].store.spec.ts # Store tests
|
|
330
|
+
features/
|
|
331
|
+
with-loading.ts # Reusable features
|
|
332
|
+
with-pagination.ts
|
|
333
|
+
index.ts # Public API
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Component vs Root-Level Stores
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// Root-level store (singleton)
|
|
340
|
+
export const AuthStore = signalStore(
|
|
341
|
+
{ providedIn: 'root' }, // Singleton for entire app
|
|
342
|
+
withState({ ... }),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Component-level store (instance per component)
|
|
346
|
+
export const FormStore = signalStore(
|
|
347
|
+
// No providedIn - must be added to component providers
|
|
348
|
+
withState({ ... }),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
@Component({
|
|
352
|
+
providers: [FormStore], // New instance for each component
|
|
353
|
+
})
|
|
354
|
+
export class MyFormComponent {
|
|
355
|
+
private readonly store = inject(FormStore);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Anti-patterns
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// BAD: Mutating state directly
|
|
363
|
+
store.users().push(newUser); // Direct mutation!
|
|
364
|
+
|
|
365
|
+
// GOOD: Use patchState with new array
|
|
366
|
+
patchState(store, { users: [...store.users(), newUser] });
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
// BAD: Complex logic in computed
|
|
370
|
+
withComputed(({ users }) => ({
|
|
371
|
+
report: computed(() => {
|
|
372
|
+
// Complex async or side-effect logic here
|
|
373
|
+
fetch('/api/report'); // WRONG!
|
|
374
|
+
return users();
|
|
375
|
+
}),
|
|
376
|
+
})),
|
|
377
|
+
|
|
378
|
+
// GOOD: Computed should be pure, use methods for side effects
|
|
379
|
+
withComputed(({ users }) => ({
|
|
380
|
+
userCount: computed(() => users().length), // Pure derivation
|
|
381
|
+
})),
|
|
382
|
+
withMethods((store) => ({
|
|
383
|
+
async generateReport(): Promise<void> { ... },
|
|
384
|
+
})),
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
// BAD: Accessing store outside Angular context
|
|
388
|
+
const store = new ProductStore(); // Won't work!
|
|
389
|
+
|
|
390
|
+
// GOOD: Always inject
|
|
391
|
+
protected readonly store = inject(ProductStore);
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// BAD: Providing root store at component level
|
|
395
|
+
export const AuthStore = signalStore({ providedIn: 'root' }, ...);
|
|
396
|
+
|
|
397
|
+
@Component({
|
|
398
|
+
providers: [AuthStore], // Creates duplicate instance!
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// GOOD: Root stores should not be in component providers
|
|
402
|
+
@Component({
|
|
403
|
+
// AuthStore is providedIn: 'root', no need to provide
|
|
404
|
+
})
|
|
405
|
+
export class MyComponent {
|
|
406
|
+
protected readonly auth = inject(AuthStore);
|
|
407
|
+
}
|
|
408
|
+
```
|
|
@@ -234,8 +234,8 @@ Only in smart components (feature/):
|
|
|
234
234
|
|
|
235
235
|
```typescript
|
|
236
236
|
// Use selectSignal for signal-based selection
|
|
237
|
-
users = this.store.selectSignal(selectAllUsers);
|
|
238
|
-
loading = this.store.selectSignal(selectUsersLoading);
|
|
237
|
+
protected readonly users = this.store.selectSignal(selectAllUsers);
|
|
238
|
+
protected readonly loading = this.store.selectSignal(selectUsersLoading);
|
|
239
239
|
|
|
240
240
|
// Dispatch actions
|
|
241
241
|
this.store.dispatch(UserActions.loadUsers());
|
|
@@ -374,10 +374,10 @@ test.describe('User Management', () => {
|
|
|
374
374
|
import { Page, Locator } from '@playwright/test';
|
|
375
375
|
|
|
376
376
|
export class UserListPage {
|
|
377
|
-
readonly page: Page;
|
|
378
|
-
readonly searchInput: Locator;
|
|
379
|
-
readonly userItems: Locator;
|
|
380
|
-
readonly addUserButton: Locator;
|
|
377
|
+
private readonly page: Page;
|
|
378
|
+
private readonly searchInput: Locator;
|
|
379
|
+
private readonly userItems: Locator;
|
|
380
|
+
private readonly addUserButton: Locator;
|
|
381
381
|
|
|
382
382
|
constructor(page: Page) {
|
|
383
383
|
this.page = page;
|
|
@@ -386,15 +386,15 @@ export class UserListPage {
|
|
|
386
386
|
this.addUserButton = page.getByRole('button', { name: 'Add User' });
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
async goto(): Promise<void> {
|
|
389
|
+
public async goto(): Promise<void> {
|
|
390
390
|
await this.page.goto('/users');
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
async searchUser(name: string): Promise<void> {
|
|
393
|
+
public async searchUser(name: string): Promise<void> {
|
|
394
394
|
await this.searchInput.fill(name);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
async selectUser(index: number): Promise<void> {
|
|
397
|
+
public async selectUser(index: number): Promise<void> {
|
|
398
398
|
await this.userItems.nth(index).click();
|
|
399
399
|
}
|
|
400
400
|
}
|