Files
everything-claude-code/rules/angular/coding-style.md
2026-05-11 19:38:21 -04:00

5.1 KiB

paths
paths
**/*.component.ts
**/*.component.html
**/*.service.ts
**/*.directive.ts
**/*.pipe.ts
**/*.guard.ts
**/*.resolver.ts
**/*.module.ts

Angular Coding Style

This file extends common/coding-style.md with Angular specific content.

Version Awareness

Always check the project's Angular version before writing code — features differ significantly between versions. Run ng version or inspect package.json. When creating a new project, do not pin a version unless the user specifies one.

After generating or modifying Angular code, always run ng build to catch errors before finishing.

File Naming

Follow Angular CLI conventions — one artifact per file:

  • user-profile.component.ts + user-profile.component.html + user-profile.component.spec.ts
  • user.service.ts, auth.guard.ts, date-format.pipe.ts
  • Feature folders: features/users/, features/auth/
  • Generate with the CLI: ng generate component features/users/user-card

Components

Prefer standalone components (v17+ default). Use OnPush change detection on all new components.

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [RouterModule],
  templateUrl: './user-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  user = input.required<User>();
  select = output<string>();
}

Dependency Injection

Use inject() over constructor injection. Keep constructors empty or remove them entirely.

// CORRECT
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private router = inject(Router);
}

// WRONG: Constructor injection is verbose and harder to tree-shake
constructor(private http: HttpClient, private router: Router) {}

Use InjectionToken for non-class dependencies:

const API_URL = new InjectionToken<string>('API_URL');

// Provide:
{ provide: API_URL, useValue: 'https://api.example.com' }

// Consume:
private apiUrl = inject(API_URL);

Signals

Core Primitives

count = signal(0);
doubled = computed(() => this.count() * 2);

increment() {
  this.count.update(n => n + 1);
}

linkedSignal — Writable Derived State

Use linkedSignal when a signal must reset or adapt when a source changes, but also be independently writable:

selectedOption = linkedSignal(() => this.options()[0]);
// Resets to first option when options changes, but user can override

resource — Async Data into Signals

Use resource() to fetch async data reactively without manual subscriptions:

userResource = resource({
  request: () => ({ id: this.userId() }),
  loader: ({ request }) => fetch(`/api/users/${request.id}`).then(r => r.json()),
});

// Access: userResource.value(), userResource.isLoading(), userResource.error()

effect Usage

Use effect() only for side effects that must react to signal changes (logging, third-party DOM manipulation). Never use effects to synchronize signals — use computed or linkedSignal instead. For DOM work after render, use afterRenderEffect.

// CORRECT: Side effect
effect(() => console.log('User changed:', this.user()));

// WRONG: Use computed instead
effect(() => { this.fullName.set(`${this.first()} ${this.last()}`); });

Templates

Use v17+ block syntax. Always provide track in @for:

@for (item of items(); track item.id) {
  <app-item [item]="item" />
}

@if (isLoading()) {
  <app-spinner />
} @else if (error()) {
  <app-error [message]="error()" />
} @else {
  <app-content [data]="data()" />
}

No logic in templates beyond simple conditionals — move to component methods or pipes.

Forms

Choose the form strategy that matches the project's existing approach:

  • Signal Forms (v21+): Preferred for new projects on v21+. Signal-based form state.
  • Reactive Forms: FormBuilder + FormGroup + FormControl. Best for complex forms with dynamic validation.
  • Template-Driven Forms: ngModel. Suitable for simple forms only.
// Reactive Forms — standard approach for most apps
export class LoginComponent {
  private fb = inject(FormBuilder);

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

  submit() {
    if (this.form.valid) {
      // use this.form.value
    }
  }
}

Component Styles

Use component-level styles with ViewEncapsulation.Emulated (default). Avoid ViewEncapsulation.None unless building a design system that intentionally bleeds styles.

  • Scope styles to the component — do not use global class names inside component stylesheets
  • Use :host for host element styling
  • Prefer CSS custom properties for themeable values

Change Detection

  • Default to ChangeDetectionStrategy.OnPush on all new components
  • Signals and async pipe handle detection automatically — avoid markForCheck() and detectChanges()
  • Never mutate @Input() objects in place when using OnPush