Claude-skill-registry frontend-web-dev

Use when building Angular applications, creating TypeScript components, implementing reactive forms, managing state, working with RxJS observables, or developing modern web UI with TypeScript.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-web-dev" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-web-dev && rm -rf "$T"
manifest: skills/data/frontend-web-dev/SKILL.md
source content

Frontend Web Development Expert

Overview

Expert guidance for building modern web applications with Angular 20+, TypeScript, Signals, and RxJS. Focused on standalone components, signal-based reactivity, and type-safe development.

When to Use

  • Creating Angular components, services, or standalone features
  • Implementing reactive forms or template-driven forms
  • Working with Signals for state management
  • Working with RxJS observables for async streams
  • Implementing HTTP clients and API integration
  • Creating reusable UI components
  • Implementing routing and navigation
  • Working with Angular dependency injection

Angular 20+ Core Patterns

Standalone Component with Signals (Preferred)

// Good: Modern Angular 20 component with signals and OnPush
import {
  Component,
  ChangeDetectionStrategy,
  input,
  output,
  computed
} from '@angular/core';

interface User {
  readonly id: number;
  readonly name: string;
  readonly email: string;
}

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user-card.component.html',
  styleUrl: './user-card.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  // Signal-based inputs (Angular 17+)
  user = input.required<User>();

  // Signal-based outputs
  userSelected = output<User>();

  // Computed signals for derived state
  displayName = computed(() => this.user().name.toUpperCase());

  onSelect(): void {
    this.userSelected.emit(this.user());
  }
}
// Bad: Outdated patterns
@Component({
  selector: 'user-card',
  template: `<div>{{user.name}}</div>`
  // Missing: standalone, OnPush, proper typing
})
export class UserCard {
  user: any;  // No type safety!
  @Output() selected: any;  // Old decorator syntax + any type
}

Service with Signals for State

// Good: Modern service using signals for state management
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface User {
  readonly id: number;
  readonly name: string;
  readonly email: string;
  readonly active: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private readonly apiUrl = '/api/users';

  // Signal-based state
  private readonly usersState = signal<User[]>([]);
  private readonly loadingState = signal(false);
  private readonly errorState = signal<string | null>(null);

  // Public readonly computed signals
  readonly users = this.usersState.asReadonly();
  readonly loading = this.loadingState.asReadonly();
  readonly error = this.errorState.asReadonly();

  // Computed derived state
  readonly activeUsers = computed(() =>
    this.usersState().filter(u => u.active)
  );
  readonly userCount = computed(() => this.usersState().length);

  constructor(private http: HttpClient) {}

  loadUsers(): void {
    this.loadingState.set(true);
    this.errorState.set(null);

    this.http.get<User[]>(this.apiUrl).subscribe({
      next: (users) => {
        this.usersState.set(users);
        this.loadingState.set(false);
      },
      error: (err) => {
        this.errorState.set(err.message);
        this.loadingState.set(false);
      }
    });
  }

  addUser(user: User): void {
    // Immutable update
    this.usersState.update(users => [...users, user]);
  }
}

Reactive Forms with Typed FormGroup

// Good: Strongly-typed reactive forms (Angular 14+)
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';

interface UserFormValue {
  email: string;
  name: string;
  age: number | null;
}

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './user-form.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserFormComponent {
  // Typed form group
  userForm = this.fb.nonNullable.group({
    email: ['', [Validators.required, Validators.email]],
    name: ['', [Validators.required, Validators.minLength(2)]],
    age: [null as number | null, [Validators.min(0), Validators.max(150)]]
  });

  constructor(private fb: FormBuilder) {}

  onSubmit(): void {
    if (this.userForm.valid) {
      const userData: UserFormValue = this.userForm.getRawValue();
      // Process typed form data
    }
  }
}

Quick Reference: Angular 20+ Best Practices

PatternRecommendation
ComponentsStandalone with
standalone: true
Change DetectionAlways use
OnPush
for performance
State (local)Signals:
signal()
,
computed()
State (shared)Services with signals
Async streamsRxJS for HTTP, events, WebSockets
Inputs
input()
/
input.required()
(signal-based)
Outputs
output()
(signal-based)
FormsTyped reactive forms with
fb.nonNullable.group()
Type SafetyStrict TypeScript, interfaces for all data

Change Detection: OnPush Explained

Angular checks components for changes to update the DOM. There are two strategies:

Default: Check this component on EVERY change detection cycle (expensive)

OnPush: Only check this component when:

  1. An
    @Input()
    or
    input()
    reference changes
  2. An event originates from this component or its children
  3. A signal used in the template updates
  4. Manually triggered via
    ChangeDetectorRef
// ALWAYS use OnPush - it's a free performance win
@Component({
  // ...
  changeDetection: ChangeDetectionStrategy.OnPush
})

With signals, OnPush becomes even more efficient - Angular knows exactly which signal changed and only updates the affected DOM nodes.

Signals vs RxJS: When to Use Each

Use CaseUse SignalsUse RxJS
Component state
signal()
Derived/computed values
computed()
Service state
signal()
⚠️ BehaviorSubject (legacy)
HTTP requests
HttpClient
returns Observable
Event streams✅ Multiple values over time
Debounce/throttle✅ RxJS operators
Combining async sources⚠️ Limited
combineLatest
,
forkJoin
// Signals: Synchronous state
const count = signal(0);
const doubled = computed(() => count() * 2);

// RxJS: Async streams (HTTP, events, WebSockets)
this.http.get<User[]>('/api/users').pipe(
  catchError(err => of([]))
).subscribe(users => this.usersState.set(users));

Common Mistakes

Forgetting OnPush:

// Bad: Missing OnPush (checks every cycle)
@Component({ selector: 'app-foo', ... })

// Good: Always include OnPush
@Component({
  selector: 'app-foo',
  changeDetection: ChangeDetectionStrategy.OnPush,
  ...
})

Mutating state directly:

// Bad: Direct mutation (won't trigger change detection with OnPush)
this.users.push(newUser);
this.usersSignal().push(newUser);  // Also bad!

// Good: Immutable update
this.users = [...this.users, newUser];
this.usersSignal.update(users => [...users, newUser]);

Not typing data structures:

// Bad: Lose type safety
const data: any = response;
navItems = signal([{ label: 'Home', icon: 'home' }]);  // Inline, no interface

// Good: Define interfaces
interface NavItem {
  readonly label: string;
  readonly icon: string;
  readonly route: string;
}
const navItems = signal<NavItem[]>([...]);

Memory leaks with RxJS (still relevant for HTTP/events):

// Bad: Subscription leak
ngOnInit() {
  this.service.getData().subscribe(data => {
    this.data = data;
  });
}

// Good: Use takeUntilDestroyed (Angular 16+)
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class MyComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.service.getData()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(data => this.dataSignal.set(data));
  }
}

// Better: Convert to signal with toSignal()
import { toSignal } from '@angular/core/rxjs-interop';

export class MyComponent {
  data = toSignal(this.service.getData(), { initialValue: [] });
}

TypeScript Best Practices

// Good: Strict interfaces with readonly properties
interface User {
  readonly id: number;
  readonly email: string;
  readonly name: string;
  readonly createdAt: Date;
}

// Good: Utility types for variations
type UserCreate = Omit<User, 'id' | 'createdAt'>;
type UserUpdate = Partial<UserCreate>;

// Good: Discriminated unions for state
type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

Testing Patterns

// Good: Testing standalone component with signals
import { ComponentFixture, TestBed } from '@angular/core/testing';

describe('UserCardComponent', () => {
  let component: UserCardComponent;
  let fixture: ComponentFixture<UserCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserCardComponent]  // Standalone: import the component itself
    }).compileComponents();

    fixture = TestBed.createComponent(UserCardComponent);
    component = fixture.componentInstance;
  });

  it('should emit userSelected when clicked', () => {
    const user: User = { id: 1, name: 'Test', email: 'test@example.com' };

    // Set signal input using componentRef
    fixture.componentRef.setInput('user', user);
    fixture.detectChanges();

    let emittedUser: User | undefined;
    component.userSelected.subscribe(u => emittedUser = u);

    component.onSelect();

    expect(emittedUser).toEqual(user);
  });
});

Key Principles

  1. Standalone components: Always use
    standalone: true
  2. OnPush always: Free performance optimization
  3. Signals for state: Simpler than RxJS for synchronous state
  4. RxJS for streams: HTTP calls, events, WebSockets
  5. Type everything: Interfaces for all data structures
  6. Immutable updates: Never mutate, always create new references
  7. Modern inputs/outputs: Use
    input()
    and
    output()
    functions