@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,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.pipe.ts"
|
|
4
|
+
- "**/*.directive.ts"
|
|
5
|
+
- "**/pipes/**"
|
|
6
|
+
- "**/directives/**"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Angular Pipes & Directives
|
|
10
|
+
|
|
11
|
+
## Custom Pipes
|
|
12
|
+
|
|
13
|
+
### Pure Pipe (default, memoized)
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// pipes/time-ago.pipe.ts
|
|
17
|
+
import { Pipe, PipeTransform } from '@angular/core';
|
|
18
|
+
|
|
19
|
+
@Pipe({
|
|
20
|
+
name: 'timeAgo',
|
|
21
|
+
})
|
|
22
|
+
export class TimeAgoPipe implements PipeTransform {
|
|
23
|
+
public transform(value: Date | string | number): string {
|
|
24
|
+
const date = new Date(value);
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
27
|
+
|
|
28
|
+
if (seconds < 60) return 'just now';
|
|
29
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
|
|
30
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
|
|
31
|
+
if (seconds < 2592000) return `${Math.floor(seconds / 86400)} days ago`;
|
|
32
|
+
|
|
33
|
+
return date.toLocaleDateString();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Usage: {{ createdAt | timeAgo }}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Pipe with Parameters
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// pipes/truncate.pipe.ts
|
|
44
|
+
@Pipe({
|
|
45
|
+
name: 'truncate',
|
|
46
|
+
})
|
|
47
|
+
export class TruncatePipe implements PipeTransform {
|
|
48
|
+
public transform(
|
|
49
|
+
value: string,
|
|
50
|
+
limit: number = 100,
|
|
51
|
+
trail: string = '...'
|
|
52
|
+
): string {
|
|
53
|
+
if (!value || value.length <= limit) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
return value.substring(0, limit).trim() + trail;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Usage: {{ description | truncate:50:'…' }}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Filter Pipe
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// pipes/filter.pipe.ts
|
|
67
|
+
@Pipe({
|
|
68
|
+
name: 'filter',
|
|
69
|
+
})
|
|
70
|
+
export class FilterPipe implements PipeTransform {
|
|
71
|
+
public transform<T>(
|
|
72
|
+
items: T[],
|
|
73
|
+
field: keyof T,
|
|
74
|
+
value: unknown
|
|
75
|
+
): T[] {
|
|
76
|
+
if (!items || !field || value === undefined) {
|
|
77
|
+
return items;
|
|
78
|
+
}
|
|
79
|
+
return items.filter(item => item[field] === value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Usage: @for (user of users() | filter:'status':'active'; track user.id)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Safe HTML Pipe
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// pipes/safe-html.pipe.ts
|
|
90
|
+
import { Pipe, PipeTransform, inject } from '@angular/core';
|
|
91
|
+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
92
|
+
|
|
93
|
+
@Pipe({
|
|
94
|
+
name: 'safeHtml',
|
|
95
|
+
})
|
|
96
|
+
export class SafeHtmlPipe implements PipeTransform {
|
|
97
|
+
private readonly sanitizer = inject(DomSanitizer);
|
|
98
|
+
|
|
99
|
+
public transform(value: string): SafeHtml {
|
|
100
|
+
return this.sanitizer.bypassSecurityTrustHtml(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Usage: <div [innerHTML]="content | safeHtml"></div>
|
|
105
|
+
// ⚠️ Only use with trusted content!
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Custom Directives
|
|
109
|
+
|
|
110
|
+
### Attribute Directive
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// directives/highlight.directive.ts
|
|
114
|
+
import { Directive, ElementRef, HostListener, input, inject } from '@angular/core';
|
|
115
|
+
|
|
116
|
+
@Directive({
|
|
117
|
+
selector: '[appHighlight]',
|
|
118
|
+
})
|
|
119
|
+
export class HighlightDirective {
|
|
120
|
+
private readonly el = inject(ElementRef);
|
|
121
|
+
|
|
122
|
+
public readonly appHighlight = input<string>('#ffff00');
|
|
123
|
+
public readonly defaultColor = input<string>('transparent');
|
|
124
|
+
|
|
125
|
+
@HostListener('mouseenter')
|
|
126
|
+
public onMouseEnter(): void {
|
|
127
|
+
this.highlight(this.appHighlight() || '#ffff00');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@HostListener('mouseleave')
|
|
131
|
+
public onMouseLeave(): void {
|
|
132
|
+
this.highlight(this.defaultColor());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private highlight(color: string): void {
|
|
136
|
+
this.el.nativeElement.style.backgroundColor = color;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Usage: <p appHighlight="#e0e0e0">Hover me</p>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Click Outside Directive
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// directives/click-outside.directive.ts
|
|
147
|
+
import { Directive, ElementRef, output, inject } from '@angular/core';
|
|
148
|
+
import { fromEvent } from 'rxjs';
|
|
149
|
+
import { filter } from 'rxjs/operators';
|
|
150
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
151
|
+
|
|
152
|
+
@Directive({
|
|
153
|
+
selector: '[appClickOutside]',
|
|
154
|
+
})
|
|
155
|
+
export class ClickOutsideDirective {
|
|
156
|
+
private readonly el = inject(ElementRef);
|
|
157
|
+
|
|
158
|
+
public readonly appClickOutside = output<void>();
|
|
159
|
+
|
|
160
|
+
constructor() {
|
|
161
|
+
fromEvent<MouseEvent>(document, 'click')
|
|
162
|
+
.pipe(
|
|
163
|
+
filter(event => !this.el.nativeElement.contains(event.target)),
|
|
164
|
+
takeUntilDestroyed(),
|
|
165
|
+
)
|
|
166
|
+
.subscribe(() => this.appClickOutside.emit());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Usage: <div appClickOutside (appClickOutside)="closeDropdown()">
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Auto Focus Directive
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// directives/auto-focus.directive.ts
|
|
177
|
+
import { Directive, ElementRef, AfterViewInit, input, inject } from '@angular/core';
|
|
178
|
+
|
|
179
|
+
@Directive({
|
|
180
|
+
selector: '[appAutoFocus]',
|
|
181
|
+
})
|
|
182
|
+
export class AutoFocusDirective implements AfterViewInit {
|
|
183
|
+
private readonly el = inject(ElementRef);
|
|
184
|
+
|
|
185
|
+
public readonly appAutoFocus = input<boolean>(true);
|
|
186
|
+
|
|
187
|
+
public ngAfterViewInit(): void {
|
|
188
|
+
if (this.appAutoFocus()) {
|
|
189
|
+
setTimeout(() => this.el.nativeElement.focus(), 0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Usage: <input appAutoFocus />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Structural Directive
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// directives/permission.directive.ts
|
|
201
|
+
import { Directive, TemplateRef, ViewContainerRef, input, effect, inject } from '@angular/core';
|
|
202
|
+
import { AuthService } from '../services/auth.service';
|
|
203
|
+
|
|
204
|
+
@Directive({
|
|
205
|
+
selector: '[appHasPermission]',
|
|
206
|
+
})
|
|
207
|
+
export class HasPermissionDirective {
|
|
208
|
+
private readonly templateRef = inject(TemplateRef<unknown>);
|
|
209
|
+
private readonly viewContainer = inject(ViewContainerRef);
|
|
210
|
+
private readonly authService = inject(AuthService);
|
|
211
|
+
|
|
212
|
+
public readonly appHasPermission = input.required<string | string[]>();
|
|
213
|
+
|
|
214
|
+
private hasView = false;
|
|
215
|
+
|
|
216
|
+
constructor() {
|
|
217
|
+
effect(() => {
|
|
218
|
+
const permissions = this.appHasPermission();
|
|
219
|
+
const permissionArray = Array.isArray(permissions) ? permissions : [permissions];
|
|
220
|
+
const hasPermission = this.authService.hasAnyPermission(permissionArray);
|
|
221
|
+
|
|
222
|
+
if (hasPermission && !this.hasView) {
|
|
223
|
+
this.viewContainer.createEmbeddedView(this.templateRef);
|
|
224
|
+
this.hasView = true;
|
|
225
|
+
} else if (!hasPermission && this.hasView) {
|
|
226
|
+
this.viewContainer.clear();
|
|
227
|
+
this.hasView = false;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage: <button *appHasPermission="'users.create'">Create User</button>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Debounce Input Directive
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// directives/debounce-input.directive.ts
|
|
240
|
+
import { Directive, ElementRef, output, input, inject, OnInit, DestroyRef } from '@angular/core';
|
|
241
|
+
import { fromEvent, debounceTime, distinctUntilChanged, map } from 'rxjs';
|
|
242
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
243
|
+
|
|
244
|
+
@Directive({
|
|
245
|
+
selector: 'input[appDebounce]',
|
|
246
|
+
})
|
|
247
|
+
export class DebounceInputDirective implements OnInit {
|
|
248
|
+
private readonly el = inject(ElementRef<HTMLInputElement>);
|
|
249
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
250
|
+
|
|
251
|
+
public readonly appDebounce = input<number>(300);
|
|
252
|
+
public readonly debounceValue = output<string>();
|
|
253
|
+
|
|
254
|
+
public ngOnInit(): void {
|
|
255
|
+
fromEvent<Event>(this.el.nativeElement, 'input')
|
|
256
|
+
.pipe(
|
|
257
|
+
map(event => (event.target as HTMLInputElement).value),
|
|
258
|
+
debounceTime(this.appDebounce()),
|
|
259
|
+
distinctUntilChanged(),
|
|
260
|
+
takeUntilDestroyed(this.destroyRef),
|
|
261
|
+
)
|
|
262
|
+
.subscribe(value => this.debounceValue.emit(value));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Usage: <input appDebounce [appDebounce]="500" (debounceValue)="onSearch($event)" />
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Composition
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// components/user-list.component.ts
|
|
273
|
+
@Component({
|
|
274
|
+
selector: 'app-user-list',
|
|
275
|
+
imports: [
|
|
276
|
+
TimeAgoPipe,
|
|
277
|
+
TruncatePipe,
|
|
278
|
+
FilterPipe,
|
|
279
|
+
HighlightDirective,
|
|
280
|
+
HasPermissionDirective,
|
|
281
|
+
],
|
|
282
|
+
template: `
|
|
283
|
+
@for (user of users() | filter:'status':'active'; track user.id) {
|
|
284
|
+
<div appHighlight="#f0f0f0">
|
|
285
|
+
<h3>{{ user.name }}</h3>
|
|
286
|
+
<p>{{ user.bio | truncate:100 }}</p>
|
|
287
|
+
<span>{{ user.createdAt | timeAgo }}</span>
|
|
288
|
+
|
|
289
|
+
<button *appHasPermission="'users.delete'">Delete</button>
|
|
290
|
+
</div>
|
|
291
|
+
}
|
|
292
|
+
`,
|
|
293
|
+
})
|
|
294
|
+
export class UserListComponent {
|
|
295
|
+
protected readonly users = input.required<User[]>();
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Anti-patterns
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// BAD: Impure pipe for filtering (causes performance issues)
|
|
303
|
+
@Pipe({ name: 'filter', pure: false })
|
|
304
|
+
|
|
305
|
+
// GOOD: Use pure pipe (default) + signal for reactivity
|
|
306
|
+
@Pipe({ name: 'filter' })
|
|
307
|
+
// And update source data via signals
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
// BAD: Direct DOM manipulation
|
|
311
|
+
this.el.nativeElement.innerHTML = '<b>text</b>';
|
|
312
|
+
|
|
313
|
+
// GOOD: Use Renderer2 or Angular bindings
|
|
314
|
+
@HostBinding('innerHTML') content = '<b>text</b>';
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
// BAD: Adding standalone: true (it's the default since Angular 19)
|
|
318
|
+
@Pipe({ name: 'myPipe', standalone: true })
|
|
319
|
+
|
|
320
|
+
// GOOD: Omit standalone (defaults to true)
|
|
321
|
+
@Pipe({ name: 'myPipe' })
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
// BAD: Using @Input() in directives
|
|
325
|
+
@Directive({ selector: '[appHighlight]' })
|
|
326
|
+
export class HighlightDirective {
|
|
327
|
+
@Input() appHighlight: string; // Use input() instead
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// GOOD: Use signal inputs
|
|
331
|
+
@Directive({ selector: '[appHighlight]' })
|
|
332
|
+
export class HighlightDirective {
|
|
333
|
+
public readonly appHighlight = input<string>();
|
|
334
|
+
}
|
|
335
|
+
```
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ngrx-slice
|
|
3
|
+
description: Generate a complete NgRx store slice with actions, reducer, effects, selectors, and Entity Adapter
|
|
4
|
+
argument-hint: <domain> [--entity <EntityName>]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Generate NgRx Store Slice
|
|
8
|
+
|
|
9
|
+
Generate a complete NgRx store slice following best practices with Entity Adapter.
|
|
10
|
+
|
|
11
|
+
## Syntax
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/ngrx-slice <domain> [--entity <EntityName>]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
/ngrx-slice users
|
|
21
|
+
/ngrx-slice products --entity Product
|
|
22
|
+
/ngrx-slice orders --entity Order
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Generated Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
libs/<domain>/data-access/src/lib/+state/
|
|
29
|
+
├── <domain>.actions.ts # Action groups with createActionGroup
|
|
30
|
+
├── <domain>.reducer.ts # Reducer with Entity Adapter
|
|
31
|
+
├── <domain>.effects.ts # Functional effects
|
|
32
|
+
├── <domain>.selectors.ts # Memoized selectors
|
|
33
|
+
├── <domain>.state.ts # State interface
|
|
34
|
+
└── <domain>.adapter.ts # Entity Adapter config
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## File Templates
|
|
38
|
+
|
|
39
|
+
### 1. State Interface (`<domain>.state.ts`)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { EntityState } from '@ngrx/entity';
|
|
43
|
+
|
|
44
|
+
export interface <Entity> {
|
|
45
|
+
id: string;
|
|
46
|
+
// Add entity properties
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface <Entity>State extends EntityState<<Entity>> {
|
|
50
|
+
selectedId: string | null;
|
|
51
|
+
loading: boolean;
|
|
52
|
+
error: string | null;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Entity Adapter (`<domain>.adapter.ts`)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { createEntityAdapter, EntityAdapter } from '@ngrx/entity';
|
|
60
|
+
import { <Entity>, <Entity>State } from './<domain>.state';
|
|
61
|
+
|
|
62
|
+
export const <entity>Adapter: EntityAdapter<<Entity>> = createEntityAdapter<<Entity>>({
|
|
63
|
+
selectId: (<entity>) => <entity>.id,
|
|
64
|
+
sortComparer: (a, b) => a.name.localeCompare(b.name),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const initial<Entity>State: <Entity>State = <entity>Adapter.getInitialState({
|
|
68
|
+
selectedId: null,
|
|
69
|
+
loading: false,
|
|
70
|
+
error: null,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Actions (`<domain>.actions.ts`)
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { createActionGroup, emptyProps, props } from '@ngrx/store';
|
|
78
|
+
import { Update } from '@ngrx/entity';
|
|
79
|
+
import { <Entity> } from './<domain>.state';
|
|
80
|
+
|
|
81
|
+
export const <Entity>Actions = createActionGroup({
|
|
82
|
+
source: '<Entities>',
|
|
83
|
+
events: {
|
|
84
|
+
// Load
|
|
85
|
+
'Load <Entities>': emptyProps(),
|
|
86
|
+
'Load <Entities> Success': props<{ <entities>: <Entity>[] }>(),
|
|
87
|
+
'Load <Entities> Failure': props<{ error: string }>(),
|
|
88
|
+
|
|
89
|
+
// Load Single
|
|
90
|
+
'Load <Entity>': props<{ id: string }>(),
|
|
91
|
+
'Load <Entity> Success': props<{ <entity>: <Entity> }>(),
|
|
92
|
+
'Load <Entity> Failure': props<{ error: string }>(),
|
|
93
|
+
|
|
94
|
+
// Create
|
|
95
|
+
'Create <Entity>': props<{ <entity>: Omit<<Entity>, 'id'> }>(),
|
|
96
|
+
'Create <Entity> Success': props<{ <entity>: <Entity> }>(),
|
|
97
|
+
'Create <Entity> Failure': props<{ error: string }>(),
|
|
98
|
+
|
|
99
|
+
// Update
|
|
100
|
+
'Update <Entity>': props<{ update: Update<<Entity>> }>(),
|
|
101
|
+
'Update <Entity> Success': props<{ <entity>: <Entity> }>(),
|
|
102
|
+
'Update <Entity> Failure': props<{ error: string }>(),
|
|
103
|
+
|
|
104
|
+
// Delete
|
|
105
|
+
'Delete <Entity>': props<{ id: string }>(),
|
|
106
|
+
'Delete <Entity> Success': props<{ id: string }>(),
|
|
107
|
+
'Delete <Entity> Failure': props<{ error: string }>(),
|
|
108
|
+
|
|
109
|
+
// Selection
|
|
110
|
+
'Select <Entity>': props<{ id: string }>(),
|
|
111
|
+
'Clear Selection': emptyProps(),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 4. Reducer (`<domain>.reducer.ts`)
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { createReducer, on } from '@ngrx/store';
|
|
120
|
+
import { <Entity>Actions } from './<domain>.actions';
|
|
121
|
+
import { <entity>Adapter, initial<Entity>State } from './<domain>.adapter';
|
|
122
|
+
|
|
123
|
+
export const <entity>Reducer = createReducer(
|
|
124
|
+
initial<Entity>State,
|
|
125
|
+
|
|
126
|
+
// Load All
|
|
127
|
+
on(<Entity>Actions.load<Entities>, (state) => ({
|
|
128
|
+
...state,
|
|
129
|
+
loading: true,
|
|
130
|
+
error: null,
|
|
131
|
+
})),
|
|
132
|
+
on(<Entity>Actions.load<Entities>Success, (state, { <entities> }) =>
|
|
133
|
+
<entity>Adapter.setAll(<entities>, { ...state, loading: false })
|
|
134
|
+
),
|
|
135
|
+
on(<Entity>Actions.load<Entities>Failure, (state, { error }) => ({
|
|
136
|
+
...state,
|
|
137
|
+
loading: false,
|
|
138
|
+
error,
|
|
139
|
+
})),
|
|
140
|
+
|
|
141
|
+
// Load Single
|
|
142
|
+
on(<Entity>Actions.load<Entity>Success, (state, { <entity> }) =>
|
|
143
|
+
<entity>Adapter.upsertOne(<entity>, state)
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
// Create
|
|
147
|
+
on(<Entity>Actions.create<Entity>Success, (state, { <entity> }) =>
|
|
148
|
+
<entity>Adapter.addOne(<entity>, state)
|
|
149
|
+
),
|
|
150
|
+
|
|
151
|
+
// Update
|
|
152
|
+
on(<Entity>Actions.update<Entity>Success, (state, { <entity> }) =>
|
|
153
|
+
<entity>Adapter.updateOne({ id: <entity>.id, changes: <entity> }, state)
|
|
154
|
+
),
|
|
155
|
+
|
|
156
|
+
// Delete
|
|
157
|
+
on(<Entity>Actions.delete<Entity>Success, (state, { id }) =>
|
|
158
|
+
<entity>Adapter.removeOne(id, state)
|
|
159
|
+
),
|
|
160
|
+
|
|
161
|
+
// Selection
|
|
162
|
+
on(<Entity>Actions.select<Entity>, (state, { id }) => ({
|
|
163
|
+
...state,
|
|
164
|
+
selectedId: id,
|
|
165
|
+
})),
|
|
166
|
+
on(<Entity>Actions.clearSelection, (state) => ({
|
|
167
|
+
...state,
|
|
168
|
+
selectedId: null,
|
|
169
|
+
})),
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 5. Selectors (`<domain>.selectors.ts`)
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
177
|
+
import { <Entity>State } from './<domain>.state';
|
|
178
|
+
import { <entity>Adapter } from './<domain>.adapter';
|
|
179
|
+
|
|
180
|
+
export const select<Entity>State = createFeatureSelector<<Entity>State>('<entities>');
|
|
181
|
+
|
|
182
|
+
const { selectAll, selectEntities, selectIds, selectTotal } =
|
|
183
|
+
<entity>Adapter.getSelectors();
|
|
184
|
+
|
|
185
|
+
export const selectAll<Entities> = createSelector(select<Entity>State, selectAll);
|
|
186
|
+
|
|
187
|
+
export const select<Entity>Entities = createSelector(select<Entity>State, selectEntities);
|
|
188
|
+
|
|
189
|
+
export const select<Entity>Ids = createSelector(select<Entity>State, selectIds);
|
|
190
|
+
|
|
191
|
+
export const select<Entity>Total = createSelector(select<Entity>State, selectTotal);
|
|
192
|
+
|
|
193
|
+
export const select<Entities>Loading = createSelector(
|
|
194
|
+
select<Entity>State,
|
|
195
|
+
(state) => state.loading
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
export const select<Entities>Error = createSelector(
|
|
199
|
+
select<Entity>State,
|
|
200
|
+
(state) => state.error
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
export const selectSelected<Entity>Id = createSelector(
|
|
204
|
+
select<Entity>State,
|
|
205
|
+
(state) => state.selectedId
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const selectSelected<Entity> = createSelector(
|
|
209
|
+
select<Entity>Entities,
|
|
210
|
+
selectSelected<Entity>Id,
|
|
211
|
+
(entities, selectedId) => (selectedId ? entities[selectedId] : null)
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 6. Effects (`<domain>.effects.ts`)
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { inject } from '@angular/core';
|
|
219
|
+
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
220
|
+
import { catchError, exhaustMap, map, of } from 'rxjs';
|
|
221
|
+
import { <Entity>Actions } from './<domain>.actions';
|
|
222
|
+
import { <Entity>Service } from '../services/<domain>.service';
|
|
223
|
+
|
|
224
|
+
export const load<Entities>$ = createEffect(
|
|
225
|
+
(
|
|
226
|
+
actions$ = inject(Actions),
|
|
227
|
+
<entity>Service = inject(<Entity>Service)
|
|
228
|
+
) =>
|
|
229
|
+
actions$.pipe(
|
|
230
|
+
ofType(<Entity>Actions.load<Entities>),
|
|
231
|
+
exhaustMap(() =>
|
|
232
|
+
<entity>Service.getAll().pipe(
|
|
233
|
+
map((<entities>) => <Entity>Actions.load<Entities>Success({ <entities> })),
|
|
234
|
+
catchError((error) =>
|
|
235
|
+
of(<Entity>Actions.load<Entities>Failure({ error: error.message }))
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
{ functional: true }
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
export const load<Entity>$ = createEffect(
|
|
244
|
+
(
|
|
245
|
+
actions$ = inject(Actions),
|
|
246
|
+
<entity>Service = inject(<Entity>Service)
|
|
247
|
+
) =>
|
|
248
|
+
actions$.pipe(
|
|
249
|
+
ofType(<Entity>Actions.load<Entity>),
|
|
250
|
+
exhaustMap(({ id }) =>
|
|
251
|
+
<entity>Service.getById(id).pipe(
|
|
252
|
+
map((<entity>) => <Entity>Actions.load<Entity>Success({ <entity> })),
|
|
253
|
+
catchError((error) =>
|
|
254
|
+
of(<Entity>Actions.load<Entity>Failure({ error: error.message }))
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
),
|
|
259
|
+
{ functional: true }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
export const create<Entity>$ = createEffect(
|
|
263
|
+
(
|
|
264
|
+
actions$ = inject(Actions),
|
|
265
|
+
<entity>Service = inject(<Entity>Service)
|
|
266
|
+
) =>
|
|
267
|
+
actions$.pipe(
|
|
268
|
+
ofType(<Entity>Actions.create<Entity>),
|
|
269
|
+
exhaustMap(({ <entity> }) =>
|
|
270
|
+
<entity>Service.create(<entity>).pipe(
|
|
271
|
+
map((<entity>) => <Entity>Actions.create<Entity>Success({ <entity> })),
|
|
272
|
+
catchError((error) =>
|
|
273
|
+
of(<Entity>Actions.create<Entity>Failure({ error: error.message }))
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
),
|
|
278
|
+
{ functional: true }
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
export const update<Entity>$ = createEffect(
|
|
282
|
+
(
|
|
283
|
+
actions$ = inject(Actions),
|
|
284
|
+
<entity>Service = inject(<Entity>Service)
|
|
285
|
+
) =>
|
|
286
|
+
actions$.pipe(
|
|
287
|
+
ofType(<Entity>Actions.update<Entity>),
|
|
288
|
+
exhaustMap(({ update }) =>
|
|
289
|
+
<entity>Service.update(update.id as string, update.changes).pipe(
|
|
290
|
+
map((<entity>) => <Entity>Actions.update<Entity>Success({ <entity> })),
|
|
291
|
+
catchError((error) =>
|
|
292
|
+
of(<Entity>Actions.update<Entity>Failure({ error: error.message }))
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
),
|
|
297
|
+
{ functional: true }
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
export const delete<Entity>$ = createEffect(
|
|
301
|
+
(
|
|
302
|
+
actions$ = inject(Actions),
|
|
303
|
+
<entity>Service = inject(<Entity>Service)
|
|
304
|
+
) =>
|
|
305
|
+
actions$.pipe(
|
|
306
|
+
ofType(<Entity>Actions.delete<Entity>),
|
|
307
|
+
exhaustMap(({ id }) =>
|
|
308
|
+
<entity>Service.delete(id).pipe(
|
|
309
|
+
map(() => <Entity>Actions.delete<Entity>Success({ id })),
|
|
310
|
+
catchError((error) =>
|
|
311
|
+
of(<Entity>Actions.delete<Entity>Failure({ error: error.message }))
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
),
|
|
316
|
+
{ functional: true }
|
|
317
|
+
);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Execution Steps
|
|
321
|
+
|
|
322
|
+
1. **Parse Arguments**
|
|
323
|
+
- Extract domain name (e.g., "users")
|
|
324
|
+
- Extract entity name (e.g., "User") or derive from domain
|
|
325
|
+
|
|
326
|
+
2. **Create Files**
|
|
327
|
+
- Generate all 6 files with proper naming
|
|
328
|
+
- Replace placeholders with actual names
|
|
329
|
+
|
|
330
|
+
3. **Update Public API**
|
|
331
|
+
- Export all from `index.ts`
|
|
332
|
+
|
|
333
|
+
4. **Provide Registration**
|
|
334
|
+
- Show how to register in `app.config.ts`
|
|
335
|
+
|
|
336
|
+
## Output
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// app.config.ts
|
|
340
|
+
import { provideState } from '@ngrx/store';
|
|
341
|
+
import { provideEffects } from '@ngrx/effects';
|
|
342
|
+
import { <entity>Reducer } from '@app/<domain>/data-access';
|
|
343
|
+
import * as <entity>Effects from '@app/<domain>/data-access';
|
|
344
|
+
|
|
345
|
+
export const appConfig: ApplicationConfig = {
|
|
346
|
+
providers: [
|
|
347
|
+
provideStore(),
|
|
348
|
+
provideState('<entities>', <entity>Reducer),
|
|
349
|
+
provideEffects(<entity>Effects),
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Placeholders
|
|
355
|
+
|
|
356
|
+
| Placeholder | Example (users) |
|
|
357
|
+
|-------------|-----------------|
|
|
358
|
+
| `<domain>` | users |
|
|
359
|
+
| `<entity>` | user |
|
|
360
|
+
| `<Entity>` | User |
|
|
361
|
+
| `<entities>` | users |
|
|
362
|
+
| `<Entities>` | Users |
|