本文へ移動
cccskills
無料GitHub で公開

angular-forms

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

インストール方法を見る

含まれるファイル(1)

  • SKILL.md12.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Angular Forms

Two paradigms: Reactive Forms (preferred for non-trivial) and Template-driven ([(ngModel)]-based, simpler for tiny forms). Pick what the project uses; default to Reactive for new code.

Detection

Marker (in template imports / *.module.ts imports)Approach
ReactiveFormsModuleReactive Forms (preferred)
FormsModule (without ReactiveFormsModule)Template-driven only
BothMixed; mirror existing style per area

Reactive Forms (preferred)

Basic typed form (Angular 14+)

import { Component, inject } from '@angular/core';
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <label>
        Email
        <input type="email" formControlName="email" [attr.aria-invalid]="emailControl.invalid && emailControl.touched" />
        @if (emailControl.invalid && emailControl.touched) {
          @if (emailControl.errors?.['required']) {
            <p role="alert">Email is required</p>
          } @else if (emailControl.errors?.['email']) {
            <p role="alert">Invalid email format</p>
          }
        }
      </label>

      <label>
        Password
        <input type="password" formControlName="password" />
        @if (passwordControl.invalid && passwordControl.touched) {
          <p role="alert">Password must be at least 8 characters</p>
        }
      </label>

      <button type="submit" [disabled]="loginForm.invalid || submitting()">
        {{ submitting() ? 'Logging in...' : 'Log in' }}
      </button>
    </form>
  `,
})
export class LoginComponent {
  loginForm = new FormGroup({
    email: new FormControl('', {
      validators: [Validators.required, Validators.email],
      nonNullable: true,
    }),
    password: new FormControl('', {
      validators: [Validators.required, Validators.minLength(8)],
      nonNullable: true,
    }),
  });

  submitting = signal(false);

  get emailControl() { return this.loginForm.controls.email; }
  get passwordControl() { return this.loginForm.controls.password; }

  async onSubmit() {
    if (this.loginForm.invalid) return;
    this.submitting.set(true);
    try {
      const { email, password } = this.loginForm.getRawValue();   // typed: { email: string; password: string }
      await this.authService.login({ email, password });
      this.router.navigate(['/dashboard']);
    } catch (err) {
      this.loginForm.setErrors({ serverError: 'Login failed' });
    } finally {
      this.submitting.set(false);
    }
  }
}

Why typed forms (Angular 14+)

const form = new FormGroup({
  email: new FormControl('', { nonNullable: true }),    // FormControl<string>
  age: new FormControl<number | null>(null),             // FormControl<number | null>
});

form.value;                          // { email?: string; age?: number | null }
form.getRawValue();                  // { email: string; age: number | null }
form.controls.email.value;            // string (because nonNullable: true)
form.controls.age.value;              // number | null

nonNullable: true makes the control non-nullable — .value is T, not T | null. Use whenever the field has a default value.

form.value is partial because disabled controls are excluded; form.getRawValue() includes everything.

FormBuilder (less verbose)

import { FormBuilder, Validators } from '@angular/forms';

private fb = inject(FormBuilder);

loginForm = this.fb.nonNullable.group({
  email: ['', [Validators.required, Validators.email]],
  password: ['', [Validators.required, Validators.minLength(8)]],
});

fb.nonNullable.group() makes all fields non-nullable by default. Same typed result as manual FormGroup + nonNullable: true.

For nullable fields use fb.group({ ... }) (without nonNullable).

Built-in validators

Validators.required
Validators.requiredTrue                      // for checkboxes (must be checked)
Validators.email
Validators.min(0)
Validators.max(100)
Validators.minLength(8)
Validators.maxLength(64)
Validators.pattern(/^[A-Za-z]+$/)
Validators.compose([Validators.required, Validators.email])

Custom validators

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Simple sync validator
function noWhitespace(control: AbstractControl): ValidationErrors | null {
  const value = control.value;
  return value && value.trim().length === 0 ? { whitespace: true } : null;
}

// Validator factory (parameterized)
function maxWords(max: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const wordCount = (control.value || '').trim().split(/\s+/).filter(Boolean).length;
    return wordCount > max ? { maxWords: { max, actual: wordCount } } : null;
  };
}

// Cross-field validator (on FormGroup)
function passwordsMatch(group: AbstractControl): ValidationErrors | null {
  const a = group.get('password')?.value;
  const b = group.get('confirm')?.value;
  return a === b ? null : { mismatch: true };
}

// Apply
const form = new FormGroup({
  password: new FormControl('', { nonNullable: true }),
  confirm: new FormControl('', { nonNullable: true }),
}, { validators: passwordsMatch });

// Read group-level error in template
@if (form.errors?.['mismatch']) {
  <p role="alert">Passwords do not match</p>
}

Async validators

import { AsyncValidatorFn, AbstractControl } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { map, catchError, of } from 'rxjs';

function uniqueEmail(http: HttpClient): AsyncValidatorFn {
  return (control: AbstractControl) =>
    http.get<{ available: boolean }>(`/api/check?email=${control.value}`).pipe(
      map((r) => (r.available ? null : { taken: true })),
      catchError(() => of(null))   // network error — don't block
    );
}

// Apply
new FormControl('', {
  validators: [Validators.required, Validators.email],
  asyncValidators: [uniqueEmail(this.http)],
  updateOn: 'blur',                // validate on blur — appropriate for expensive checks
});

updateOn: 'blur' debounces the validation — only fires when user leaves the field. Critical for async validators to avoid hammering the server.

FormArray (dynamic field lists)

import { FormArray, FormBuilder, Validators } from '@angular/forms';

private fb = inject(FormBuilder);

contactsForm = this.fb.nonNullable.group({
  contacts: this.fb.array<FormGroup<{ email: FormControl<string> }>>([
    this.createContact(),
  ]),
});

get contacts() {
  return this.contactsForm.controls.contacts;
}

createContact() {
  return this.fb.nonNullable.group({
    email: ['', [Validators.required, Validators.email]],
  });
}

addContact() {
  this.contacts.push(this.createContact());
}

removeContact(index: number) {
  this.contacts.removeAt(index);
}

Template:

<div formArrayName="contacts">
  @for (contact of contacts.controls; track $index; let i = $index) {
    <div [formGroupName]="i">
      <input formControlName="email" />
      <button type="button" (click)="removeContact(i)">Remove</button>
    </div>
  }
</div>
<button type="button" (click)="addContact()">Add contact</button>

Server error mapping

When server returns 422 / 409 with field-specific errors:

async onSubmit() {
  if (this.loginForm.invalid) return;
  try {
    await this.authService.login(this.loginForm.getRawValue());
  } catch (err) {
    if (err instanceof FieldValidationError) {
      // Map each server error to its form control
      for (const [field, message] of Object.entries(err.fields)) {
        const control = this.loginForm.get(field);
        if (control) control.setErrors({ server: message });
      }
    } else {
      this.loginForm.setErrors({ serverError: 'Generic error' });
    }
  }
}

In template:

@if (emailControl.errors?.['server']; as msg) {
  <p role="alert">{{ msg }}</p>
}
@if (loginForm.errors?.['serverError']; as msg) {
  <p role="alert">{{ msg }}</p>
}

Multi-step forms

Two patterns:

A. Single FormGroup, conditional UI:

@Component({...})
export class WizardComponent {
  step = signal(0);
  form = this.fb.nonNullable.group({
    profile: this.fb.nonNullable.group({
      name: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
    }),
    address: this.fb.nonNullable.group({
      street: ['', Validators.required],
      city: ['', Validators.required],
    }),
  });

  next() {
    const currentStep = this.step() === 0 ? this.form.controls.profile : this.form.controls.address;
    currentStep.markAllAsTouched();
    if (currentStep.valid) this.step.update((s) => s + 1);
  }
}

B. Separate forms per step + parent state holder (signals or service): each step has its own form, parent merges.

Pick A for short wizards (≤3 steps), B for longer or when steps reorder dynamically.

Template-driven Forms (legacy / simple cases)

import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="onSubmit(f)">
      <input name="email" [(ngModel)]="email" required type="email" #emailField="ngModel" />
      @if (emailField.invalid && emailField.touched) {
        <p role="alert">Invalid email</p>
      }

      <input name="password" [(ngModel)]="password" required minlength="8" />

      <button type="submit" [disabled]="f.invalid">Submit</button>
    </form>
  `,
})
export class LoginComponent {
  email = '';
  password = '';

  onSubmit(form: NgForm) {
    if (form.invalid) return;
    this.authService.login({ email: this.email, password: this.password });
  }
}

Easier for tiny forms (1-3 fields). Harder to test, harder to type, harder to handle async validation. Don't use for non-trivial forms.

Mixing Reactive + Template-driven in the SAME form is unsupported — pick one per form.

Validation timing (updateOn)

new FormControl('', {
  validators: [Validators.required],
  updateOn: 'change' | 'blur' | 'submit',
});

// Or per-FormGroup:
new FormGroup({...}, { updateOn: 'blur' });
ModeWhen
'change' (default)Every keystroke
'blur'When field loses focus
'submit'Only on form submission

Use 'blur' for async validators (avoid hammering server). Use 'submit' rarely (delays user feedback).

Accessibility checklist

  • Every input has <label> (visible or via aria-label).
  • aria-invalid on invalid + touched fields.
  • role="alert" or aria-live="polite" on error messages.
  • Submit button shows loading state (disabled + visual indicator).
  • Focus moves to first error after failed submit (use markAllAsTouched() + scroll to first invalid control).

Anti-patterns

  • ❌ Untyped FormControl / FormGroup (use generics or nonNullable: true).
  • ❌ Mixing Reactive + Template-driven in same form.
  • ❌ Validating on every keystroke for expensive async checks (use updateOn: 'blur').
  • ❌ Skipping client validation thinking server suffices (UX suffers).
  • ❌ Skipping server validation thinking client suffices (security suffers).
  • ❌ Forgetting to disable submit button while submitting — double-submits.
  • ❌ Not handling the submitting state (no spinner / disabled feedback).
  • ❌ (ngSubmit)="onSubmit()" without checking form.invalid first.
  • ❌ Using (submit) on a <form> instead of (ngSubmit) — bypasses Angular's form lifecycle.
  • ❌ Mutating form.value directly — value is a getter, not the source of truth; use form.setValue() / patchValue().
  • ❌ Resetting form after server error — user re-types everything.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

ASP.NET Core web framework conventions: Minimal API vs MVC controllers, Program.cs composition, DI lifetimes (Scoped/Singleton/Transient), Options pattern with IOptions<T>, middleware ordering (HTTPS redirect → routing → authentication → authorization), model binding and validation (FluentValidation / DataAnnotations), ProblemDetails error handling, structured logging with ILogger<T>, configuration layering (appsettings.json + environment variables + User Secrets), and health checks. Works alongside csharp-foundation:csharp-conventions and aspnet-core-plugin:efcore-patterns. Use this skill to: - Compose Program.cs correctly — register services, configure middleware in the right order, map endpoints. - Apply the Options pattern to avoid passing raw IConfiguration into services. - Write Minimal API endpoint groups with typed results and authorization. - Handle cross-cutting errors uniformly with ProblemDetails. - Configure structured logging and health checks for production readiness. Do NOT use this skill for: - EF Core entity configuration and migrations — see aspnet-core-plugin:efcore-patterns. - C# language idioms — see csharp-foundation:csharp-conventions. - Testing — see csharp-foundation:dotnet-testing.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

AratKruglik のスキルをすべて見る

このスキルの問題を報告する