@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
|
@@ -7,6 +7,59 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
# Component Rules (Angular 21)
|
|
9
9
|
|
|
10
|
+
## Visibility Modifiers (MANDATORY)
|
|
11
|
+
|
|
12
|
+
**Every class member MUST have an explicit visibility modifier.**
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// BAD - No visibility modifiers
|
|
16
|
+
export class UserComponent {
|
|
17
|
+
store = inject(Store);
|
|
18
|
+
users = this.store.selectSignal(selectUsers);
|
|
19
|
+
|
|
20
|
+
loadUsers(): void {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GOOD - Explicit visibility on ALL members
|
|
24
|
+
export class UserComponent {
|
|
25
|
+
private readonly store = inject(Store);
|
|
26
|
+
|
|
27
|
+
protected readonly users = this.store.selectSignal(selectUsers);
|
|
28
|
+
|
|
29
|
+
// Inputs/outputs are public (they are the component API)
|
|
30
|
+
public readonly name = input.required<string>();
|
|
31
|
+
public readonly userSelected = output<User>();
|
|
32
|
+
|
|
33
|
+
public loadUsers(): void {}
|
|
34
|
+
|
|
35
|
+
private formatUser(user: User): string {}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Visibility Rules
|
|
40
|
+
|
|
41
|
+
| Visibility | When to Use |
|
|
42
|
+
|------------|-------------|
|
|
43
|
+
| `private` | Injected services, internal state, helper methods |
|
|
44
|
+
| `protected` | State/methods needed by child classes |
|
|
45
|
+
| `public` | Inputs, outputs, template-bound methods |
|
|
46
|
+
| `readonly` | Injected services, signals (add with visibility) |
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Standard pattern
|
|
50
|
+
private readonly store = inject(Store); // Service injection
|
|
51
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
52
|
+
|
|
53
|
+
protected readonly loading = signal(false); // Internal state
|
|
54
|
+
protected readonly error = signal<string | null>(null);
|
|
55
|
+
|
|
56
|
+
public readonly items = input.required<Item[]>(); // Component API
|
|
57
|
+
public readonly itemSelected = output<Item>();
|
|
58
|
+
|
|
59
|
+
public onItemClick(item: Item): void {} // Template-bound
|
|
60
|
+
private processItem(item: Item): void {} // Internal helper
|
|
61
|
+
```
|
|
62
|
+
|
|
10
63
|
## Code Quality
|
|
11
64
|
|
|
12
65
|
### No Useless Comments
|
|
@@ -108,15 +161,15 @@ Smart components are located in `feature/` libraries.
|
|
|
108
161
|
export class ProductPageComponent {
|
|
109
162
|
private readonly store = inject(Store);
|
|
110
163
|
|
|
111
|
-
categories = this.store.selectSignal(selectCategories);
|
|
112
|
-
filteredProducts = this.store.selectSignal(selectFilteredProducts);
|
|
113
|
-
loading = this.store.selectSignal(selectProductsLoading);
|
|
164
|
+
protected readonly categories = this.store.selectSignal(selectCategories);
|
|
165
|
+
protected readonly filteredProducts = this.store.selectSignal(selectFilteredProducts);
|
|
166
|
+
protected readonly loading = this.store.selectSignal(selectProductsLoading);
|
|
114
167
|
|
|
115
|
-
onFilterChange(filters: Filters): void {
|
|
168
|
+
public onFilterChange(filters: Filters): void {
|
|
116
169
|
this.store.dispatch(ProductActions.setFilters({ filters }));
|
|
117
170
|
}
|
|
118
171
|
|
|
119
|
-
onAddToCart(product: Product): void {
|
|
172
|
+
public onAddToCart(product: Product): void {
|
|
120
173
|
this.store.dispatch(CartActions.addItem({ product }));
|
|
121
174
|
}
|
|
122
175
|
}
|
|
@@ -168,8 +221,8 @@ UI components are located in `ui/` libraries. They are purely presentational.
|
|
|
168
221
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
169
222
|
})
|
|
170
223
|
export class ProductCardComponent {
|
|
171
|
-
product = input.required<Product>();
|
|
172
|
-
addToCart = output<Product>();
|
|
224
|
+
public readonly product = input.required<Product>();
|
|
225
|
+
public readonly addToCart = output<Product>();
|
|
173
226
|
}
|
|
174
227
|
```
|
|
175
228
|
|
|
@@ -191,17 +244,17 @@ Always use signal-based functions, NOT decorators:
|
|
|
191
244
|
|
|
192
245
|
```typescript
|
|
193
246
|
// Inputs - use input(), NOT @Input()
|
|
194
|
-
name = input<string>(); // Optional
|
|
195
|
-
name = input('default'); // With default
|
|
196
|
-
name = input.required<string>(); // Required
|
|
247
|
+
public readonly name = input<string>(); // Optional
|
|
248
|
+
public readonly name = input('default'); // With default
|
|
249
|
+
public readonly name = input.required<string>(); // Required
|
|
197
250
|
|
|
198
251
|
// Outputs - use output(), NOT @Output()
|
|
199
|
-
clicked = output<void>();
|
|
200
|
-
selected = output<Item>();
|
|
252
|
+
public readonly clicked = output<void>();
|
|
253
|
+
public readonly selected = output<Item>();
|
|
201
254
|
|
|
202
255
|
// Two-way binding - use model()
|
|
203
|
-
value = model<string>(''); // Optional with default
|
|
204
|
-
value = model.required<string>(); // Required
|
|
256
|
+
public readonly value = model<string>(''); // Optional with default
|
|
257
|
+
public readonly value = model.required<string>(); // Required
|
|
205
258
|
|
|
206
259
|
// Emit
|
|
207
260
|
this.clicked.emit();
|
|
@@ -216,9 +269,10 @@ this.value.set('new value'); // model is writable
|
|
|
216
269
|
@Component({
|
|
217
270
|
selector: 'app-search-input',
|
|
218
271
|
templateUrl: './search-input.component.html',
|
|
272
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
219
273
|
})
|
|
220
274
|
export class SearchInputComponent {
|
|
221
|
-
query = model(''); // Creates [(query)] binding
|
|
275
|
+
public readonly query = model(''); // Creates [(query)] binding
|
|
222
276
|
}
|
|
223
277
|
```
|
|
224
278
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.component.ts"
|
|
4
|
+
- "**/*.store.ts"
|
|
5
|
+
- "**/services/**/*.ts"
|
|
6
|
+
- "**/data-access/**/*.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Angular Resource API
|
|
10
|
+
|
|
11
|
+
The `resource()` and `rxResource()` APIs provide declarative data fetching with signals.
|
|
12
|
+
|
|
13
|
+
## Basic Resource
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { resource, Signal } from '@angular/core';
|
|
17
|
+
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'app-user-profile',
|
|
20
|
+
template: `
|
|
21
|
+
@if (userResource.isLoading()) {
|
|
22
|
+
<app-spinner />
|
|
23
|
+
} @else if (userResource.error()) {
|
|
24
|
+
<app-error [message]="userResource.error()?.message" />
|
|
25
|
+
} @else if (userResource.hasValue()) {
|
|
26
|
+
<app-user-card [user]="userResource.value()!" />
|
|
27
|
+
}
|
|
28
|
+
`,
|
|
29
|
+
})
|
|
30
|
+
export class UserProfileComponent {
|
|
31
|
+
private readonly userService = inject(UserService);
|
|
32
|
+
|
|
33
|
+
public readonly userId = input.required<string>();
|
|
34
|
+
|
|
35
|
+
// Resource automatically refetches when userId changes
|
|
36
|
+
protected readonly userResource = resource({
|
|
37
|
+
request: () => this.userId(),
|
|
38
|
+
loader: ({ request: userId }) => this.userService.getById(userId),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Resource Properties
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const userResource = resource({
|
|
47
|
+
request: () => this.userId(),
|
|
48
|
+
loader: ({ request }) => this.userService.getById(request),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Signals exposed by resource
|
|
52
|
+
userResource.value(); // T | undefined - the loaded data
|
|
53
|
+
userResource.isLoading(); // boolean - true while loading
|
|
54
|
+
userResource.error(); // unknown | undefined - error if failed
|
|
55
|
+
userResource.status(); // ResourceStatus - 'idle' | 'loading' | 'resolved' | 'error'
|
|
56
|
+
|
|
57
|
+
// Type guard
|
|
58
|
+
userResource.hasValue(); // boolean - true if value is available
|
|
59
|
+
|
|
60
|
+
// Manual control
|
|
61
|
+
userResource.reload(); // Force refetch with current request
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Resource with Multiple Dependencies
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
@Component({ ... })
|
|
68
|
+
export class ProductListComponent {
|
|
69
|
+
protected readonly category = signal<string>('all');
|
|
70
|
+
protected readonly sortBy = signal<'name' | 'price'>('name');
|
|
71
|
+
protected readonly page = signal(1);
|
|
72
|
+
|
|
73
|
+
// Refetches when ANY dependency changes
|
|
74
|
+
protected readonly productsResource = resource({
|
|
75
|
+
request: () => ({
|
|
76
|
+
category: this.category(),
|
|
77
|
+
sortBy: this.sortBy(),
|
|
78
|
+
page: this.page(),
|
|
79
|
+
}),
|
|
80
|
+
loader: ({ request }) => this.productService.search(request),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## rxResource - RxJS Integration
|
|
86
|
+
|
|
87
|
+
Use `rxResource` when your service returns Observables:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { rxResource } from '@angular/core/rxjs-interop';
|
|
91
|
+
|
|
92
|
+
@Component({ ... })
|
|
93
|
+
export class UserListComponent {
|
|
94
|
+
private readonly userService = inject(UserService);
|
|
95
|
+
|
|
96
|
+
protected readonly searchQuery = signal('');
|
|
97
|
+
|
|
98
|
+
// Works with Observable-returning services
|
|
99
|
+
protected readonly usersResource = rxResource({
|
|
100
|
+
request: () => this.searchQuery(),
|
|
101
|
+
loader: ({ request: query }) => this.userService.search(query),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Conditional Loading
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
@Component({ ... })
|
|
110
|
+
export class UserDetailComponent {
|
|
111
|
+
public readonly userId = input<string | null>(null);
|
|
112
|
+
|
|
113
|
+
protected readonly userResource = resource({
|
|
114
|
+
// Only fetch when userId is provided
|
|
115
|
+
request: () => {
|
|
116
|
+
const id = this.userId();
|
|
117
|
+
return id ? { id } : undefined; // undefined = don't load
|
|
118
|
+
},
|
|
119
|
+
loader: ({ request }) => this.userService.getById(request.id),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Resource with AbortSignal
|
|
125
|
+
|
|
126
|
+
Handle cancellation for long-running requests:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
protected readonly searchResource = resource({
|
|
130
|
+
request: () => this.query(),
|
|
131
|
+
loader: async ({ request, abortSignal }) => {
|
|
132
|
+
const response = await fetch(`/api/search?q=${request}`, {
|
|
133
|
+
signal: abortSignal, // Automatically cancelled on new request
|
|
134
|
+
});
|
|
135
|
+
return response.json();
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Local State with Resource
|
|
141
|
+
|
|
142
|
+
Combine resource with local state for optimistic updates:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
@Component({ ... })
|
|
146
|
+
export class TodoListComponent {
|
|
147
|
+
protected readonly todosResource = resource({
|
|
148
|
+
request: () => ({}),
|
|
149
|
+
loader: () => this.todoService.getAll(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Local state for optimistic updates
|
|
153
|
+
protected readonly localTodos = linkedSignal(() => this.todosResource.value() ?? []);
|
|
154
|
+
|
|
155
|
+
public async addTodo(text: string): Promise<void> {
|
|
156
|
+
const tempTodo = { id: crypto.randomUUID(), text, completed: false };
|
|
157
|
+
|
|
158
|
+
// Optimistic update
|
|
159
|
+
this.localTodos.update(todos => [...todos, tempTodo]);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const saved = await this.todoService.create({ text });
|
|
163
|
+
// Replace temp with saved
|
|
164
|
+
this.localTodos.update(todos =>
|
|
165
|
+
todos.map(t => t.id === tempTodo.id ? saved : t)
|
|
166
|
+
);
|
|
167
|
+
} catch {
|
|
168
|
+
// Rollback on error
|
|
169
|
+
this.localTodos.update(todos =>
|
|
170
|
+
todos.filter(t => t.id !== tempTodo.id)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Error Handling
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
@Component({
|
|
181
|
+
template: `
|
|
182
|
+
@switch (usersResource.status()) {
|
|
183
|
+
@case ('idle') {
|
|
184
|
+
<p>Enter a search term</p>
|
|
185
|
+
}
|
|
186
|
+
@case ('loading') {
|
|
187
|
+
<app-spinner />
|
|
188
|
+
}
|
|
189
|
+
@case ('error') {
|
|
190
|
+
<div class="error">
|
|
191
|
+
<p>Failed to load users</p>
|
|
192
|
+
<button (click)="usersResource.reload()">Retry</button>
|
|
193
|
+
</div>
|
|
194
|
+
}
|
|
195
|
+
@case ('resolved') {
|
|
196
|
+
@for (user of usersResource.value(); track user.id) {
|
|
197
|
+
<app-user-card [user]="user" />
|
|
198
|
+
} @empty {
|
|
199
|
+
<p>No users found</p>
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
`,
|
|
204
|
+
})
|
|
205
|
+
export class UserSearchComponent {
|
|
206
|
+
protected readonly query = signal('');
|
|
207
|
+
|
|
208
|
+
protected readonly usersResource = resource({
|
|
209
|
+
request: () => {
|
|
210
|
+
const q = this.query();
|
|
211
|
+
return q.length >= 3 ? q : undefined;
|
|
212
|
+
},
|
|
213
|
+
loader: ({ request }) => this.userService.search(request),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Prefetching Data
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
@Component({ ... })
|
|
222
|
+
export class ProductPageComponent {
|
|
223
|
+
public readonly productId = input.required<string>();
|
|
224
|
+
|
|
225
|
+
// Main product data
|
|
226
|
+
protected readonly productResource = resource({
|
|
227
|
+
request: () => this.productId(),
|
|
228
|
+
loader: ({ request }) => this.productService.getById(request),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Prefetch related products (starts loading immediately)
|
|
232
|
+
protected readonly relatedResource = resource({
|
|
233
|
+
request: () => this.productId(),
|
|
234
|
+
loader: ({ request }) => this.productService.getRelated(request),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Anti-patterns
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// BAD: Using resource for mutations
|
|
243
|
+
const saveResource = resource({
|
|
244
|
+
request: () => this.formData(),
|
|
245
|
+
loader: ({ request }) => this.service.save(request), // Mutating!
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// GOOD: Use resource for reads, methods for mutations
|
|
249
|
+
protected readonly userResource = resource({ ... });
|
|
250
|
+
|
|
251
|
+
public async save(): Promise<void> {
|
|
252
|
+
await this.userService.save(this.formData());
|
|
253
|
+
this.userResource.reload(); // Refresh after mutation
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// BAD: Not handling loading state
|
|
258
|
+
@if (userResource.value()) {
|
|
259
|
+
<app-user [user]="userResource.value()!" />
|
|
260
|
+
}
|
|
261
|
+
// Missing loading and error states!
|
|
262
|
+
|
|
263
|
+
// GOOD: Handle all states
|
|
264
|
+
@if (userResource.isLoading()) {
|
|
265
|
+
<app-spinner />
|
|
266
|
+
} @else if (userResource.error()) {
|
|
267
|
+
<app-error />
|
|
268
|
+
} @else if (userResource.hasValue()) {
|
|
269
|
+
<app-user [user]="userResource.value()!" />
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
// BAD: Subscribing to service in component
|
|
274
|
+
ngOnInit() {
|
|
275
|
+
this.userService.getById(this.userId()).subscribe(user => {
|
|
276
|
+
this.user = user;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// GOOD: Use resource for declarative data fetching
|
|
281
|
+
protected readonly userResource = resource({
|
|
282
|
+
request: () => this.userId(),
|
|
283
|
+
loader: ({ request }) => this.userService.getById(request),
|
|
284
|
+
});
|
|
285
|
+
```
|