agent-rules-kit
Version:
CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.
291 lines (235 loc) • 6.93 kB
Markdown
---
description: Guidelines for using Angular Signals for reactive state management in Angular 16+.
globs: <root>/src/app/**/*.ts # Relevant for signal usage
alwaysApply: true # Applies if v16+ is detected
---
# Angular Signals (Angular 16-17)
This project uses Angular Signals, introduced in Angular 16 and enhanced in Angular 17. Follow these guidelines for state management.
## Signal Basics
Signals are a system for reactive state management, allowing Angular to track dependencies and update only what's needed.
### Creating Signals
```typescript
import { signal } from '@angular/core';
// Creating a signal with an initial value
const count = signal(0);
// Reading a signal value
console.log(count()); // 0
// Updating a signal
count.set(1); // Direct set
count.update((current) => current + 1); // Update based on current value
// Creating computed signals
const doubledCount = computed(() => count() * 2);
```
## Component Usage
```typescript
import { Component, signal, computed } from '@angular/core';
({
selector: 'app-counter',
template: `
<div>Count: {{ count() }}</div>
<div>Doubled: {{ doubled() }}</div>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update((c) => c + 1);
}
}
```
## Signal Inputs
Angular 17 introduces signal-based inputs:
```typescript
import { Component, input } from '@angular/core';
({
selector: 'app-user-card',
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>Role: {{ role() || 'User' }}</p>
</div>
`,
})
export class UserCardComponent {
// Required input
name = input.required<string>();
// Optional input with default value
role = input<string>('User');
}
```
## Signal Effects
Use effects for side effects when signals change:
```typescript
import { Component, signal, effect } from '@angular/core';
({
selector: 'app-theme-switcher',
template: ` <button (click)="toggleTheme()">Toggle Theme</button> `,
})
export class ThemeSwitcherComponent implements OnInit {
theme = signal('light');
constructor() {
// Effect runs when signals it depends on change
effect(() => {
document.body.className = this.theme();
localStorage.setItem('theme', this.theme());
});
}
toggleTheme() {
this.theme.update((current) =>
current === 'light' ? 'dark' : 'light'
);
}
ngOnInit() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
this.theme.set(savedTheme);
}
}
}
```
## Models
Models can use signals for reactive properties:
```typescript
export class User {
id = signal<string>('');
name = signal<string>('');
email = signal<string>('');
constructor(data?: Partial<{ id: string; name: string; email: string }>) {
if (data?.id) this.id.set(data.id);
if (data?.name) this.name.set(data.name);
if (data?.email) this.email.set(data.email);
}
update(data: Partial<{ name: string; email: string }>) {
if (data.name) this.name.set(data.name);
if (data.email) this.email.set(data.email);
}
}
```
## Services with Signals
Services can expose state as signals:
```typescript
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
({
providedIn: 'root',
})
export class TodoService {
private todos = signal<Todo[]>([]);
private loading = signal<boolean>(false);
private error = signal<string | null>(null);
// Computed values derived from signals
completedTodos = computed(() =>
this.todos().filter((todo) => todo.completed)
);
incompleteTodos = computed(() =>
this.todos().filter((todo) => !todo.completed)
);
// Public signals for components to consume
// Allows read-only access to private signals
todosList = this.todos.asReadonly();
isLoading = this.loading.asReadonly();
errorMessage = this.error.asReadonly();
constructor(private http: HttpClient) {}
loadTodos() {
this.loading.set(true);
this.error.set(null);
this.http.get<Todo[]>('/api/todos').subscribe({
next: (data) => {
this.todos.set(data);
this.loading.set(false);
},
error: (err) => {
this.error.set(err.message);
this.loading.set(false);
},
});
}
addTodo(title: string) {
const newTodo: Todo = {
id: Date.now().toString(),
title,
completed: false,
};
this.todos.update((current) => [...current, newTodo]);
// API call to persist
this.http.post<Todo>('/api/todos', newTodo).subscribe({
error: (err) => {
// Rollback on error
this.todos.update((current) =>
current.filter((t) => t.id !== newTodo.id)
);
this.error.set(err.message);
},
});
}
toggleTodo(id: string) {
this.todos.update((current) =>
current.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
// Persist change to API
// ...
}
}
```
## Working with RxJS and Signals
Integrating RxJS with signals:
```typescript
import { Component, signal, computed, OnInit, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
import { UserService } from './user.service';
({
selector: 'app-user-dashboard',
template: `
<div *ngIf="loading()">Loading...</div>
<div *ngIf="error()">Error: {{ error() }}</div>
<div *ngIf="activeUsers()">
<h2>Active Users: {{ activeUsers()?.length }}</h2>
<ul>
<li *ngFor="let user of activeUsers()">{{ user.name }}</li>
</ul>
</div>
`,
})
export class UserDashboardComponent implements OnInit {
private userService = inject(UserService);
// Convert observable to signal
private usersSignal = toSignal(
this.userService
.getUsers()
.pipe(map((users) => users.filter((user) => user.active))),
{ initialValue: [] }
);
// Regular signals
loading = signal(true);
error = signal<string | null>(null);
// Expose the signal directly
activeUsers = this.usersSignal;
ngOnInit() {
try {
this.userService.loadUsers().subscribe({
next: () => this.loading.set(false),
error: (err) => {
this.error.set(err.message);
this.loading.set(false);
},
});
} catch (err) {
this.error.set('Failed to load users');
this.loading.set(false);
}
}
}
```
## Best Practices
1. **Signal Granularity**: Use individual signals for independent pieces of state
2. **Computed for Derived State**: Use computed signals for derived state
3. **Signal Collections**: Consider using a single signal for related collections
4. **Read-only Signals**: Expose signals as read-only when providing them to components
5. **Effects for Side Effects**: Use effects for side effects that run when signals change
6. **Signal Inputs**: Use signal-based inputs for component property binding in Angular 17+
7. **Lazy Evaluation**: Take advantage of the lazy evaluation nature of signals and computed values