with one click
angular-signals
// Use when building Angular 16+ applications requiring fine-grained reactive state management and zone-less change detection.
// Use when building Angular 16+ applications requiring fine-grained reactive state management and zone-less change detection.
Use when building modular Angular applications requiring dependency injection with providers, injectors, and services.
Use when handling async operations in Angular applications with observables, operators, and subjects.
Guides end-to-end feature development through 8 phases: discover requirements, explore codebase patterns, clarify ambiguities with the user, design architecture, implement with TDD, run multi-agent code review, validate all quality gates, and write a blog post. Use when asked to add a feature, implement a new capability, build functionality, or develop a feature end-to-end.
Use when creating or modifying Han plugins. Covers plugin structure, configuration, hooks, skills, and best practices.
Minimize token consumption through efficient tool usage patterns
Prefer explicit configuration over framework defaults to prevent environment-dependent failures
| name | angular-signals |
| user-invocable | false |
| description | Use when building Angular 16+ applications requiring fine-grained reactive state management and zone-less change detection. |
| allowed-tools | ["Bash","Read"] |
Master Angular Signals for building reactive applications with fine-grained reactivity and improved performance.
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count is: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}
import { signal } from '@angular/core';
// Create signal
const count = signal(0);
// set - replace value
count.set(5);
// update - transform current value
count.update(value => value + 1);
// mutate - modify object (experimental)
const user = signal({ name: 'John', age: 30 });
user.mutate(value => {
value.age = 31; // Mutate in place
});
// Read value
const current = count(); // Call as function
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Computed signal
const fullName = computed(() => {
return `${firstName()} ${lastName()}`;
});
console.log(fullName()); // John Doe
firstName.set('Jane');
console.log(fullName()); // Jane Doe (automatically updates)
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
// Computed: total items
itemCount = computed(() => {
return this.items().reduce((sum, item) => sum + item.quantity, 0);
});
// Computed: subtotal
subtotal = computed(() => {
return this.items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
// Computed: tax
tax = computed(() => this.subtotal() * 0.08);
// Computed: total
total = computed(() => this.subtotal() + this.tax());
// Computed: formatted total
formattedTotal = computed(() => {
return `$${this.total().toFixed(2)}`;
});
}
const count = signal(1);
const doubled = computed(() => count() * 2);
const quadrupled = computed(() => doubled() * 2);
const formatted = computed(() => `Count: ${quadrupled()}`);
console.log(formatted()); // Count: 4
count.set(2);
console.log(formatted()); // Count: 8
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-logger'
})
export class LoggerComponent {
count = signal(0);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(v => v + 1); // Triggers effect
}
}
import { effect } from '@angular/core';
const count = signal(0);
effect((onCleanup) => {
const timer = setInterval(() => {
console.log(count());
}, 1000);
// Cleanup function
onCleanup(() => {
clearInterval(timer);
});
});
import { effect, signal } from '@angular/core';
const enabled = signal(true);
const count = signal(0);
effect(() => {
// Only run if enabled
if (!enabled()) return;
console.log(`Count: ${count()}`);
});
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<div>
<h2>{{ displayName() }}</h2>
<p>Age: {{ age() }}</p>
<p>Is adult: {{ isAdult() }}</p>
</div>
`
})
export class UserProfileComponent {
// Signal inputs (Angular 17.1+)
firstName = input.required<string>();
lastName = input.required<string>();
age = input(0); // Optional with default
// Computed from inputs
displayName = computed(() =>
`${this.firstName()} ${this.lastName()}`
);
isAdult = computed(() => this.age() >= 18);
}
// Usage
<app-user-profile
[firstName]="'John'"
[lastName]="'Doe'"
[age]="30"
/>
import { Component, input } from '@angular/core';
@Component({
selector: 'app-formatted-text'
})
export class FormattedTextComponent {
// Transform input
text = input('', {
transform: (value: string) => value.toUpperCase()
});
// Alias input
label = input('', { alias: 'labelText' });
}
// Usage
<app-formatted-text
[text]="'hello'"
[labelText]="'Name'"
/>
import { Component, output } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button (click)="handleClick()">
{{ label() }}
</button>
`
})
export class ButtonComponent {
label = input('Click me');
// Signal output (Angular 17.1+)
clicked = output<void>();
valueChanged = output<number>();
private clickCount = signal(0);
handleClick() {
this.clickCount.update(v => v + 1);
this.clicked.emit();
this.valueChanged.emit(this.clickCount());
}
}
// Usage
<app-button
(clicked)="onClicked()"
(valueChanged)="onValueChanged($event)"
/>
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-input-focus',
template: `
<input #inputElement type="text" />
<button (click)="focusInput()">Focus</button>
`
})
export class InputFocusComponent {
// Signal-based viewChild
inputElement = viewChild<ElementRef>('inputElement');
constructor() {
afterNextRender(() => {
// Access element after render
const element = this.inputElement()?.nativeElement;
if (element) {
element.focus();
}
});
}
focusInput() {
this.inputElement()?.nativeElement.focus();
}
}
import { Component, viewChildren, ElementRef } from '@angular/core';
@Component({
selector: 'app-list',
standalone: true,
template: `
@for (item of items(); track item) {
<div #item>{{ item }}</div>
}
<p>Item count: {{ itemElements().length }}</p>
`
})
export class ListComponent {
items = signal(['A', 'B', 'C']);
// Signal-based viewChildren
itemElements = viewChildren<ElementRef>('item');
logItemCount() {
console.log(`Count: ${this.itemElements().length}`);
}
}
import { Component, contentChild, Directive } from '@angular/core';
@Directive({
selector: '[appHeader]'
})
export class HeaderDirective {}
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<ng-content select="[appHeader]" />
<ng-content />
@if (hasHeader()) {
<p>Has custom header</p>
}
</div>
`
})
export class CardComponent {
// Signal-based contentChild
header = contentChild(HeaderDirective);
hasHeader = computed(() => !!this.header());
}
// Usage
<app-card>
<h2 appHeader>Title</h2>
<p>Content</p>
</app-card>
// Use signals for synchronous state
@Component({
selector: 'app-counter'
})
export class CounterComponent {
count = signal(0); // Signal for synchronous state
increment() {
this.count.update(v => v + 1);
}
}
// Use observables for async operations
@Component({
selector: 'app-user-list'
})
export class UserListComponent {
private http = inject(HttpClient);
users$: Observable<User[]> = this.http.get<User[]>('/api/users');
}
import { Component, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap } from 'rxjs/operators';
@Component({
selector: 'app-search'
})
export class SearchComponent {
private http = inject(HttpClient);
// Signal for search query
searchQuery = signal('');
// Convert signal to observable
searchQuery$ = toObservable(this.searchQuery);
// Use observable operators
results$ = this.searchQuery$.pipe(
debounceTime(300),
switchMap(query => this.http.get(`/api/search?q=${query}`))
);
// Convert back to signal
results = toSignal(this.results$, { initialValue: [] });
}
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
`
})
export class UserListComponent {
private readonly http = inject(HttpClient);
// Convert observable to signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] as User[] }
);
}
import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'app-search'
})
export class SearchComponent {
searchTerm = signal('');
// Convert signal to observable
searchTerm$ = toObservable(this.searchTerm);
constructor() {
// Use observable operators
this.searchTerm$.pipe(
debounceTime(300)
).subscribe(term => {
console.log('Searching for:', term);
});
}
}
import { signal } from '@angular/core';
interface User {
id: number;
name: string;
}
// Custom equality check
const user = signal<User>(
{ id: 1, name: 'John' },
{
equal: (a, b) => a.id === b.id // Only compare IDs
}
);
user.set({ id: 1, name: 'Jane' }); // No update (same ID)
user.set({ id: 2, name: 'John' }); // Updates (different ID)
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
// Signal updates trigger change detection automatically
this.count.update(v => v + 1);
}
}
import { Component } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-cart'
})
export class CartComponentOld {
private items$ = new BehaviorSubject<Product[]>([]);
private discount$ = new BehaviorSubject<number>(0);
total$ = combineLatest([this.items$, this.discount$]).pipe(
map(([items, discount]) => {
const subtotal = items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
return subtotal * (1 - discount);
})
);
addItem(item: Product) {
this.items$.next([...this.items$.value, item]);
}
}
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
discount = signal(0);
total = computed(() => {
const subtotal = this.items().reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
return subtotal * (1 - this.discount());
});
addItem(item: Product) {
this.items.update(items => [...items, item]);
}
}
Use angular-signals when building modern, production-ready applications that require:
count vs count()import { signal, computed } from '@angular/core';
interface TodoState {
items: Todo[];
filter: 'all' | 'active' | 'completed';
}
@Injectable({
providedIn: 'root'
})
export class TodoStore {
// Private state
private state = signal<TodoState>({
items: [],
filter: 'all'
});
// Public selectors
items = computed(() => this.state().items);
filter = computed(() => this.state().filter);
filteredItems = computed(() => {
const items = this.items();
const filter = this.filter();
switch (filter) {
case 'active':
return items.filter(item => !item.completed);
case 'completed':
return items.filter(item => item.completed);
default:
return items;
}
});
// Actions
addTodo(text: string) {
this.state.update(state => ({
...state,
items: [...state.items, { id: Date.now(), text, completed: false }]
}));
}
toggleTodo(id: number) {
this.state.update(state => ({
...state,
items: state.items.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
)
}));
}
setFilter(filter: TodoState['filter']) {
this.state.update(state => ({ ...state, filter }));
}
}
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-login-form'
})
export class LoginFormComponent {
email = signal('');
password = signal('');
emailError = computed(() => {
const email = this.email();
if (!email) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return 'Invalid email format';
}
return null;
});
passwordError = computed(() => {
const password = this.password();
if (!password) return 'Password is required';
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
});
isValid = computed(() =>
!this.emailError() && !this.passwordError() &&
this.email() && this.password()
);
submit() {
if (!this.isValid()) return;
// Submit form
}
}