@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,424 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.component.ts"
|
|
4
|
+
- "**/*.component.html"
|
|
5
|
+
- "**/forms/**/*.ts"
|
|
6
|
+
- "**/*-form.component.ts"
|
|
7
|
+
- "**/*-form/**/*.ts"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Angular Signal-Based Forms
|
|
11
|
+
|
|
12
|
+
Use signals for reactive form state. These patterns leverage `signal()`, `computed()`, and `linkedSignal()` for form management.
|
|
13
|
+
|
|
14
|
+
## Basic Signal Form
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Component, signal, computed } from '@angular/core';
|
|
18
|
+
import { FormsModule } from '@angular/forms';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'app-login',
|
|
22
|
+
imports: [FormsModule],
|
|
23
|
+
template: `
|
|
24
|
+
<form (ngSubmit)="onSubmit()">
|
|
25
|
+
<input
|
|
26
|
+
type="email"
|
|
27
|
+
[ngModel]="email()"
|
|
28
|
+
(ngModelChange)="email.set($event)"
|
|
29
|
+
placeholder="Email"
|
|
30
|
+
/>
|
|
31
|
+
@if (emailError()) {
|
|
32
|
+
<span class="error">{{ emailError() }}</span>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
<input
|
|
36
|
+
type="password"
|
|
37
|
+
[ngModel]="password()"
|
|
38
|
+
(ngModelChange)="password.set($event)"
|
|
39
|
+
placeholder="Password"
|
|
40
|
+
/>
|
|
41
|
+
@if (passwordError()) {
|
|
42
|
+
<span class="error">{{ passwordError() }}</span>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
<button type="submit" [disabled]="!isValid()">Login</button>
|
|
46
|
+
</form>
|
|
47
|
+
`,
|
|
48
|
+
})
|
|
49
|
+
export class LoginComponent {
|
|
50
|
+
// Form state as signals
|
|
51
|
+
protected readonly email = signal('');
|
|
52
|
+
protected readonly password = signal('');
|
|
53
|
+
|
|
54
|
+
// Computed validations
|
|
55
|
+
protected readonly emailError = computed(() => {
|
|
56
|
+
const value = this.email();
|
|
57
|
+
if (!value) return 'Email is required';
|
|
58
|
+
if (!value.includes('@')) return 'Invalid email format';
|
|
59
|
+
return null;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
protected readonly passwordError = computed(() => {
|
|
63
|
+
const value = this.password();
|
|
64
|
+
if (!value) return 'Password is required';
|
|
65
|
+
if (value.length < 8) return 'Password must be at least 8 characters';
|
|
66
|
+
return null;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
protected readonly isValid = computed(() => !this.emailError() && !this.passwordError());
|
|
70
|
+
|
|
71
|
+
public onSubmit(): void {
|
|
72
|
+
if (this.isValid()) {
|
|
73
|
+
console.log({ email: this.email(), password: this.password() });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Form with model() for Two-Way Binding
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { Component, model, computed } from '@angular/core';
|
|
83
|
+
|
|
84
|
+
@Component({
|
|
85
|
+
selector: 'app-user-form',
|
|
86
|
+
template: `
|
|
87
|
+
<input [(ngModel)]="name" placeholder="Name" />
|
|
88
|
+
<input [(ngModel)]="email" placeholder="Email" />
|
|
89
|
+
|
|
90
|
+
<p>Preview: {{ fullInfo() }}</p>
|
|
91
|
+
`,
|
|
92
|
+
})
|
|
93
|
+
export class UserFormComponent {
|
|
94
|
+
// Two-way bindable signals
|
|
95
|
+
public readonly name = model('');
|
|
96
|
+
public readonly email = model('');
|
|
97
|
+
|
|
98
|
+
protected readonly fullInfo = computed(() => `${this.name()} <${this.email()}>`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Parent usage
|
|
102
|
+
// <app-user-form [(name)]="userName" [(email)]="userEmail" />
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Complex Form with Nested Objects
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
interface Address {
|
|
109
|
+
street: string;
|
|
110
|
+
city: string;
|
|
111
|
+
zipCode: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface UserForm {
|
|
115
|
+
name: string;
|
|
116
|
+
email: string;
|
|
117
|
+
address: Address;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Component({
|
|
121
|
+
selector: 'app-user-form',
|
|
122
|
+
template: `
|
|
123
|
+
<input [ngModel]="form().name" (ngModelChange)="updateField('name', $event)" />
|
|
124
|
+
<input [ngModel]="form().email" (ngModelChange)="updateField('email', $event)" />
|
|
125
|
+
|
|
126
|
+
<fieldset>
|
|
127
|
+
<legend>Address</legend>
|
|
128
|
+
<input
|
|
129
|
+
[ngModel]="form().address.street"
|
|
130
|
+
(ngModelChange)="updateAddress('street', $event)"
|
|
131
|
+
/>
|
|
132
|
+
<input
|
|
133
|
+
[ngModel]="form().address.city"
|
|
134
|
+
(ngModelChange)="updateAddress('city', $event)"
|
|
135
|
+
/>
|
|
136
|
+
<input
|
|
137
|
+
[ngModel]="form().address.zipCode"
|
|
138
|
+
(ngModelChange)="updateAddress('zipCode', $event)"
|
|
139
|
+
/>
|
|
140
|
+
</fieldset>
|
|
141
|
+
`,
|
|
142
|
+
})
|
|
143
|
+
export class UserFormComponent {
|
|
144
|
+
protected readonly form = signal<UserForm>({
|
|
145
|
+
name: '',
|
|
146
|
+
email: '',
|
|
147
|
+
address: { street: '', city: '', zipCode: '' },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
protected updateField<K extends keyof UserForm>(field: K, value: UserForm[K]): void {
|
|
151
|
+
this.form.update((f) => ({ ...f, [field]: value }));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected updateAddress<K extends keyof Address>(field: K, value: string): void {
|
|
155
|
+
this.form.update((f) => ({
|
|
156
|
+
...f,
|
|
157
|
+
address: { ...f.address, [field]: value },
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Form Array (Dynamic Fields)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
interface TodoItem {
|
|
167
|
+
id: string;
|
|
168
|
+
text: string;
|
|
169
|
+
completed: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Component({
|
|
173
|
+
selector: 'app-todo-form',
|
|
174
|
+
template: `
|
|
175
|
+
@for (item of items(); track item.id) {
|
|
176
|
+
<div class="todo-item">
|
|
177
|
+
<input
|
|
178
|
+
type="checkbox"
|
|
179
|
+
[checked]="item.completed"
|
|
180
|
+
(change)="toggleComplete(item.id)"
|
|
181
|
+
/>
|
|
182
|
+
<input
|
|
183
|
+
[value]="item.text"
|
|
184
|
+
(input)="updateText(item.id, $event)"
|
|
185
|
+
/>
|
|
186
|
+
<button (click)="removeItem(item.id)">Remove</button>
|
|
187
|
+
</div>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
<button (click)="addItem()">Add Item</button>
|
|
191
|
+
`,
|
|
192
|
+
})
|
|
193
|
+
export class TodoFormComponent {
|
|
194
|
+
protected readonly items = signal<TodoItem[]>([]);
|
|
195
|
+
|
|
196
|
+
protected addItem(): void {
|
|
197
|
+
this.items.update((items) => [
|
|
198
|
+
...items,
|
|
199
|
+
{ id: crypto.randomUUID(), text: '', completed: false },
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected removeItem(id: string): void {
|
|
204
|
+
this.items.update((items) => items.filter((i) => i.id !== id));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected updateText(id: string, event: Event): void {
|
|
208
|
+
const value = (event.target as HTMLInputElement).value;
|
|
209
|
+
this.items.update((items) =>
|
|
210
|
+
items.map((i) => (i.id === id ? { ...i, text: value } : i))
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected toggleComplete(id: string): void {
|
|
215
|
+
this.items.update((items) =>
|
|
216
|
+
items.map((i) => (i.id === id ? { ...i, completed: !i.completed } : i))
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Async Validation
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
@Component({
|
|
226
|
+
selector: 'app-signup',
|
|
227
|
+
template: `
|
|
228
|
+
<input
|
|
229
|
+
[ngModel]="username()"
|
|
230
|
+
(ngModelChange)="onUsernameChange($event)"
|
|
231
|
+
/>
|
|
232
|
+
@if (isCheckingUsername()) {
|
|
233
|
+
<span>Checking...</span>
|
|
234
|
+
}
|
|
235
|
+
@if (usernameError()) {
|
|
236
|
+
<span class="error">{{ usernameError() }}</span>
|
|
237
|
+
}
|
|
238
|
+
@if (usernameAvailable()) {
|
|
239
|
+
<span class="success">Username available!</span>
|
|
240
|
+
}
|
|
241
|
+
`,
|
|
242
|
+
})
|
|
243
|
+
export class SignupComponent {
|
|
244
|
+
private readonly userService = inject(UserService);
|
|
245
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
246
|
+
|
|
247
|
+
protected readonly username = signal('');
|
|
248
|
+
protected readonly isCheckingUsername = signal(false);
|
|
249
|
+
protected readonly usernameAvailable = signal<boolean | null>(null);
|
|
250
|
+
|
|
251
|
+
protected readonly usernameError = computed(() => {
|
|
252
|
+
const value = this.username();
|
|
253
|
+
if (!value) return 'Username is required';
|
|
254
|
+
if (value.length < 3) return 'Username must be at least 3 characters';
|
|
255
|
+
if (this.usernameAvailable() === false) return 'Username is taken';
|
|
256
|
+
return null;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
protected onUsernameChange(value: string): void {
|
|
260
|
+
this.username.set(value);
|
|
261
|
+
this.usernameAvailable.set(null);
|
|
262
|
+
|
|
263
|
+
if (value.length >= 3) {
|
|
264
|
+
this.checkUsernameAvailability(value);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private checkUsernameAvailability(username: string): void {
|
|
269
|
+
this.isCheckingUsername.set(true);
|
|
270
|
+
|
|
271
|
+
this.userService
|
|
272
|
+
.checkUsername(username)
|
|
273
|
+
.pipe(
|
|
274
|
+
debounceTime(300),
|
|
275
|
+
takeUntilDestroyed(this.destroyRef),
|
|
276
|
+
)
|
|
277
|
+
.subscribe({
|
|
278
|
+
next: (available) => {
|
|
279
|
+
this.usernameAvailable.set(available);
|
|
280
|
+
this.isCheckingUsername.set(false);
|
|
281
|
+
},
|
|
282
|
+
error: () => {
|
|
283
|
+
this.isCheckingUsername.set(false);
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Form Submission with Loading State
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
@Component({
|
|
294
|
+
selector: 'app-contact-form',
|
|
295
|
+
template: `
|
|
296
|
+
<form (ngSubmit)="onSubmit()">
|
|
297
|
+
<input [ngModel]="name()" (ngModelChange)="name.set($event)" />
|
|
298
|
+
<textarea [ngModel]="message()" (ngModelChange)="message.set($event)"></textarea>
|
|
299
|
+
|
|
300
|
+
@if (submitError()) {
|
|
301
|
+
<div class="error">{{ submitError() }}</div>
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
<button type="submit" [disabled]="isSubmitting() || !isValid()">
|
|
305
|
+
@if (isSubmitting()) {
|
|
306
|
+
Sending...
|
|
307
|
+
} @else {
|
|
308
|
+
Send Message
|
|
309
|
+
}
|
|
310
|
+
</button>
|
|
311
|
+
</form>
|
|
312
|
+
`,
|
|
313
|
+
})
|
|
314
|
+
export class ContactFormComponent {
|
|
315
|
+
private readonly contactService = inject(ContactService);
|
|
316
|
+
|
|
317
|
+
protected readonly name = signal('');
|
|
318
|
+
protected readonly message = signal('');
|
|
319
|
+
protected readonly isSubmitting = signal(false);
|
|
320
|
+
protected readonly submitError = signal<string | null>(null);
|
|
321
|
+
|
|
322
|
+
protected readonly isValid = computed(() => this.name().length > 0 && this.message().length > 0);
|
|
323
|
+
|
|
324
|
+
public async onSubmit(): Promise<void> {
|
|
325
|
+
if (!this.isValid() || this.isSubmitting()) return;
|
|
326
|
+
|
|
327
|
+
this.isSubmitting.set(true);
|
|
328
|
+
this.submitError.set(null);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
await this.contactService.send({
|
|
332
|
+
name: this.name(),
|
|
333
|
+
message: this.message(),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Reset form
|
|
337
|
+
this.name.set('');
|
|
338
|
+
this.message.set('');
|
|
339
|
+
} catch (error) {
|
|
340
|
+
this.submitError.set('Failed to send message. Please try again.');
|
|
341
|
+
} finally {
|
|
342
|
+
this.isSubmitting.set(false);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Form Reset with linkedSignal
|
|
349
|
+
|
|
350
|
+
Use `linkedSignal` when form values should reset based on external state:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
@Component({
|
|
354
|
+
selector: 'app-user-editor',
|
|
355
|
+
template: `
|
|
356
|
+
<input [ngModel]="email()" (ngModelChange)="email.set($event)" />
|
|
357
|
+
<button (click)="save()">Save</button>
|
|
358
|
+
`,
|
|
359
|
+
})
|
|
360
|
+
export class UserEditorComponent {
|
|
361
|
+
// When selectedUser changes, email resets to user's email
|
|
362
|
+
// But user can still edit it manually
|
|
363
|
+
public readonly selectedUser = input.required<User>();
|
|
364
|
+
|
|
365
|
+
protected readonly email = linkedSignal(() => this.selectedUser().email);
|
|
366
|
+
|
|
367
|
+
protected save(): void {
|
|
368
|
+
// email() contains the current (possibly edited) value
|
|
369
|
+
console.log('Saving:', this.email());
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## When to Use Reactive Forms Instead
|
|
375
|
+
|
|
376
|
+
Use traditional `FormGroup`/`FormControl` when you need:
|
|
377
|
+
- Complex cross-field validation
|
|
378
|
+
- Dynamic form generation from schema
|
|
379
|
+
- Integration with third-party form libraries
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// For complex forms, ReactiveFormsModule is still valid
|
|
383
|
+
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
384
|
+
|
|
385
|
+
// But prefer signal-based forms for new, simpler forms
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Anti-patterns
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// BAD: Using BehaviorSubject for form state
|
|
392
|
+
private readonly email$ = new BehaviorSubject('');
|
|
393
|
+
|
|
394
|
+
// GOOD: Use signals
|
|
395
|
+
protected readonly email = signal('');
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
// BAD: Manual subscription for validation
|
|
399
|
+
ngOnInit() {
|
|
400
|
+
this.email$.subscribe(value => {
|
|
401
|
+
this.emailError = this.validateEmail(value);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// GOOD: Use computed
|
|
406
|
+
protected readonly emailError = computed(() => this.validateEmail(this.email()));
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
// BAD: Forgetting to handle loading state
|
|
410
|
+
public onSubmit(): void {
|
|
411
|
+
this.service.save(this.form()).subscribe(); // No loading indicator!
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// GOOD: Track submission state
|
|
415
|
+
protected readonly isSubmitting = signal(false);
|
|
416
|
+
public async onSubmit(): Promise<void> {
|
|
417
|
+
this.isSubmitting.set(true);
|
|
418
|
+
try {
|
|
419
|
+
await firstValueFrom(this.service.save(this.form()));
|
|
420
|
+
} finally {
|
|
421
|
+
this.isSubmitting.set(false);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|