Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns.
.claude/skills/davila7-angular/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 277% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 385% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 231% | 0% |
| case-02 | ✓→✗ | ▼ Worse | 203% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 302% | 0% |
Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.
angular-migration skilltypescript-expert skill| Version | Release | Key Features | | -------------- | ------- | ------------------------------------------------------ | | Angular 20 | Q2 2025 | Signals stable, Zoneless stable, Incremental hydration | | Angular 21 | Q4 2025 | Signals-first default, Enhanced SSR | | Angular 22 | Q2 2026 | Signal Forms, Selectorless components |
Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.
typescriptimport { signal, computed, effect } from "@angular/core"; // Writable signal const count = signal(0); // Read value console.log(count()); // 0 // Update value count.set(5); // Direct set count.update((v) => v + 1); // Functional update // Computed (derived) signal const doubled = computed(() => count() * 2); // Effect (side effects) effect(() => { console.log(`Count changed to: ${count()}`); });
typescriptimport { Component, input, output, model } from "@angular/core"; @Component({ selector: "app-user-card", standalone: true, template: ` <div class="card"> <h3>{{ name() }}</h3> <span>{{ role() }}</span> <button (click)="select.emit(id())">Select</button> </div> `, }) export class UserCardComponent { // Signal inputs (read-only) id = input.required<string>(); name = input.required<string>(); role = input<string>("User"); // With default // Output select = output<string>(); // Two-way binding (model) isSelected = model(false); } // Usage: // <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />
typescriptimport { Component, viewChild, viewChildren, contentChild, } from "@angular/core"; @Component({ selector: "app-container", standalone: true, template: ` <input #searchInput /> <app-item *ngFor="let item of items()" /> `, }) export class ContainerComponent { // Signal-based queries searchInput = viewChild<ElementRef>("searchInput"); items = viewChildren(ItemComponent); projectedContent = contentChild(HeaderDirective); focusSearch() { this.searchInput()?.nativeElement.focus(); } }
| Use Case | Signals | RxJS | | ----------------------- | --------------- | -------------------------------- | | Local component state | ✅ Preferred | Overkill | | Derived/computed values | ✅ computed() | combineLatest works | | Side effects | ✅ effect() | tap operator | | HTTP requests | ❌ | ✅ HttpClient returns Observable | | Event streams | ❌ | ✅ fromEvent, operators | | Complex async flows | ❌ | ✅ switchMap, mergeMap |
Standalone components are self-contained and don't require NgModule declarations.
typescriptimport { Component } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterLink } from "@angular/router"; @Component({ selector: "app-header", standalone: true, imports: [CommonModule, RouterLink], // Direct imports template: ` <header> <a routerLink="/">Home</a> <a routerLink="/about">About</a> </header> `, }) export class HeaderComponent {}
typescript// main.ts import { bootstrapApplication } from "@angular/platform-browser"; import { provideRouter } from "@angular/router"; import { provideHttpClient } from "@angular/common/http"; import { AppComponent } from "./app/app.component"; import { routes } from "./app/app.routes"; bootstrapApplication(AppComponent, { providers: [provideRouter(routes), provideHttpClient()], });
typescript// app.routes.ts import { Routes } from "@angular/router"; export const routes: Routes = [ { path: "dashboard", loadComponent: () => import("./dashboard/dashboard.component").then( (m) => m.DashboardComponent, ), }, { path: "admin", loadChildren: () => import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES), }, ];
Zoneless applications don't use zone.js, improving performance and debugging.
typescript// main.ts import { bootstrapApplication } from "@angular/platform-browser"; import { provideZonelessChangeDetection } from "@angular/core"; import { AppComponent } from "./app/app.component"; bootstrapApplication(AppComponent, { providers: [provideZonelessChangeDetection()], });
typescriptimport { Component, signal, ChangeDetectionStrategy } from "@angular/core"; @Component({ selector: "app-counter", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div>Count: {{ count() }}</div> <button (click)="increment()">+</button> `, }) export class CounterComponent { count = signal(0); increment() { this.count.update((v) => v + 1); // No zone.js needed - Signal triggers change detection } }
bashng add @angular/ssr
typescript// app.config.ts import { ApplicationConfig } from "@angular/core"; import { provideClientHydration, withEventReplay, } from "@angular/platform-browser"; export const appConfig: ApplicationConfig = { providers: [provideClientHydration(withEventReplay())], };
typescriptimport { Component } from "@angular/core"; @Component({ selector: "app-page", standalone: true, template: ` <app-hero /> @defer (hydrate on viewport) { <app-comments /> } @defer (hydrate on interaction) { <app-chat-widget /> } `, }) export class PageComponent {}
| Trigger | When to Use | | ---------------- | --------------------------------------- | | on idle | Low-priority, hydrate when browser idle | | on viewport | Hydrate when element enters viewport | | on interaction | Hydrate on first user interaction | | on hover | Hydrate when user hovers | | on timer(ms) | Hydrate after specified delay |
typescript// auth.guard.ts import { inject } from "@angular/core"; import { Router, CanActivateFn } from "@angular/router"; import { AuthService } from "./auth.service"; export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); const router = inject(Router); if (auth.isAuthenticated()) { return true; } return router.createUrlTree(["/login"], { queryParams: { returnUrl: state.url }, }); }; // Usage in routes export const routes: Routes = [ { path: "dashboard", loadComponent: () => import("./dashboard.component"), canActivate: [authGuard], }, ];
typescriptimport { inject } from '@angular/core'; import { ResolveFn } from '@angular/router'; import { UserService } from './user.service'; import { User } from './user.model'; export const userResolver: ResolveFn<User> = (route) => { const userService = inject(UserService); return userService.getUser(route.paramMap.get('id')!); }; // In routes { path: 'user/:id', loadComponent: () => import('./user.component'), resolve: { user: userResolver } } // In component export class UserComponent { private route = inject(ActivatedRoute); user = toSignal(this.route.data.pipe(map(d => d['user']))); }
typescriptimport { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { UserService } from './user.service'; @Component({...}) export class UserComponent { // Modern inject() - no constructor needed private http = inject(HttpClient); private userService = inject(UserService); // Works in any injection context users = toSignal(this.userService.getUsers()); }
typescriptimport { InjectionToken, inject } from "@angular/core"; // Define token export const API_BASE_URL = new InjectionToken<string>("API_BASE_URL"); // Provide in config bootstrapApplication(AppComponent, { providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }], }); // Inject in service @Injectable({ providedIn: "root" }) export class ApiService { private baseUrl = inject(API_BASE_URL); get(endpoint: string) { return this.http.get(`${this.baseUrl}/${endpoint}`); } }
typescript@Component({ selector: 'app-card', template: ` <div class="card"> <div class="header"> <!-- Select by attribute --> <ng-content select="[card-header]"></ng-content> </div> <div class="body"> <!-- Default slot --> <ng-content></ng-content> </div> </div> ` }) export class CardComponent {} // Usage <app-card> <h3 card-header>Title</h3> <p>Body content</p> </app-card>
typescript// Reusable behaviors without inheritance @Directive({ standalone: true, selector: '[appTooltip]', inputs: ['tooltip'] // Signal input alias }) export class TooltipDirective { ... } @Component({ selector: 'app-button', standalone: true, hostDirectives: [ { directive: TooltipDirective, inputs: ['tooltip: title'] // Map input } ], template: `<ng-content />` }) export class ButtonComponent {}
typescriptimport { Injectable, signal, computed } from "@angular/core"; interface AppState { user: User | null; theme: "light" | "dark"; notifications: Notification[]; } @Injectable({ providedIn: "root" }) export class StateService { // Private writable signals private _user = signal<User | null>(null); private _theme = signal<"light" | "dark">("light"); private _notifications = signal<Notification[]>([]); // Public read-only computed readonly user = computed(() => this._user()); readonly theme = computed(() => this._theme()); readonly notifications = computed(() => this._notifications()); readonly unreadCount = computed( () => this._notifications().filter((n) => !n.read).length, ); // Actions setUser(user: User | null) { this._user.set(user); } toggleTheme() { this._theme.update((t) => (t === "light" ? "dark" : "light")); } addNotification(notification: Notification) { this._notifications.update((n) => [...n, notification]); } }
typescriptimport { Injectable, signal, computed, inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { toSignal } from "@angular/core/rxjs-interop"; @Injectable() export class ProductStore { private http = inject(HttpClient); // State private _products = signal<Product[]>([]); private _loading = signal(false); private _filter = signal(""); // Selectors readonly products = computed(() => this._products()); readonly loading = computed(() => this._loading()); readonly filteredProducts = computed(() => { const filter = this._filter().toLowerCase(); return this._products().filter((p) => p.name.toLowerCase().includes(filter), ); }); // Actions loadProducts() { this._loading.set(true); this.http.get<Product[]>("/api/products").subscribe({ next: (products) => { this._products.set(products); this._loading.set(false); }, error: () => this._loading.set(false), }); } setFilter(filter: string) { this._filter.set(filter); } }
typescriptimport { Component, inject } from "@angular/core"; import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms"; @Component({ selector: "app-user-form", standalone: true, imports: [ReactiveFormsModule], template: ` <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="name" placeholder="Name" /> <input formControlName="email" type="email" placeholder="Email" /> <button [disabled]="form.invalid">Submit</button> </form> `, }) export class UserFormComponent { private fb = inject(FormBuilder); form = this.fb.group({ name: ["", Validators.required], email: ["", [Validators.required, Validators.email]], }); onSubmit() { if (this.form.valid) { console.log(this.form.value); } } }
typescript// Future Signal Forms API (experimental) import { Component, signal } from '@angular/core'; @Component({...}) export class SignalFormComponent { name = signal(''); email = signal(''); // Computed validation isValid = computed(() => this.name().length > 0 && this.email().includes('@') ); submit() { if (this.isValid()) { console.log({ name: this.name(), email: this.email() }); } } }
typescript@Component({ changeDetection: ChangeDetectionStrategy.OnPush, // Only checks when: // 1. Input signal/reference changes // 2. Event handler runs // 3. Async pipe emits // 4. Signal value changes })
typescript@Component({ template: ` <!-- Immediate loading --> <app-header /> <!-- Lazy load when visible --> @defer (on viewport) { <app-heavy-chart /> } @placeholder { <div class="skeleton" /> } @loading (minimum 200ms) { <app-spinner /> } @error { <p>Failed to load chart</p> } ` })
typescriptimport { NgOptimizedImage } from '@angular/common'; @Component({ imports: [NgOptimizedImage], template: ` <img ngSrc="hero.jpg" width="800" height="600" priority /> <img ngSrc="thumbnail.jpg" width="200" height="150" loading="lazy" placeholder="blur" /> ` })
typescriptimport { ComponentFixture, TestBed } from "@angular/core/testing"; import { CounterComponent } from "./counter.component"; describe("CounterComponent", () => { let component: CounterComponent; let fixture: ComponentFixture<CounterComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CounterComponent], // Standalone import }).compileComponents(); fixture = TestBed.createComponent(CounterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should increment count", () => { expect(component.count()).toBe(0); component.increment(); expect(component.count()).toBe(1); }); it("should update DOM on signal change", () => { component.count.set(5); fixture.detectChanges(); const el = fixture.nativeElement.querySelector(".count"); expect(el.textContent).toContain("5"); }); });
typescriptimport { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentRef } from "@angular/core"; import { UserCardComponent } from "./user-card.component"; describe("UserCardComponent", () => { let fixture: ComponentFixture<UserCardComponent>; let componentRef: ComponentRef<UserCardComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent], }).compileComponents(); fixture = TestBed.createComponent(UserCardComponent); componentRef = fixture.componentRef; // Set signal inputs via setInput componentRef.setInput("id", "123"); componentRef.setInput("name", "John Doe"); fixture.detectChanges(); }); it("should display user name", () => { const el = fixture.nativeElement.querySelector("h3"); expect(el.textContent).toContain("John Doe"); }); });
| Pattern | ✅ Do | ❌ Don't | | -------------------- | ------------------------------ | ------------------------------- | | State | Use Signals for local state | Overuse RxJS for simple state | | Components | Standalone with direct imports | Bloated SharedModules | | Change Detection | OnPush + Signals | Default CD everywhere | | Lazy Loading | @defer and loadComponent | Eager load everything | | DI | inject() function | Constructor injection (verbose) | | Inputs | input() signal function | @Input() decorator (legacy) | | Zoneless | Enable for new projects | Force on legacy without testing |
| Issue | Solution | | ------------------------------ | --------------------------------------------------- | | Signal not updating UI | Ensure OnPush + call signal as function count() | | Hydration mismatch | Check server/client content consistency | | Circular dependency | Use inject() with forwardRef | | Zoneless not detecting changes | Trigger via signal updates, not mutations | | SSR fetch fails | Use TransferState or withFetch() |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 8,527 | 7,942 | -7% | 1 | 1 | 0% | 1,867 | 7,044 | +277% | 0 | 0 | — |
case-02 | pass→fail | 12,182 | 8,414 | -31% | 1 | 1 | 0% | 2,236 | 6,765 | +203% | 0 | 0 | — |
case-03 | pass→pass | 8,956 | 9,806 | +9% | 1 | 1 | 0% | 1,794 | 7,211 | +302% | 0 | 0 | — |
case-04 | pass→pass | 14,040 | 9,042 | -36% | 1 | 1 | 0% | 2,780 | 6,979 | +151% | 0 | 0 | — |
case-05 | pass→pass | 12,390 | 7,690 | -38% | 1 | 1 | 0% | 2,203 | 6,820 | +210% | 0 | 0 | — |
case-06 | pass→pass | 8,100 | 6,088 | -25% | 1 | 1 | 0% | 1,623 | 6,543 | +303% | 0 | 0 | — |
case-07 | pass→pass | 4,159 | 3,194 | -23% | 1 | 1 | 0% | 811 | 5,781 | +613% | 0 | 0 | — |
case-08 | pass→pass | 2,998 | 3,868 | +29% | 1 | 1 | 0% | 658 | 5,926 | +801% | 0 | 0 | — |
case-09 | pass→pass | 3,474 | 2,681 | -23% | 1 | 1 | 0% | 673 | 5,716 | +749% | 0 | 0 | — |
case-10 | pass→pass | 6,482 | 3,868 | -40% | 1 | 1 | 0% | 1,092 | 5,893 | +440% | 0 | 0 | — |
case-11 | fail→pass | 6,880 | 5,133 | -25% | 1 | 1 | 0% | 1,284 | 6,223 | +385% | 0 | 0 | — |
case-12 | pass→pass | 9,005 | 4,636 | -49% | 1 | 1 | 0% | 1,938 | 6,129 | +216% | 0 | 0 | — |
case-13 | pass→pass | 8,055 | 5,467 | -32% | 1 | 1 | 0% | 1,429 | 6,207 | +334% | 0 | 0 | — |
case-14 | pass→pass | 3,308 | 2,892 | -13% | 1 | 1 | 0% | 587 | 5,746 | +879% | 0 | 0 | — |
case-15 | fail→pass | 11,721 | 11,938 | +2% | 1 | 1 | 0% | 2,331 | 7,708 | +231% | 0 | 0 | — |
case-16 | pass→pass | 4,643 | 4,122 | -11% | 1 | 1 | 0% | 894 | 5,957 | +566% | 0 | 0 | — |
case-17 | pass→pass | 10,953 | 5,817 | -47% | 1 | 1 | 0% | 2,197 | 6,412 | +192% | 0 | 0 | — |
case-18 | pass→pass | 4,073 | 4,148 | +2% | 1 | 1 | 0% | 728 | 5,940 | +716% | 0 | 0 | — |
case-19 | pass→pass | 5,267 | 4,222 | -20% | 1 | 1 | 0% | 983 | 5,905 | +501% | 0 | 0 | — |
case-20 | pass→pass | 7,964 | 6,853 | -14% | 1 | 1 | 0% | 1,716 | 6,613 | +285% | 0 | 0 | — |
case-21 | pass→pass | 3,453 | 3,281 | -5% | 1 | 1 | 0% | 613 | 5,807 | +847% | 0 | 0 | — |
case-22 | pass→pass | 8,696 | 7,165 | -18% | 1 | 1 | 0% | 1,782 | 6,681 | +275% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.