<< All versions
Skill v1.0.0
currentAutomated scansickn33/antigravity-awesome-skills/angular
──Details
PublishedMarch 27, 2026 at 06:07 AM
Content Hashsha256:e3b0c44298fc1c14...
Git SHAdiscovery:6c
──Files
Files (1 file, 20.0 KB)
SKILL.md20.0 KBactive
SKILL.md · 825 lines · 20.0 KB
version: "1.0.0" name: angular description: Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns. risk: safe source: self date_added: '2026-02-27'
Angular Expert
Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.
When to Use This Skill
- Building new Angular applications (v20+)
- Implementing Signals-based reactive patterns
- Creating Standalone Components and migrating from NgModules
- Configuring Zoneless Angular applications
- Implementing SSR, prerendering, and hydration
- Optimizing Angular performance
- Adopting modern Angular patterns and best practices
Do Not Use This Skill When
- Migrating from AngularJS (1.x) → use
angular-migrationskill - Working with legacy Angular apps that cannot upgrade
- General TypeScript issues → use
typescript-expertskill
Instructions
- Assess the Angular version and project structure
- Apply modern patterns (Signals, Standalone, Zoneless)
- Implement with proper typing and reactivity
- Validate with build and tests
Safety
- Always test changes in development before production
- Gradual migration for existing apps (don't big-bang refactor)
- Keep backward compatibility during transitions
Angular Version Timeline
| 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 |
1. Signals: The New Reactive Primitive
Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.
Core Concepts
typescript
import { signal, computed, effect } from "@angular/core";// Writable signalconst count = signal(0);// Read valueconsole.log(count()); // 0// Update valuecount.set(5); // Direct setcount.update((v) => v + 1); // Functional update// Computed (derived) signalconst doubled = computed(() => count() * 2);// Effect (side effects)effect(() => {console.log(`Count changed to: ${count()}`);});
Signal-Based Inputs and Outputs
typescript
import { 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// Outputselect = output<string>();// Two-way binding (model)isSelected = model(false);}// Usage:// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />
Signal Queries (ViewChild/ContentChild)
typescript
import {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 queriessearchInput = viewChild<ElementRef>("searchInput");items = viewChildren(ItemComponent);projectedContent = contentChild(HeaderDirective);focusSearch() {this.searchInput()?.nativeElement.focus();}}
When to Use Signals vs RxJS
| 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 |
2. Standalone Components
Standalone components are self-contained and don't require NgModule declarations.
Creating Standalone Components
typescript
import { Component } from "@angular/core";import { CommonModule } from "@angular/common";import { RouterLink } from "@angular/router";@Component({selector: "app-header",standalone: true,imports: [CommonModule, RouterLink], // Direct importstemplate: `<header><a routerLink="/">Home</a><a routerLink="/about">About</a></header>`,})export class HeaderComponent {}
Bootstrapping Without NgModule
typescript
// main.tsimport { 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()],});
Lazy Loading Standalone Components
typescript
// app.routes.tsimport { 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),},];
3. Zoneless Angular
Zoneless applications don't use zone.js, improving performance and debugging.
Enabling Zoneless Mode
typescript
// main.tsimport { bootstrapApplication } from "@angular/platform-browser";import { provideZonelessChangeDetection } from "@angular/core";import { AppComponent } from "./app/app.component";bootstrapApplication(AppComponent, {providers: [provideZonelessChangeDetection()],});
Zoneless Component Patterns
typescript
import { 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}}
Key Zoneless Benefits
- Performance: No zone.js patches on async APIs
- Debugging: Clean stack traces without zone wrappers
- Bundle size: Smaller without zone.js (~15KB savings)
- Interoperability: Better with Web Components and micro-frontends
4. Server-Side Rendering & Hydration
SSR Setup with Angular CLI
bash
ng add @angular/ssr
Hydration Configuration
typescript
// app.config.tsimport { ApplicationConfig } from "@angular/core";import {provideClientHydration,withEventReplay,} from "@angular/platform-browser";export const appConfig: ApplicationConfig = {providers: [provideClientHydration(withEventReplay())],};
Incremental Hydration (v20+)
typescript
import { 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 {}
Hydration Triggers
| 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 |
5. Modern Routing Patterns
Functional Route Guards
typescript
// auth.guard.tsimport { 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 routesexport const routes: Routes = [{path: "dashboard",loadComponent: () => import("./dashboard.component"),canActivate: [authGuard],},];
Route-Level Data Resolvers
typescript
import { 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 componentexport class UserComponent {private route = inject(ActivatedRoute);user = toSignal(this.route.data.pipe(map(d => d['user'])));}
6. Dependency Injection Patterns
Modern inject() Function
typescript
import { Component, inject } from '@angular/core';import { HttpClient } from '@angular/common/http';import { UserService } from './user.service';@Component({...})export class UserComponent {// Modern inject() - no constructor neededprivate http = inject(HttpClient);private userService = inject(UserService);// Works in any injection contextusers = toSignal(this.userService.getUsers());}
Injection Tokens for Configuration
typescript
import { InjectionToken, inject } from "@angular/core";// Define tokenexport const API_BASE_URL = new InjectionToken<string>("API_BASE_URL");// Provide in configbootstrapApplication(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}`);}}
7. Component Composition & Reusability
Content Projection (Slots)
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>
Host Directives (Composition)
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 {}
8. State Management Patterns
Signal-Based State Service
typescript
import { Injectable, signal, computed } from "@angular/core";interface AppState {user: User | null;theme: "light" | "dark";notifications: Notification[];}@Injectable({ providedIn: "root" })export class StateService {// Private writable signalsprivate _user = signal<User | null>(null);private _theme = signal<"light" | "dark">("light");private _notifications = signal<Notification[]>([]);// Public read-only computedreadonly user = computed(() => this._user());readonly theme = computed(() => this._theme());readonly notifications = computed(() => this._notifications());readonly unreadCount = computed(() => this._notifications().filter((n) => !n.read).length,);// ActionssetUser(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]);}}
Component Store Pattern with Signals
typescript
import { 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);// Stateprivate _products = signal<Product[]>([]);private _loading = signal(false);private _filter = signal("");// Selectorsreadonly 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),);});// ActionsloadProducts() {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);}}
9. Forms with Signals (Coming in v22+)
Current Reactive Forms
typescript
import { 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);}}}
Signal-Aware Form Patterns (Preview)
typescript
// Future Signal Forms API (experimental)import { Component, signal } from '@angular/core';@Component({...})export class SignalFormComponent {name = signal('');email = signal('');// Computed validationisValid = computed(() =>this.name().length > 0 &&this.email().includes('@'));submit() {if (this.isValid()) {console.log({ name: this.name(), email: this.email() });}}}
10. Performance Optimization
Change Detection Strategies
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})
Defer Blocks for Lazy Loading
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>}`})
NgOptimizedImage
typescript
import { NgOptimizedImage } from '@angular/common';@Component({imports: [NgOptimizedImage],template: `<imgngSrc="hero.jpg"width="800"height="600"priority/><imgngSrc="thumbnail.jpg"width="200"height="150"loading="lazy"placeholder="blur"/>`})
11. Testing Modern Angular
Testing Signal Components
typescript
import { 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");});});
Testing with Signal Inputs
typescript
import { 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 setInputcomponentRef.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");});});
Best Practices Summary
| 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 |
Resources
Common Troubleshooting
| 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() |
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.