Implement Angular 18+ features: Signals, standalone components, @defer blocks, SSR, zoneless change detection, new control flow syntax, and Material 3 integration.
Implements modern Angular 18+ features like Signals, standalone components, @defer blocks, SSR, zoneless change detection, new control flow syntax, and Material 3 integration. Use when building or migrating Angular apps to leverage the latest framework capabilities for improved performance and developer experience.
/plugin marketplace add pluginagentmarketplace/custom-plugin-angular/plugin install angular-development-assistant@pluginagentmarketplace-angularThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/README.mdassets/config.yamlassets/signals.template.tsreferences/GUIDE.mdreferences/README.mdreferences/SIGNALS.mdscripts/README.mdscripts/helper.pyscripts/migrate-standalone.shimport { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="increment()">{{ count() }}</button>
<p>Double: {{ double() }}</p>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal (auto-updates)
double = computed(() => this.count() * 2);
constructor() {
// Effect (side effects)
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(n => n + 1);
// or: this.count.set(this.count() + 1);
}
}
@Injectable({ providedIn: 'root' })
export class UserStore {
// Private state signal
private state = signal<{
users: User[];
loading: boolean;
error: string | null;
}>({
users: [],
loading: false,
error: null
});
// Public computed selectors
readonly users = computed(() => this.state().users);
readonly loading = computed(() => this.state().loading);
readonly error = computed(() => this.state().error);
readonly userCount = computed(() => this.users().length);
// Actions
async loadUsers() {
this.state.update(s => ({ ...s, loading: true }));
try {
const users = await this.http.get<User[]>('/api/users');
this.state.update(s => ({ ...s, users, loading: false }));
} catch (error) {
this.state.update(s => ({
...s,
error: error.message,
loading: false
}));
}
}
addUser(user: User) {
this.state.update(s => ({
...s,
users: [...s.users, user]
}));
}
}
// ❌ OLD: RxJS BehaviorSubject
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(map(u => u?.name ?? 'Guest'));
ngOnDestroy() {
this.userSubject.complete();
}
// ✅ NEW: Angular Signals
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
// No cleanup needed!
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, RouterModule], // Import dependencies directly
template: `
<h1>Dashboard</h1>
<router-outlet />
`
})
export class DashboardComponent {}
// 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(),
// Add other providers
]
});
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'users',
loadChildren: () => import('./users/users.routes').then(m => m.USERS_ROUTES)
}
];
# Automated migration to standalone
ng generate @angular/core:standalone
@defer {
<app-heavy-component />
} @placeholder {
<div class="loading-skeleton"></div>
}
// On viewport (when visible)
@defer (on viewport) {
<app-chart [data]="data" />
} @placeholder {
<div class="chart-placeholder"></div>
}
// On interaction (click or keydown)
@defer (on interaction) {
<app-advanced-editor />
} @placeholder {
<button>Load Editor</button>
}
// On hover
@defer (on hover) {
<app-tooltip [content]="tooltipContent" />
}
// On idle (browser idle)
@defer (on idle) {
<app-analytics-dashboard />
}
// On timer
@defer (on timer(5s)) {
<app-promotional-banner />
}
@defer (on interaction; prefetch on idle) {
<app-video-player [src]="videoUrl" />
} @loading (minimum 500ms; after 100ms) {
<app-spinner />
} @placeholder (minimum 1s) {
<button>Load Video Player</button>
} @error {
<p>Failed to load video player</p>
}
<div class="page">
<!-- Critical content loads immediately -->
<app-header />
<app-hero-section />
<!-- Defer below-the-fold content -->
@defer (on viewport) {
<app-features-section />
}
@defer (on viewport) {
<app-testimonials />
}
<!-- Defer interactive widgets -->
@defer (on interaction; prefetch on idle) {
<app-chat-widget />
} @placeholder {
<button class="chat-trigger">Chat with us</button>
}
</div>
// OLD
<div *ngIf="user">{{ user.name }}</div>
<div *ngIf="user; else loading">{{ user.name }}</div>
// NEW
@if (user) {
<div>{{ user.name }}</div>
}
@if (user) {
<div>{{ user.name }}</div>
} @else {
<div>Loading...</div>
}
// OLD
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
// NEW
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
} @empty {
<p>No items found</p>
}
// OLD
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">Loading...</p>
<p *ngSwitchCase="'error'">Error occurred</p>
<p *ngSwitchDefault>Success</p>
</div>
// NEW
@switch (status) {
@case ('loading') {
<p>Loading...</p>
}
@case ('error') {
<p>Error occurred</p>
}
@default {
<p>Success</p>
}
}
@if (users.length > 0) {
<ul>
@for (user of users; track user.id) {
<li>
{{ user.name }}
@if (user.isAdmin) {
<span class="badge">Admin</span>
}
</li>
} @empty {
<li>No users found</li>
}
</ul>
} @else {
<p>Loading users...</p>
}
# Add SSR to existing project
ng add @angular/ssr
# Or create new project with SSR
ng new my-app --ssr
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration() // Enable hydration
]
};
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({...})
export class MapComponent {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngOnInit() {
// Only run in browser
if (isPlatformBrowser(this.platformId)) {
this.initializeMap();
this.loadGoogleMapsAPI();
}
}
private initializeMap() {
// Browser-specific code
const map = new google.maps.Map(document.getElementById('map'));
}
}
import { Component, makeStateKey, TransferState } from '@angular/core';
const USERS_KEY = makeStateKey<User[]>('users');
@Component({...})
export class UsersComponent {
constructor(
private http: HttpClient,
private transferState: TransferState
) {}
loadUsers() {
// Check if data exists in transfer state (from SSR)
const users = this.transferState.get(USERS_KEY, null);
if (users) {
// Use cached data from SSR
return of(users);
}
// Fetch from API and cache for hydration
return this.http.get<User[]>('/api/users').pipe(
tap(users => this.transferState.set(USERS_KEY, users))
);
}
}
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection()
]
};
@Component({...})
export class MyComponent {
count = signal(0); // Signals work great with zoneless!
// Manual change detection when needed
constructor(private cdr: ChangeDetectorRef) {}
onManualUpdate() {
this.legacyProperty = 'new value';
this.cdr.markForCheck(); // Trigger change detection manually
}
}
ng add @angular/material
// styles.scss
@use '@angular/material' as mat;
$my-theme: mat.define-theme((
color: (
theme-type: light,
primary: mat.$azure-palette,
),
));
html {
@include mat.all-component-themes($my-theme);
}
// Dark mode
html.dark-theme {
$dark-theme: mat.define-theme((
color: (
theme-type: dark,
primary: mat.$azure-palette,
),
));
@include mat.all-component-colors($dark-theme);
}
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
standalone: true,
imports: [MatButtonModule, MatCardModule, MatIconModule],
template: `
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Material 3 Card</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Beautiful Material Design 3 components</p>
</mat-card-content>
<mat-card-actions>
<button mat-button>Action</button>
<button mat-raised-button color="primary">
<mat-icon>favorite</mat-icon>
Primary
</button>
</mat-card-actions>
</mat-card>
`
})
export class MaterialCardComponent {}
// BEFORE: NgModule
@NgModule({
declarations: [UserComponent, UserListComponent],
imports: [CommonModule, RouterModule],
exports: [UserComponent]
})
export class UserModule {}
// AFTER: Standalone
export const USER_ROUTES: Routes = [{
path: '',
loadComponent: () => import('./user.component').then(m => m.UserComponent)
}];
@Component({
standalone: true,
imports: [CommonModule, RouterModule]
})
export class UserComponent {}
// BEFORE: RxJS
class UserService {
private usersSubject = new BehaviorSubject<User[]>([]);
users$ = this.usersSubject.asObservable();
addUser(user: User) {
const current = this.usersSubject.value;
this.usersSubject.next([...current, user]);
}
}
// AFTER: Signals
class UserService {
users = signal<User[]>([]);
addUser(user: User) {
this.users.update(users => [...users, user]);
}
}
// Can reduce initial bundle by 40-60%!
@defer (on viewport) {
<app-heavy-chart-library />
}
// 20-30% performance improvement
provideExperimentalZonelessChangeDetection()
// Dramatically improves LCP, FCP, TTFB
provideClientHydration()
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.