--- name: "angular" description: "Angular Expert workflow skill. Use this skill when the user needs Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns and the operator should preserve the upstream workflow, copied support files, and provenance before merging or handing off." version: "0.0.1" category: "frontend" tags: - "angular" - "modern" - "v20" - "expert" - "deep" - "knowledge" - "signals" - "standalone" - "omni-enhanced" complexity: "advanced" risk: "caution" tools: - "codex-cli" - "claude-code" - "cursor" - "gemini-cli" - "opencode" source: "omni-team" author: "Omni Skills Team" date_added: "2026-04-14" date_updated: "2026-04-23" source_type: "omni-curated" maintainer: "Omni Skills Team" family_id: "angular" family_name: "Angular Expert" variant_id: "omni" variant_label: "Omni Curated" is_default_variant: true derived_from: "skills/angular" upstream_skill: "skills/angular" upstream_author: "sickn33" upstream_source: "community" upstream_pr: "126" upstream_head_repo: "diegosouzapw/awesome-omni-skills" upstream_head_sha: "032affbbd536f09d7636f0fbbfd35093380dae89" curation_surface: "skills_omni" enhanced_origin: "omni-skills-private" source_repo: "diegosouzapw/awesome-omni-skills" replaces: - "angular" --- # Angular Expert ## Overview This public intake copy packages `plugins/antigravity-awesome-skills-claude/skills/angular` from `https://github.com/sickn33/antigravity-awesome-skills` into the native Omni Skills editorial shape without hiding its origin. Use it when the operator needs the upstream workflow, support files, and repository context to stay intact while the public validator and private enhancer continue their normal downstream flow. This intake keeps the copied upstream files intact and uses `metadata.json` plus `ORIGIN.md` as the provenance anchor for review. # Angular Expert Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns. Imported source sections that did not map cleanly to the public headings are still preserved below or in the support files. Notable imported sections: Safety, Angular Version Timeline, 1. Signals: The New Reactive Primitive, 2. Standalone Components, 3. Zoneless Angular, 4. Server-Side Rendering & Hydration. ## When to Use This Skill Use this section as the trigger filter. It should make the activation boundary explicit before the operator loads files, runs commands, or opens a pull request. - 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 ## Operating Table | Situation | Start here | Why it matters | | --- | --- | --- | | First-time use | `metadata.json` | Confirms repository, branch, commit, and imported path before touching the copied workflow | | Provenance review | `ORIGIN.md` | Gives reviewers a plain-language audit trail for the imported source | | Workflow execution | `README.md` | Starts with the smallest copied file that materially changes execution | | Supporting context | `metadata.json` | Adds the next most relevant copied source file without loading the entire package | | Handoff decision | `## Related Skills` | Helps the operator switch to a stronger native skill when the task drifts | ## Workflow This workflow is intentionally editorial and operational at the same time. It keeps the imported source useful to the operator while still satisfying the public intake standards that feed the downstream enhancer flow. 1. Assess the Angular version and project structure 2. Apply modern patterns (Signals, Standalone, Zoneless) 3. Implement with proper typing and reactivity 4. Validate with build and tests 5. Confirm the user goal, the scope of the imported workflow, and whether this skill is still the right router for the task. 6. Read the overview and provenance files before loading any copied upstream support files. 7. Load only the references, examples, prompts, or scripts that materially change the outcome for the current request. ### Imported Workflow Notes #### Imported: Instructions 1. Assess the Angular version and project structure 2. Apply modern patterns (Signals, Standalone, Zoneless) 3. Implement with proper typing and reactivity 4. Validate with build and tests #### Imported: Safety - Always test changes in development before production - Gradual migration for existing apps (don't big-bang refactor) - Keep backward compatibility during transitions --- ## Examples ### Example 1: Ask for the upstream workflow directly ```text Use @angular to handle . Start from the copied upstream workflow, load only the files that change the outcome, and keep provenance visible in the answer. ``` **Explanation:** This is the safest starting point when the operator needs the imported workflow, but not the entire repository. ### Example 2: Ask for a provenance-grounded review ```text Review @angular against metadata.json and ORIGIN.md, then explain which copied upstream files you would load first and why. ``` **Explanation:** Use this before review or troubleshooting when you need a precise, auditable explanation of origin and file selection. ### Example 3: Narrow the copied support files before execution ```text Use @angular for . Load only the copied references, examples, or scripts that change the outcome, and name the files explicitly before proceeding. ``` **Explanation:** This keeps the skill aligned with progressive disclosure instead of loading the whole copied package by default. ### Example 4: Build a reviewer packet ```text Review @angular using the copied upstream files plus provenance, then summarize any gaps before merge. ``` **Explanation:** This is useful when the PR is waiting for human review and you want a repeatable audit packet. ## Best Practices Treat the generated public skill as a reviewable packaging layer around the upstream repository. The goal is to keep provenance explicit and load only the copied source material that materially improves execution. - Keep the imported skill grounded in the upstream repository; do not invent steps that the source material cannot support. - Prefer the smallest useful set of support files so the workflow stays auditable and fast to review. - Keep provenance, source commit, and imported file paths visible in notes and PR descriptions. - Point directly at the copied upstream files that justify the workflow instead of relying on generic review boilerplate. - Treat generated examples as scaffolding; adapt them to the concrete task before execution. - Route to a stronger native skill when architecture, debugging, design, or security concerns become dominant. ## Troubleshooting ### Problem: The operator skipped the imported context and answered too generically **Symptoms:** The result ignores the upstream workflow in `plugins/antigravity-awesome-skills-claude/skills/angular`, fails to mention provenance, or does not use any copied source files at all. **Solution:** Re-open `metadata.json`, `ORIGIN.md`, and the most relevant copied upstream files. Load only the files that materially change the answer, then restate the provenance before continuing. ### Problem: The imported workflow feels incomplete during review **Symptoms:** Reviewers can see the generated `SKILL.md`, but they cannot quickly tell which references, examples, or scripts matter for the current task. **Solution:** Point at the exact copied references, examples, scripts, or assets that justify the path you took. If the gap is still real, record it in the PR instead of hiding it. ### Problem: The task drifted into a different specialization **Symptoms:** The imported skill starts in the right place, but the work turns into debugging, architecture, design, security, or release orchestration that a native skill handles better. **Solution:** Use the related skills section to hand off deliberately. Keep the imported provenance visible so the next skill inherits the right context instead of starting blind. ### Imported Troubleshooting Notes #### Imported: 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()` | ## Related Skills - `@00-andruia-consultant` - Use when the work is better handled by that native specialization after this imported skill establishes context. - `@10-andruia-skill-smith` - Use when the work is better handled by that native specialization after this imported skill establishes context. - `@20-andruia-niche-intelligence` - Use when the work is better handled by that native specialization after this imported skill establishes context. - `@3d-web-experience` - Use when the work is better handled by that native specialization after this imported skill establishes context. ## Additional Resources Use this support matrix and the linked files below as the operator packet for this imported skill. They should reflect real copied source material, not generic scaffolding. | Resource family | What it gives the reviewer | Example path | | --- | --- | --- | | `references` | copied reference notes, guides, or background material from upstream | `references/n/a` | | `examples` | worked examples or reusable prompts copied from upstream | `examples/n/a` | | `scripts` | upstream helper scripts that change execution or validation | `scripts/n/a` | | `agents` | routing or delegation notes that are genuinely part of the imported package | `agents/n/a` | | `assets` | supporting assets or schemas copied from the source package | `assets/n/a` | - [README.md](README.md) - [metadata.json](metadata.json) ### Imported Reference Notes #### Imported: Resources - [Angular.dev Documentation](https://angular.dev) - [Angular Signals Guide](https://angular.dev/guide/signals) - [Angular SSR Guide](https://angular.dev/guide/ssr) - [Angular Update Guide](https://angular.dev/update-guide) - [Angular Blog](https://blog.angular.dev) --- #### Imported: 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 | --- #### Imported: 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 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()}`); }); ``` ### Signal-Based Inputs and Outputs ```typescript import { Component, input, output, model } from "@angular/core"; @Component({ selector: "app-user-card", standalone: true, template: `

{{ name() }}

{{ role() }}
`, }) export class UserCardComponent { // Signal inputs (read-only) id = input.required(); name = input.required(); role = input("User"); // With default // Output select = output(); // Two-way binding (model) isSelected = model(false); } // Usage: // ``` ### Signal Queries (ViewChild/ContentChild) ```typescript import { Component, viewChild, viewChildren, contentChild, } from "@angular/core"; @Component({ selector: "app-container", standalone: true, template: ` `, }) export class ContainerComponent { // Signal-based queries searchInput = viewChild("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` | --- #### Imported: 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 imports template: `
Home About
`, }) export class HeaderComponent {} ``` ### Bootstrapping Without NgModule ```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()], }); ``` ### Lazy Loading Standalone Components ```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), }, ]; ``` --- #### Imported: 3. Zoneless Angular Zoneless applications don't use zone.js, improving performance and debugging. ### Enabling Zoneless Mode ```typescript // main.ts import { 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: `
Count: {{ count() }}
`, }) 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 --- #### Imported: 4. Server-Side Rendering & Hydration ### SSR Setup with Angular CLI ```bash ng add @angular/ssr ``` ### Hydration Configuration ```typescript // app.config.ts import { 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: ` @defer (hydrate on viewport) { } @defer (hydrate on interaction) { } `, }) 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 | --- #### Imported: 5. Modern Routing Patterns ### Functional Route Guards ```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], }, ]; ``` ### 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 = (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']))); } ``` --- #### Imported: 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 needed private http = inject(HttpClient); private userService = inject(UserService); // Works in any injection context users = toSignal(this.userService.getUsers()); } ``` ### Injection Tokens for Configuration ```typescript import { InjectionToken, inject } from "@angular/core"; // Define token export const API_BASE_URL = new InjectionToken("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}`); } } ``` --- #### Imported: 7. Component Composition & Reusability ### Content Projection (Slots) ```typescript @Component({ selector: 'app-card', template: `
` }) export class CardComponent {} // Usage

Title

Body content

``` ### 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: `` }) export class ButtonComponent {} ``` --- #### Imported: 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 signals private _user = signal(null); private _theme = signal<"light" | "dark">("light"); private _notifications = signal([]); // 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]); } } ``` ### 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); // State private _products = signal([]); 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("/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); } } ``` --- #### Imported: 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: `
`, }) 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 validation isValid = computed(() => this.name().length > 0 && this.email().includes('@') ); submit() { if (this.isValid()) { console.log({ name: this.name(), email: this.email() }); } } } ``` --- #### Imported: 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: ` @defer (on viewport) { } @placeholder {
} @loading (minimum 200ms) { } @error {

Failed to load chart

} ` }) ``` ### NgOptimizedImage ```typescript import { NgOptimizedImage } from '@angular/common'; @Component({ imports: [NgOptimizedImage], template: ` ` }) ``` --- #### Imported: 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; 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; let componentRef: ComponentRef; 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"); }); }); ``` --- #### Imported: 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 | --- #### Imported: 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.