# Angular

> Use when building or refactoring Angular applications — choosing between signals, RxJS, and NgRx for state, configuring routing with guards and lazy loading, optimizing change detection, or writing TestBed component tests.

- **Type:** Skill
- **Install:** `agentstack add skill-kid-sid-claude-spellbook-angular`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [kid-sid](https://agentstack.voostack.com/s/kid-sid)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [kid-sid](https://github.com/kid-sid)
- **Source:** https://github.com/kid-sid/claude-spellbook/tree/main/skills/angular

## Install

```sh
agentstack add skill-kid-sid-claude-spellbook-angular
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Angular Patterns

Modern Angular (v17+) conventions using standalone components, signals, and typed forms.

## When to Activate

- Creating or refactoring Angular components, services, or directives
- Choosing between signals, RxJS, or NgRx for state
- Setting up routing with guards, resolvers, or lazy loading
- Building reactive or template-driven forms with validation
- Writing Angular tests (TestBed, jasmine, or jest)
- Configuring HttpClient with interceptors or typed responses
- Optimizing change detection with `OnPush`

---

## Project Structure

```
src/
├── app/
│   ├── core/               # Singleton services, interceptors, guards (provided in root)
│   │   ├── services/
│   │   ├── interceptors/
│   │   └── guards/
│   ├── shared/             # Reusable standalone components, pipes, directives
│   │   ├── components/
│   │   ├── pipes/
│   │   └── directives/
│   ├── features/           # Feature modules — each is a route subtree
│   │   ├── users/
│   │   │   ├── user-list/
│   │   │   ├── user-detail/
│   │   │   └── users.routes.ts
│   │   └── orders/
│   ├── app.component.ts
│   ├── app.config.ts       # bootstrapApplication providers
│   └── app.routes.ts
```

---

## Standalone Components (Angular 17+)

No NgModule required. Default for all new components.

```typescript
import { Component, inject, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { UserService } from '../core/services/user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule, RouterLink],  // import what you use
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    
      @for (user of users(); track user.id) {
        
          {{ user.name }}
        
      }
      @empty { No users found. }
    
  `,
})
export class UserListComponent {
  private userService = inject(UserService);   // inject() instead of constructor DI
  users = this.userService.users;              // signal from service
}
```

### New Control Flow (Angular 17+)

Prefer `@if`, `@for`, `@switch` over `*ngIf`, `*ngFor`:

```html
@if (isLoading()) {
  
} @else if (error()) {
  
} @else {
  
}

@for (item of items(); track item.id) {
  
} @empty {
  Nothing here.
}

@switch (status()) {
  @case ('active')   { Active }
  @case ('inactive') { Inactive }
  @default           { Unknown }
}
```

---

## Signals

Signals replace `BehaviorSubject` for synchronous local and service state.

```typescript
import { signal, computed, effect } from '@angular/core';

// Writable signal
const count = signal(0);
count.set(5);
count.update(n => n + 1);
count();        // read — always call as a function

// Computed — automatically tracks dependencies
const doubled = computed(() => count() * 2);

// Effect — runs when dependencies change (use sparingly)
effect(() => {
  console.log('count changed:', count());
});
```

### Signals in Services

```typescript
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  // Private writable, public readonly
  private _users = signal([]);
  readonly users = this._users.asReadonly();

  readonly activeUsers = computed(() =>
    this._users().filter(u => u.active)
  );

  load() {
    this.http.get('/api/users').subscribe(users => {
      this._users.set(users);
    });
  }

  add(user: User) {
    this._users.update(list => [...list, user]);
  }
}
```

### toSignal / toObservable — bridging signals ↔ RxJS

```typescript
import { toSignal, toObservable } from '@angular/core/rxjs-interop';

// Convert Observable to Signal (use in components instead of async pipe)
readonly users = toSignal(this.http.get('/api/users'), {
  initialValue: [] as User[],
});

// Convert Signal to Observable (pass to RxJS operators)
const count$ = toObservable(this.count);
count$.pipe(debounceTime(300)).subscribe(...);
```

---

## Dependency Injection

```typescript
// Provided in root (singleton for the whole app)
@Injectable({ providedIn: 'root' })
export class AuthService { ... }

// Provided in a specific component subtree (new instance per component)
@Component({
  providers: [LocalCartService],  // scoped to this component and its children
})

// inject() function — preferred over constructor injection
export class MyComponent {
  private auth = inject(AuthService);
  private router = inject(Router);
}

// Injection tokens for non-class values
export const API_URL = new InjectionToken('API_URL');

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' },
    provideHttpClient(withInterceptors([authInterceptor])),
    provideRouter(routes),
  ],
};

// Usage
private apiUrl = inject(API_URL);
```

---

## HttpClient

```typescript
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = inject(API_URL);

  getUsers(): Observable {
    return this.http.get(`${this.apiUrl}/users`);
  }

  createUser(data: CreateUserDto): Observable {
    return this.http.post(`${this.apiUrl}/users`, data);
  }

  updateUser(id: string, data: Partial): Observable {
    return this.http.patch(`${this.apiUrl}/users/${id}`, data);
  }

  deleteUser(id: string): Observable {
    return this.http.delete(`${this.apiUrl}/users/${id}`);
  }
}
```

### Functional Interceptors (Angular 15+)

```typescript
// core/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.getToken();

  if (!token) return next(req);

  return next(req.clone({
    setHeaders: { Authorization: `Bearer ${token}` },
  }));
};

// core/interceptors/error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) inject(AuthService).logout();
      if (error.status === 0) console.error('Network error');
      return throwError(() => error);
    }),
  );
};

// app.config.ts
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))
```

---

## Routing

```typescript
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },

  // Lazy-loaded feature route
  {
    path: 'users',
    loadChildren: () => import('./features/users/users.routes').then(m => m.USERS_ROUTES),
  },

  // Protected route
  {
    path: 'admin',
    canActivate: [authGuard],
    loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
  },

  { path: '**', component: NotFoundComponent },
];

// features/users/users.routes.ts
export const USERS_ROUTES: Routes = [
  { path: '',    component: UserListComponent },
  { path: ':id', component: UserDetailComponent,
    resolve: { user: userResolver } },
];
```

### Guards and Resolvers

```typescript
// Functional guard (Angular 14+)
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);
  if (auth.isLoggedIn()) return true;
  return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};

// Functional resolver
export const userResolver: ResolveFn = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

// Component reads resolved data
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  user = toSignal(this.route.data.pipe(map(d => d['user'] as User)));
}
```

---

## Forms

### Reactive Forms (preferred for complex forms)

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

@Component({
  imports: [ReactiveFormsModule],
  template: `
    
      
      @if (email.invalid && email.touched) {
        {{ emailError() }}
      }
      
      Save
    
  `,
})
export class UserFormComponent {
  private fb = inject(FormBuilder);

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

  get email() { return this.form.controls.email; }

  emailError = computed(() => {
    if (this.email.hasError('required')) return 'Email is required';
    if (this.email.hasError('email'))    return 'Invalid email format';
    return '';
  });

  submit() {
    if (this.form.invalid) return;
    console.log(this.form.getRawValue());
  }
}
```

### Custom Validator

```typescript
function noSpaces(control: AbstractControl) {
  return control.value?.includes(' ')
    ? { noSpaces: 'Username cannot contain spaces' }
    : null;
}

// Async validator (e.g. check username availability)
function uniqueUsername(userService: UserService): AsyncValidatorFn {
  return (control) =>
    userService.checkUsername(control.value).pipe(
      map(taken => taken ? { usernameTaken: true } : null),
      catchError(() => of(null)),
    );
}
```

### Template-Driven Forms (simple forms only)

```typescript
@Component({
  imports: [FormsModule],
  template: `
    
      
      @if (nameField.invalid && nameField.touched) {
        Name is required
      }
      Save
    
  `,
})
export class SimpleFormComponent {
  submit(form: NgForm) {
    if (form.valid) console.log(form.value);
  }
}
```

---

## RxJS in Angular

Common operators used in Angular services and components:

```typescript
// switchMap — cancel previous, use latest (search, route params)
this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => this.search(query)),  // cancels in-flight on new keystroke
).subscribe(results => this._results.set(results));

// combineLatest — react when any input changes
combineLatest([this.userId$, this.filter$]).pipe(
  switchMap(([id, filter]) => this.loadOrders(id, filter)),
).subscribe(orders => this._orders.set(orders));

// takeUntilDestroyed — auto-unsubscribe when component destroys (Angular 16+)
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

this.data$.pipe(takeUntilDestroyed()).subscribe(...);

// shareReplay — multicast HTTP call so multiple subscribers don't cause multiple requests
readonly users$ = this.http.get('/api/users').pipe(
  shareReplay(1),
);
```

---

## Change Detection

`OnPush` skips re-render unless: an `@Input()` reference changes, a signal read in the template changes, an Observable piped with `async` emits, or `markForCheck()` is called.

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    {{ title }}
    {{ count() }}          
    {{ data$ | async }}    
  `,
})
export class MyComponent {
  @Input() title = '';            // change detection on reference change
  count = signal(0);              // change detection on signal change
  data$ = this.service.data$;     // change detection on async pipe emit
}
```

**Always use `OnPush`** for new components. Combine with signals or `async` pipe — avoid manual `markForCheck()`.

---

## Pipes

```typescript
// Custom pure pipe (re-runs only when input changes)
@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 100, trail = '…'): string {
    return value.length > limit ? value.slice(0, limit) + trail : value;
  }
}

// Usage in template
{{ longText | truncate:50 }}
{{ longText | truncate:50:'...' }}

// Impure pipe (re-runs on every change detection cycle) — use sparingly
@Pipe({ name: 'filter', standalone: true, pure: false })
```

Built-in pipes: `date`, `currency`, `number`, `percent`, `json`, `async`, `keyvalue`, `titlecase`, `uppercase`, `lowercase`, `slice`.

---

## Custom Directives

```typescript
// Attribute directive — adds behaviour to host element
@Directive({ selector: '[appHighlight]', standalone: true })
export class HighlightDirective {
  @Input('appHighlight') color = 'yellow';

  private el = inject(ElementRef);
  private renderer = inject(Renderer2);

  @HostListener('mouseenter') onEnter() {
    this.renderer.setStyle(this.el.nativeElement, 'background', this.color);
  }
  @HostListener('mouseleave') onLeave() {
    this.renderer.removeStyle(this.el.nativeElement, 'background');
  }
}

// Usage
Hover me
```

---

## NgRx (when signals aren't enough)

Use NgRx when: multiple features share state, you need time-travel debugging, or complex side effects via Effects.

```typescript
// state/user.actions.ts
import { createActionGroup, emptyProps, props } from '@ngrx/store';

export const UserActions = createActionGroup({
  source: 'Users',
  events: {
    'Load Users':         emptyProps(),
    'Load Users Success': props(),
    'Load Users Failure': props(),
  },
});

// state/user.reducer.ts
const initialState: UserState = { users: [], loading: false, error: null };

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers,         state => ({ ...state, loading: true })),
  on(UserActions.loadUsersSuccess,  (state, { users }) => ({ ...state, users, loading: false })),
  on(UserActions.loadUsersFailure,  (state, { error }) => ({ ...state, error, loading: false })),
);

// state/user.effects.ts
export const loadUsersEffect = createEffect(
  (actions$ = inject(Actions), userService = inject(UserService)) =>
    actions$.pipe(
      ofType(UserActions.loadUsers),
      switchMap(() =>
        userService.getUsers().pipe(
          map(users => UserActions.loadUsersSuccess({ users })),
          catchError(e => of(UserActions.loadUsersFailure({ error: e.message }))),
        ),
      ),
    ),
  { functional: true },
);

// Component
store.dispatch(UserActions.loadUsers());
users = toSignal(store.select(selectAllUsers));
```

---

## Testing

### Component Tests (TestBed)

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

describe('UserListComponent', () => {
  let fixture: ComponentFixture;
  let userService: jasmine.SpyObj;

  beforeEach(async () => {
    userService = jasmine.createSpyObj('UserService', ['getUsers']);
    userService.getUsers.and.returnValue(of([{ id: '1', name: 'Alice' }]));

    await TestBed.configureTestingModule({
      imports: [UserListComponent],              // standalone: import directly
      providers: [{ provide: UserService, useValue: userService }],
    }).compileComponents();

    fixture = TestBed.createComponent(UserListComponent);
    fixture.detectChanges();
  });

  it('renders a user', () => {
    const item = fixture.debugElement.query(By.css('li'));
    expect(item.nativeElement.textContent).toContain('Alice');
  });

  it('calls service on init', () => {
    expect(userService.getUsers).toHaveBeenCalled();
  });
});
```

### Service Tests

```typescript
describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [UserService, provideHttpClientTesting()],
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify());  // ensure no unexpected requests

  it('fetches users', () => {
    service.getUsers().subscribe(users => {
      expect(users.length).toBe(1);
      expect(users[0].name).toBe('Alice');
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush([{ id: '1', name: 'Alice' }]);
  });
});
```

---

## Common Pitfalls

| Pitfall | Fix |
|---|---|
| Memory leaks from subscriptions | Use `takeUntilDestroyed()` or `async` pipe |
| `ExpressionChangedAfterItHasBeenCheckedError` | Don't mutate state

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [kid-sid](https://github.com/kid-sid)
- **Source:** [kid-sid/claude-spellbook](https://github.com/kid-sid/claude-spellbook)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-kid-sid-claude-spellbook-angular
- Seller: https://agentstack.voostack.com/s/kid-sid
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
