diff --git a/.agents/skills/angular-developer/SKILL.md b/.agents/skills/angular-developer/SKILL.md index 6ab0e3d770..b0bf568944 100644 --- a/.agents/skills/angular-developer/SKILL.md +++ b/.agents/skills/angular-developer/SKILL.md @@ -1,6 +1,6 @@ --- name: angular-developer -description: Generates Angular code and provides architectural guidance. Trigger when creating projects, components, services, or HTTP communication, or for best practices on reactivity (signals, linkedSignal, resource, httpResource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling. +description: Generates Angular code and provides architectural guidance. Trigger when creating projects, components, services, or HTTP communication, or for best practices on reactivity (signals, linkedSignal, resource, httpResource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, naming conventions, or CLI tooling. license: MIT metadata: author: Copyright 2026 Google LLC @@ -20,7 +20,7 @@ metadata: If no guidelines are provided by the user, here are some default rules to follow when creating a new Angular project: 1. Use the latest stable version of Angular unless the user specifies otherwise. -2. Use Signals Forms for form management in new projects (available in Angular v21 and newer) [Find out more](references/signal-forms.md). +2. Use Signal Forms for form management in new projects (stable in Angular v22 and newer) [Find out more](references/signal-forms.md). **Execution Rules for `ng new`:** When asked to create a new Angular project, you must determine the correct execution command by following these strict steps: @@ -45,10 +45,11 @@ When asked to create a new Angular project, you must determine the correct execu When working with Angular components, consult the following references based on the task: -- **Fundamentals**: Anatomy, metadata, core concepts, and template control flow (@if, @for, @switch). Read [components.md](references/components.md) +- **Fundamentals**: Anatomy, metadata, core concepts, self-closing tags, and template control flow (@if, @for, @switch). Read [components.md](references/components.md) - **Inputs**: Signal-based inputs, transforms, and model inputs. Read [inputs.md](references/inputs.md) - **Outputs**: Signal-based outputs and custom event best practices. Read [outputs.md](references/outputs.md) - **Host Elements**: Host bindings and attribute injection. Read [host-elements.md](references/host-elements.md) +- **Naming Conventions**: Modern Angular v20+ naming style ("Intent over Role") for files, components, services, directives, pipes, and models. Read [naming-conventions.md](references/naming-conventions.md) If you require deeper documentation not found in the references above, read the documentation at `https://angular.dev/guide/components`. @@ -71,7 +72,7 @@ When communicating with backend services, use Angular HTTP APIs and consult the In most cases for new apps, **prefer signal forms**. When making a forms decision, analyze the project and consider the following guidelines: -- If the application is using v21 or newer and this is a new form, **prefer signal forms**. +- If the application is using v22 or newer and this is a new form, **prefer Signal Forms**. - For older applications or when working with existing forms, use the appropriate form type that matches the applications current form strategy. - **Signal Forms**: Use signals for form state management. Read [signal-forms.md](references/signal-forms.md) diff --git a/.agents/skills/angular-developer/references/angular-aria.md b/.agents/skills/angular-developer/references/angular-aria.md index 4241a2576a..6c618a8b33 100644 --- a/.agents/skills/angular-developer/references/angular-aria.md +++ b/.agents/skills/angular-developer/references/angular-aria.md @@ -17,7 +17,7 @@ Common ARIA attributes to target in CSS: --- -**CRITICAL**: Before using this package, it must be installed via the package manager. Confirm that it has been installed in the project. Use `npm install @angular/aria` to install if necessary. +**CRITICAL**: Before using this package, confirm that `@angular/aria` is installed. If necessary, install it using the package manager configured for the project. ## 1. Accordion @@ -331,9 +331,25 @@ Groups related controls (like text formatting). ```html
-
- - +
+ +
``` diff --git a/.agents/skills/angular-developer/references/components.md b/.agents/skills/angular-developer/references/components.md index 829a46d800..4b2c031d3a 100644 --- a/.agents/skills/angular-developer/references/components.md +++ b/.agents/skills/angular-developer/references/components.md @@ -48,6 +48,24 @@ To use a component, add it to the `imports` array of the consuming component and export class App {} ``` +### Self-Closing Tags + +Angular supports self-closing tags for custom components. + +**Rule:** Always use self-closing tags when a component does not contain projected content or child nodes: + +```html + + + + + + + + + +``` + ## Template Control Flow Angular uses built-in blocks for conditional rendering and loops. diff --git a/.agents/skills/angular-developer/references/creating-services.md b/.agents/skills/angular-developer/references/creating-services.md index 00ba49a480..92b5a757e3 100644 --- a/.agents/skills/angular-developer/references/creating-services.md +++ b/.agents/skills/angular-developer/references/creating-services.md @@ -10,21 +10,20 @@ You can generate a service using the Angular CLI: ng generate service my-data ``` -Or you can manually create a TypeScript class and decorate it with `@Service()`. +Or you can manually create a TypeScript class and decorate it with `@Service()`. For reactive state management, store data in a private `signal()` and expose it publicly via `.asReadonly()`: ```ts -import {Service} from '@angular/core'; +import {Service, signal} from '@angular/core'; @Service() export class BasicDataStore { - private data: string[] = []; + private readonly dataSignal = signal([]); - addData(item: string): void { - this.data.push(item); - } + // Expose state as a read-only signal to prevent direct external mutation + readonly data = this.dataSignal.asReadonly(); - getData(): string[] { - return [...this.data]; + addData(item: string): void { + this.dataSignal.update((items) => [...items, item]); } } ``` @@ -39,7 +38,7 @@ Using `@Service` is the recommended approach for most services. It tells Angular #### The `autoProvided` option -If you don't want to create a singleton of your service, you can set `@Service({autoProvided: false})` and declare the service a `providers` array. +If you don't want to create a singleton of your service, you can set `@Service({autoProvided: false})` and declare the service in a `providers` array. ## Injecting a Service @@ -55,36 +54,33 @@ import {BasicDataStore} from './basic-data-store.service'; selector: 'app-example', template: `
-

Data items: {{ dataStore.getData().length }}

+

Data items: {{ dataStore.data().length }}

`, }) export class Example { // Inject the service as a class field - dataStore = inject(BasicDataStore); + readonly dataStore = inject(BasicDataStore); } ``` ### Injecting into Another Service -Services can inject other services in the exact same way. +Services can inject other services in the exact same way. Use `computed()` to derive values from injected services reactively: ```ts -import {Injectable, inject} from '@angular/core'; +import {Service, computed, inject, signal} from '@angular/core'; import {AdvancedDataStore} from './advanced-data-store.service'; @Service() -export class BasicDataStore { +export class CombinedDataStore { // Injecting another service - private advancedDataStore = inject(AdvancedDataStore); - - private data: string[] = []; + private readonly advancedDataStore = inject(AdvancedDataStore); + private readonly dataSignal = signal([]); - getData(): string[] { - // Combine data from this service and the injected service - return [...this.data, ...this.advancedDataStore.getData()]; - } + // Combine reactive state from this service and the injected service + readonly allData = computed(() => [...this.dataSignal(), ...this.advancedDataStore.data()]); } ``` diff --git a/.agents/skills/angular-developer/references/define-routes.md b/.agents/skills/angular-developer/references/define-routes.md index e36bdf0681..72552038ac 100644 --- a/.agents/skills/angular-developer/references/define-routes.md +++ b/.agents/skills/angular-developer/references/define-routes.md @@ -38,6 +38,24 @@ Use `redirectTo` to point one path to another. { path: 'blog', component: Blog }, ``` +### Conditional Redirects + +Pass a function to `redirectTo` to apply logic when redirecting. + +```ts +{ + path: 'restaurant/:location/menu', + redirectTo: ({ params }) => { + const base = `/restaurant/${params['location']}/menu`; + const hour = new Date().getHours(); + + if (hour < 11) return `${base}/breakfast`; + if (hour < 17) return `${base}/lunch`; + return `${base}/dinner`; + }, +}, +``` + ## Page Titles Associate titles with routes for accessibility. Titles can be static or dynamic (via `ResolveFn` or a custom `TitleStrategy`). diff --git a/.agents/skills/angular-developer/references/effects.md b/.agents/skills/angular-developer/references/effects.md index 1175b1199a..cff11a5107 100644 --- a/.agents/skills/angular-developer/references/effects.md +++ b/.agents/skills/angular-developer/references/effects.md @@ -55,7 +55,7 @@ import { Component, afterRenderEffect, viewChild, ElementRef } from '@angular/co @Component({...}) export class Chart { - canvas = viewChild.required('canvas'); + canvas = viewChild.required>('canvas'); constructor() { afterRenderEffect({ @@ -63,10 +63,10 @@ export class Chart { earlyRead: () => { return this.canvas().nativeElement.getBoundingClientRect().width; }, - // 2. Write to the DOM (receives the result of the previous phase) + // 2. Write to the DOM (receives the previous phase result as a Signal) write: (width) => { // NEVER read from the DOM in the write phase. - setupChart(this.canvas().nativeElement, width); + setupChart(this.canvas().nativeElement, width()); } }); } diff --git a/.agents/skills/angular-developer/references/environment-configuration.md b/.agents/skills/angular-developer/references/environment-configuration.md index 311e935c78..8276df200a 100644 --- a/.agents/skills/angular-developer/references/environment-configuration.md +++ b/.agents/skills/angular-developer/references/environment-configuration.md @@ -81,10 +81,15 @@ Load the configuration before the application starts: ```ts import {Service, inject} from '@angular/core'; import {HttpClient} from '@angular/common/http'; +import {tap} from 'rxjs'; + +interface AppConfig { + apiUrl: string; +} @Service() export class AppConfigService { - private config!: {apiUrl: string}; + private config!: AppConfig; private readonly http = inject(HttpClient); diff --git a/.agents/skills/angular-developer/references/loading-strategies.md b/.agents/skills/angular-developer/references/loading-strategies.md index 848bff1245..f4d269f908 100644 --- a/.agents/skills/angular-developer/references/loading-strategies.md +++ b/.agents/skills/angular-developer/references/loading-strategies.md @@ -24,7 +24,7 @@ Use `loadComponent` to fetch the component on demand. ```ts { path: 'admin', - loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)`, + loadComponent: () => import('./admin').then(m => m.Admin), } ``` @@ -39,6 +39,8 @@ Use `loadChildren` to fetch a set of routes. } ``` +Return the `import()` promise directly only when the loaded file uses a `default` export. + ## Injection Context and Lazy Loading Loader functions run within the **injection context** of the current route. This allows you to call `inject()` to make context-aware loading decisions. diff --git a/.agents/skills/angular-developer/references/mcp.md b/.agents/skills/angular-developer/references/mcp.md index 5446087691..f0649f1d3b 100644 --- a/.agents/skills/angular-developer/references/mcp.md +++ b/.agents/skills/angular-developer/references/mcp.md @@ -9,24 +9,15 @@ When the MCP server is enabled, AI agents have access to the following tools: | Name | Description | | :-------------------------- | :-------------------------------------------------------------------------------------------------------- | | `ai_tutor` | Launches an interactive AI-powered Angular tutor. | +| `devserver.start` | Asynchronously starts a dev server (`ng serve`). Returns immediately. | +| `devserver.stop` | Stops the dev server. | +| `devserver.wait_for_build` | Returns the logs of the most recent build in a running dev server. | | `get_best_practices` | Retrieves the Angular Best Practices Guide (crucial for standalone components, typed forms, etc.). | | `list_projects` | Lists all applications and libraries in the workspace by reading `angular.json`. | | `onpush_zoneless_migration` | Analyzes code and provides a plan to migrate it to `OnPush` change detection (prerequisite for zoneless). | +| `run_target` | Executes a configured target. | | `search_documentation` | Searches the official documentation at `https://angular.dev`. | -## Experimental Tools - -Some tools must be enabled explicitly using the `--experimental-tool` (or `-E`) flag. - -| Name | Description | -| :------------------------- | :-------------------------------------------------------------------- | -| `build` | Performs a one-off build using `ng build`. | -| `devserver.start` | Asynchronously starts a dev server (`ng serve`). Returns immediately. | -| `devserver.stop` | Stops the dev server. | -| `devserver.wait_for_build` | Returns the logs of the most recent build in a running dev server. | -| `e2e` | Executes end-to-end tests. | -| `test` | Runs the project's unit tests. | - ## Configuration To use the MCP server, you configure your host environment (IDE or CLI) to run `npx @angular/cli mcp`. @@ -97,10 +88,9 @@ You can pass arguments to the MCP server in the `args` array of your configurati - `--read-only`: Only registers tools that do not modify the project. - `--local-only`: Only registers tools that do not require an internet connection. -- `--experimental-tool` (`-E`): Enables specific experimental tools (e.g., `-E build`, `-E devserver`). -Example for read-only mode with experimental tools enabled: +Example for read-only mode: ```json -"args": ["-y", "@angular/cli", "mcp", "--read-only", "-E", "build", "-E", "test"] +"args": ["-y", "@angular/cli", "mcp", "--read-only"] ``` diff --git a/.agents/skills/angular-developer/references/naming-conventions.md b/.agents/skills/angular-developer/references/naming-conventions.md new file mode 100644 index 0000000000..c0c1fcb84e --- /dev/null +++ b/.agents/skills/angular-developer/references/naming-conventions.md @@ -0,0 +1,76 @@ +# Angular Naming Conventions (Angular v20+ Style Guide) + +This skill enforces Angular naming conventions for components, services, directives, pipes, and models. While it promotes the modern **"Intent over Role"** philosophy introduced in Angular v20, **it must respect existing project configurations first**. + +--- + +## Core Principles + +1. **Prioritize Existing Conventions**: Before generating or refactoring files, check the existing project files, `angular.json` configuration, and ESLint rules. **Do not force suffixless naming on projects that rely on standard suffixes.** +2. **Remove Role Suffixes (Modern Projects Only)**: In projects configured for "Intent over Role" or newly bootstrapped v20+ projects, filenames no longer include functional extensions like `.component.ts`, `.service.ts`, or `.directive.ts`. Corresponding TypeScript classes drop suffixes like `Component`, `Service`, or `Directive`. +3. **Intent/Purpose-Based Naming**: When suffixless naming is active, name files and classes based on their specific domain, responsibility, or business purpose (e.g., `-data`, `-store`, `-api`, or `-formatter`). +4. **Folder Location as Context**: Lean on folder hierarchy (`core/`, `features/`, `shared/`) and IDE capabilities to identify the technical role of files, rather than encoding that context within the file name. +5. **Interface/Model Exception**: Interfaces and data models still retain the `.model.ts` suffix to clearly declare type contracts. + +--- + +## Recommended Project Structure & Naming Rules + +### 1. File/Identifier Matching & Consistency + +- **Hyphens in Filenames**: Continue using kebab-case (hyphens) to separate words in filenames (e.g., `product-list.ts`). +- **Identifier Matching**: Filenames must align directly with the primary TypeScript class/identifier (e.g., `product-list.ts` contains `class ProductList`). +- **Unified Filenames**: If using split template or style files, keep names identical to the main TypeScript file: + - `product-list.ts` + - `product-list.html` + - `product-list.css` +- **Test Files**: Continue to use the same base name with the `.spec.ts` suffix (e.g., `product-list.spec.ts` for `product-list.ts`). + +### 2. Core Directory (Application Foundation) + +Houses singleton services, global state, and system-wide models. + +- **Services (Logic/State)**: + - _Old_: `auth.service.ts` (Class: `AuthService`) + - _New_: `auth.ts` (Class: `AuthService`) + - _Alternative (Intent-specific)_: Use descriptive domain-purpose suffixes like `[domain]-data.ts`, `[domain]-store.ts`, or `[domain]-data-client.ts` (e.g., `auth-data.ts` / `AuthData`, `user-data-client.ts` / `UserDataClient`). +- **Models**: Retain the `.model.ts` suffix for data shapes. + - _Example_: `user.model.ts` (Interface: `User`) + +### 3. Features Directory (Domain Business Logic) + +Organize files into feature-specific folders containing components, local services, and routes related to that domain. + +- **Main Feature Component**: Name the main feature component after the route or feature itself. + - _Example_: `features/profile/profile.ts` (Class: `Profile`) +- **Feature Sub-Components**: Name sub-components based on their display or functional role. + - _Example_: `features/profile/components/profile-header.ts` (Class: `ProfileHeader`) + - _Example_: `features/projects/components/project-card.ts` (Class: `ProjectCard`) +- **Feature Services**: Name feature services based on feature-specific data or state needs. + - _Example_: `features/projects/projects-data.ts` (Class: `ProjectsData`) + +### 4. Shared Directory (Reusable UI Toolkit) + +Store pure, presentational elements and helpers with zero business logic in a shared folder. + +- **Shared Components**: Name shared components based on their reusable UI role. + - _Example_: `shared/components/button/button.ts` (Class: `Button`) + - _Example_: `shared/components/spinner/spinner.ts` (Class: `Spinner`) +- **Shared Pipes**: Name shared pipes according to their formatting purpose. + - _Example_: `shared/pipes/format-date.ts` (Class: `FormatDate`) +- **Shared Directives**: Name directives according to the behavior they attach to elements. + - _Old_: `highlight.directive.ts` (Class: `HighlightDirective`) + - _New_: `highlight.ts` (Class: `Highlight`) + +--- + +## Best Practices & Coexistence Rules + +- **How to Determine the Style in Use**: + 1. Inspect adjacent files in the target directory (do they end in `.component.ts` or `.ts`?). + 2. Check `angular.json` for custom schematics options that might configure suffix behaviors. + 3. If unsure, use the traditional role suffix style (`.component.ts`, `.service.ts`) as it is the safest default in the Angular ecosystem. +- **Avoid Namespace Collisions**: Without role suffixes, files like `user.ts` (component) and `user.model.ts` (model) can collide if they both declare a class/interface named `User`. + - To prevent this use more specific, intent-based names for components (e.g. `class UserProfile` in `user-profile.ts` or `class UserDetail` in `user-detail.ts`) while keeping the simple domain name for the interface (`interface User` in `user.model.ts`). +- **Consistency Check**: Do not mix old suffix styles and new suffixless styles in the same feature folder or module. Keep existing legacy code as-is unless migrating the entire module to the modern structure. +- **Lean on the IDE**: Rely on modern IDE code navigation (e.g., "Go to Definition" or fuzzy searches for class names like `AuthData` or `ProfileHeader`) and file type icons rather than visual scan of suffix strings. diff --git a/.agents/skills/angular-developer/references/pipes.md b/.agents/skills/angular-developer/references/pipes.md index f3b50fb93a..a3182a29a4 100644 --- a/.agents/skills/angular-developer/references/pipes.md +++ b/.agents/skills/angular-developer/references/pipes.md @@ -54,10 +54,10 @@ export class KebabCasePipe implements PipeTransform { ```ts // formatter.service.ts — import the function, NOT the pipe -import {Injectable} from '@angular/core'; +import {Service} from '@angular/core'; import {toKebabCase} from './kebab-case'; -@Injectable({providedIn: 'root'}) +@Service() export class FormatterService { toSlug(title: string): string { return toKebabCase(title); @@ -80,10 +80,10 @@ Inject `LOCALE_ID` to get the current locale and pass it to the function. ```ts // CORRECT — use formatNumber instead of injecting DecimalPipe -import {Injectable, LOCALE_ID, inject} from '@angular/core'; +import {Service, LOCALE_ID, inject} from '@angular/core'; import {formatNumber} from '@angular/common'; -@Injectable({providedIn: 'root'}) +@Service() export class PriceService { private locale = inject(LOCALE_ID); @@ -95,10 +95,10 @@ export class PriceService { ```ts // WRONG — do not inject pipe classes -import {Injectable} from '@angular/core'; +import {Service, inject} from '@angular/core'; import {DecimalPipe} from '@angular/common'; -@Injectable({providedIn: 'root'}) +@Service() export class PriceService { // ❌ DecimalPipe is not designed to be injected private pipe = inject(DecimalPipe); diff --git a/.agents/skills/angular-developer/references/router-testing.md b/.agents/skills/angular-developer/references/router-testing.md index 35328e41e4..9f6665e760 100644 --- a/.agents/skills/angular-developer/references/router-testing.md +++ b/.agents/skills/angular-developer/references/router-testing.md @@ -12,7 +12,7 @@ The `RouterTestingHarness` is the primary tool for testing routing scenarios. Yo ```ts import {TestBed} from '@angular/core/testing'; -import {provideRouter} from '@angular/router'; +import {provideRouter, Router} from '@angular/router'; import {RouterTestingHarness} from '@angular/router/testing'; import {Dashboard} from './dashboard.component'; import {HeroDetail} from './hero-detail.component'; @@ -41,7 +41,7 @@ describe('Dashboard Component Routing', () => { ### Key Concepts 1. **`provideRouter([...])`**: Provide a test-specific routing configuration. This should include the routes necessary for the component-under-test to function correctly. -2. **`RouterTestingHarness.create()`**: Asynchronously creates and initializes the harness and performs an initial navigation to the root URL (`/`). +2. **`RouterTestingHarness.create(initialUrl?)`**: Asynchronously creates the harness and optionally performs an initial navigation. ## Writing Router Tests @@ -62,13 +62,14 @@ it('should navigate to a hero detail when a hero is selected', async () => { await harness.fixture.whenStable(); // 2. Assert on the URL - expect(harness.router.url).toEqual('/heroes/42'); + const router = TestBed.inject(Router); + expect(router.url).toEqual('/heroes/42'); // 3. Get the activated component after navigation - const heroDetail = await harness.getHarness(HeroDetail); + const heroDetail = harness.routeDebugElement?.componentInstance as HeroDetail; // 4. Assert on the state of the new component - expect(await heroDetail.componentInstance.hero.name).toBe('Test Hero'); + expect(heroDetail.hero.name).toBe('Test Hero'); }); it('should get the activated component directly', async () => { @@ -82,6 +83,6 @@ it('should get the activated component directly', async () => { ### Best Practices - **Navigate with the Harness:** Always use `harness.navigateByUrl()` to simulate navigation. This method returns a promise that resolves with the instance of the activated component. -- **Access the Router State:** Use `harness.router` to access the live router instance and assert on its state (e.g., `harness.router.url`). -- **Get Activated Components:** Use `harness.getHarness(ComponentType)` to get an instance of a component harness for the currently activated routed component, or `harness.routeDebugElement` to get the `DebugElement`. +- **Access the Router State:** Inject `Router` from `TestBed` to inspect the live router state. +- **Get Activated Components:** Use the component returned by `navigateByUrl(url, ComponentType)`. After application-driven navigation, read `harness.routeDebugElement?.componentInstance`. - **Wait for Stability:** After performing an action that causes navigation, always `await harness.fixture.whenStable()` to ensure the routing is complete before making assertions. diff --git a/skills-lock.json b/skills-lock.json index 64e946a890..852b42707e 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -2,10 +2,10 @@ "version": 1, "skills": { "angular-developer": { - "source": "angular/skills", + "source": "angular/angular", "sourceType": "github", - "skillPath": "angular-developer/SKILL.md", - "computedHash": "ded1e95fb8d75d60901201665c6ab7e348eec3b79a6c53824c4a30a487d99f58" + "skillPath": "skills/dev-skills/angular-developer/SKILL.md", + "computedHash": "0850aa966b6a7da1151daa9d0ef2081bf397f052573a35c1643fb92b948f0f00" }, "karpathy-guidelines": { "source": "multica-ai/andrej-karpathy-skills",