Claude
Chat app · Skill upload
In Claude, open Customize → Skills and use Create skill to upload the ZIP. Skill availability depends on your plan and workspace settings.
Official Claude setup →Community skill · Free instruction package
Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns. Use PROACTIVELY for Angular development, component architecture, state management, performance optimization, and migration to modern patterns.
Community instruction package. Read the full workflow below, then choose your app in the setup guide. Package instructions and compatibility claims have not been individually verified; check dependencies and license before use.
CHOOSE YOUR AI APP
Choose your app below for setup instructions. Keep the complete downloaded folder together. Each package may need tools, dependencies or permissions that your app does not provide.
Chat app · Skill upload
In Claude, open Customize → Skills and use Create skill to upload the ZIP. Skill availability depends on your plan and workspace settings.
Official Claude setup →Chat app · Access varies
Where Skills is available, open Plugins → Skills → Create → Upload from your computer. Otherwise, copy the instructions into a chat and add your brief; this does not install a skill or include its supporting files.
Official ChatGPT setup →Agent · Project skills
Extract the complete skill folder into your project’s .agents/skills directory. Ask the agent to use the skill for your task.
.agents/skills/angular/SKILL.mdOfficial Google Antigravity setup →Code editor · Project skills
Extract the complete folder into .cursor/skills. Check the editor’s skill settings and ask the agent to use it.
.cursor/skills/angular/SKILL.mdOfficial Cursor setup →Coding agent · Project skills
Extract the complete folder into .claude/skills. Ask Claude to use the named skill, or use its slash command when available.
.claude/skills/angular/SKILL.mdOfficial Claude Code setup →For text-only workflows, you can also paste the instructions into an AI conversation. Copying text does not enable scripts, connect accounts or grant tool access. App subscriptions may cost extra. Logos identify the products; KuchhBhi is independent and is not endorsed by these companies.
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.
import { 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()}`);
});
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
// Output
select = output<string>();
// Two-way binding (model)
isSelected = model(false);
}
// Usage:
// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />
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 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.
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 imports
template: `
<header>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</header>
`,
})
export class HeaderComponent {}
// 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()],
});
// 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.
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideZonelessChangeDetection } from "@angular/core";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
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
}
}
ng add @angular/ssr
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import {
provideClientHydration,
withEventReplay,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())],
};
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 {}
| 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 |
// 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],
},
];
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 component
export class UserComponent {
private route = inject(ActivatedRoute);
user = toSignal(this.route.data.pipe(map(d => d['user'])));
}
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 needed
private http = inject(HttpClient);
private userService = inject(UserService);
// Works in any injection context
users = toSignal(this.userService.getUsers());
}
import { 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}`);
}
}
@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>
// 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 {}
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 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]);
}
}
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);
// 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);
}
}
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);
}
}
}
// 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() });
}
}
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// Only checks when:
// 1. Input signal/reference changes
// 2. Event handler runs
// 3. Async pipe emits
// 4. Signal value changes
})
@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>
}
`
})
import { 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"
/>
`
})
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");
});
});
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 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() |
The instruction package is free to download. Your AI app, model usage, connected services and third-party assets may have separate costs.
It is a set of instructions for a compatible AI assistant. It does not run by itself, connect accounts or install an MCP server.
Check that the named folder contains SKILL.md directly, not a second nested ZIP folder. Confirm that your editor supports skills and that its current settings allow this location. See the official documentation below.