Browse Source

feat(agents): update angular-developer skill

pull/7632/head
KenTandrian 3 days ago
parent
commit
0a87c5fdd6
  1. 23
      .agents/skills/angular-developer/SKILL.md
  2. 2
      .agents/skills/angular-developer/references/angular-animations.md
  3. 269
      .agents/skills/angular-developer/references/angular-aria.md
  4. 2
      .agents/skills/angular-developer/references/cli.md
  5. 6
      .agents/skills/angular-developer/references/component-harnesses.md
  6. 24
      .agents/skills/angular-developer/references/creating-services.md
  7. 6
      .agents/skills/angular-developer/references/data-resolvers.md
  8. 22
      .agents/skills/angular-developer/references/di-fundamentals.md
  9. 73
      .agents/skills/angular-developer/references/e2e-testing.md
  10. 2
      .agents/skills/angular-developer/references/effects.md
  11. 132
      .agents/skills/angular-developer/references/environment-configuration.md
  12. 2
      .agents/skills/angular-developer/references/hierarchical-injectors.md
  13. 8
      .agents/skills/angular-developer/references/host-elements.md
  14. 108
      .agents/skills/angular-developer/references/http-client.md
  15. 6
      .agents/skills/angular-developer/references/injection-context.md
  16. 14
      .agents/skills/angular-developer/references/inputs.md
  17. 8
      .agents/skills/angular-developer/references/linked-signal.md
  18. 10
      .agents/skills/angular-developer/references/outputs.md
  19. 145
      .agents/skills/angular-developer/references/pipes.md
  20. 26
      .agents/skills/angular-developer/references/reactive-forms.md
  21. 9
      .agents/skills/angular-developer/references/resource.md
  22. 4
      .agents/skills/angular-developer/references/router-testing.md
  23. 38
      .agents/skills/angular-developer/references/signal-forms.md
  24. 9
      .agents/skills/angular-developer/references/testing-fundamentals.md
  25. 2
      skills-lock.json

23
.agents/skills/angular-developer/SKILL.md

@ -1,6 +1,6 @@
--- ---
name: angular-developer name: angular-developer
description: Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), 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, or CLI tooling.
license: MIT license: MIT
metadata: metadata:
author: Copyright 2026 Google LLC author: Copyright 2026 Google LLC
@ -17,7 +17,7 @@ metadata:
## Creating New Projects ## Creating New Projects
If no guidelines are provided by the user, here are same default rules to follow when creating a new Angular project: 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. 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 Signals Forms for form management in new projects (available in Angular v21 and newer) [Find out more](references/signal-forms.md).
@ -61,12 +61,18 @@ When managing state and data reactivity, use Angular Signals and consult the fol
- **Async Reactivity (`resource`)**: Fetching asynchronous data directly into signal state. Read [resource.md](references/resource.md) - **Async Reactivity (`resource`)**: Fetching asynchronous data directly into signal state. Read [resource.md](references/resource.md)
- **Side Effects (`effect`)**: Logging, third-party DOM manipulation (`afterRenderEffect`), and when NOT to use effects. Read [effects.md](references/effects.md) - **Side Effects (`effect`)**: Logging, third-party DOM manipulation (`afterRenderEffect`), and when NOT to use effects. Read [effects.md](references/effects.md)
## HTTP Communication
When communicating with backend services, use Angular HTTP APIs and consult the following reference:
- **HTTP Client and Resources**: `provideHttpClient`, `HttpClient`, interceptors, and `httpResource`. Read [http-client.md](references/http-client.md)
## Forms ## Forms
In most cases for new apps, **prefer signal forms**. When making a forms decision, analyze the project and consider the following guidelines: 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 v21 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. - 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) - **Signal Forms**: Use signals for form state management. Read [signal-forms.md](references/signal-forms.md)
- **Template-driven forms**: Use for simple forms. Read [template-driven-forms.md](references/template-driven-forms.md) - **Template-driven forms**: Use for simple forms. Read [template-driven-forms.md](references/template-driven-forms.md)
@ -82,6 +88,12 @@ When implementing dependency injection in Angular, follow these guidelines:
- **Injection Context**: Where `inject()` is allowed, `runInInjectionContext`, and `assertInInjectionContext`. Read [injection-context.md](references/injection-context.md) - **Injection Context**: Where `inject()` is allowed, `runInInjectionContext`, and `assertInInjectionContext`. Read [injection-context.md](references/injection-context.md)
- **Hierarchical Injectors**: The `EnvironmentInjector` vs `ElementInjector`, resolution rules, modifiers (`optional`, `skipSelf`), and `providers` vs `viewProviders`. Read [hierarchical-injectors.md](references/hierarchical-injectors.md) - **Hierarchical Injectors**: The `EnvironmentInjector` vs `ElementInjector`, resolution rules, modifiers (`optional`, `skipSelf`), and `providers` vs `viewProviders`. Read [hierarchical-injectors.md](references/hierarchical-injectors.md)
## Pipes
When formatting values in templates, creating custom pipes, or reusing pipe-like logic in TypeScript, consult the following reference. Prefer pipes in templates; outside templates, avoid injecting pipe classes just to call `transform()`.
- **Pipes**: Built-in pipe imports, custom pipe naming and implementation, pure vs impure pipes, and TypeScript reuse patterns using standalone formatting functions or extracted plain functions. Read [pipes.md](references/pipes.md)
## Angular Aria ## Angular Aria
When building accessible custom components for any of the following patterns: Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid, consult the following reference: When building accessible custom components for any of the following patterns: Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid, consult the following reference:
@ -119,7 +131,7 @@ When writing or updating tests, consult the following references based on the ta
- **Fundamentals**: Best practices for unit testing (Vitest), async patterns, and `TestBed`. Read [testing-fundamentals.md](references/testing-fundamentals.md) - **Fundamentals**: Best practices for unit testing (Vitest), async patterns, and `TestBed`. Read [testing-fundamentals.md](references/testing-fundamentals.md)
- **Component Harnesses**: Standard patterns for robust component interaction. Read [component-harnesses.md](references/component-harnesses.md) - **Component Harnesses**: Standard patterns for robust component interaction. Read [component-harnesses.md](references/component-harnesses.md)
- **Router Testing**: Using `RouterTestingHarness` for reliable navigation tests. Read [router-testing.md](references/router-testing.md) - **Router Testing**: Using `RouterTestingHarness` for reliable navigation tests. Read [router-testing.md](references/router-testing.md)
- **End-to-End (E2E) Testing**: Best practices for E2E tests with Cypress. Read [e2e-testing.md](references/e2e-testing.md) - **End-to-End (E2E) Testing**: Setting up and running E2E tests. Read [e2e-testing.md](references/e2e-testing.md)
## Tooling ## Tooling
@ -128,3 +140,4 @@ When working with Angular tooling, consult the following references:
- **Angular CLI**: Creating applications, generating code (components, routes, services), serving, and building. Read [cli.md](references/cli.md) - **Angular CLI**: Creating applications, generating code (components, routes, services), serving, and building. Read [cli.md](references/cli.md)
- **Code Modernization**: Automatically refactoring to modern standards using migrations. Read [migrations.md](references/migrations.md) - **Code Modernization**: Automatically refactoring to modern standards using migrations. Read [migrations.md](references/migrations.md)
- **Angular MCP Server**: Available tools, configuration, and experimental features. Read [mcp.md](references/mcp.md) - **Angular MCP Server**: Available tools, configuration, and experimental features. Read [mcp.md](references/mcp.md)
- **Environment Configuration**: Strategies for build-time and runtime configuration. Read [environment-configuration.md](references/environment-configuration.md)

2
.agents/skills/angular-developer/references/angular-animations.md

@ -155,6 +155,6 @@ import {trigger, state, style, animate, transition} from '@angular/animations';
template: `<div [@openClose]="isOpen() ? 'open' : 'closed'">...</div>`, template: `<div [@openClose]="isOpen() ? 'open' : 'closed'">...</div>`,
}) })
export class OpenClose { export class OpenClose {
isOpen = signal(true); protected readonly isOpen = signal(true);
} }
``` ```

269
.agents/skills/angular-developer/references/angular-aria.md

@ -44,11 +44,11 @@ export class App {
```html ```html
<div ngAccordionGroup [multiExpandable]="false"> <div ngAccordionGroup [multiExpandable]="false">
<div class="accordion-item"> <div class="accordion-item">
<button ngAccordionTrigger panelId="panel-1" class="accordion-header"> <button ngAccordionTrigger [panel]="panel1" class="accordion-header">
Section 1 Section 1
<span class="icon"></span> <span class="icon"></span>
</button> </button>
<div ngAccordionPanel panelId="panel-1" class="accordion-panel"> <div ngAccordionPanel #panel1="ngAccordionPanel" class="accordion-panel">
<ng-template ngAccordionContent> <ng-template ngAccordionContent>
<p>Lazy loaded content here.</p> <p>Lazy loaded content here.</p>
</ng-template> </ng-template>
@ -98,7 +98,7 @@ export class App {
```html ```html
<!-- horizontal or vertical orientation --> <!-- horizontal or vertical orientation -->
<ul ngListbox [(values)]="selectedItems" orientation="horizontal" [multi]="true"> <ul ngListbox [(value)]="selectedItems" orientation="horizontal" [multi]="true">
<li ngOption value="apple" class="option">Apple</li> <li ngOption value="apple" class="option">Apple</li>
<li ngOption value="banana" class="option">Banana</li> <li ngOption value="banana" class="option">Banana</li>
</ul> </ul>
@ -126,37 +126,69 @@ Target `[aria-selected="true"]` for selected state and `:focus-visible` or `[dat
## 3. Combobox, Select, and Multiselect ## 3. Combobox, Select, and Multiselect
These patterns combine `ngCombobox` with a popup containing an `ngListbox`. These patterns combine the `ngCombobox` directive (applied directly to the trigger/combobox element) with a popup containing an `ngListbox` widget.
- **Combobox**: Text input + popup (used for Autocomplete). - **Combobox (Autocomplete)**: Applied to an `<input ngCombobox>` element. Ideal when typing filters the list.
- **Select**: Readonly Combobox + single-select Listbox. - **Select**: Applied to a focusable wrapper like a `<div ngCombobox>` or `<button ngCombobox>` element. Users select from a list of options.
- **Multiselect**: Readonly Combobox + multi-select Listbox. - **Multiselect**: A Combobox or Select paired with a multi-select `ngListbox`.
**Usage:** The Combobox is a low-level primitive directive that synchronizes a text input with a popup, serving as the foundational logic for autocomplete, select, and multiselect patterns. Use it specifically for building custom filtering, unique selection requirements, or specialized input-to-popup coordination that deviates from standard, documented components.
**Imports:** **Imports:**
``` ```ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox'; import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox'; import {Listbox, Option} from '@angular/aria/listbox';
``` ```
**Directives:** `ngCombobox`, `ngComboboxInput`, `ngComboboxPopupContainer`, `ngListbox`, `ngOption`. **Directives:** `ngCombobox`, `ngComboboxPopup`, `ngComboboxWidget`, `ngListbox`, `ngOption`.
```html ```html
<!-- Example: Standard Select --> <!-- Example 1: Standard Autocomplete -->
<div ngCombobox [readonly]="true"> <div>
<button ngComboboxInput class="select-trigger"> <input
{{ selectedValue() || 'Choose an option' }} ngCombobox
</button> #combobox="ngCombobox"
[(value)]="searchString"
<ng-template ngComboboxPopupContainer> [(expanded)]="isExpanded"
<ul ngListbox [(values)]="selectedValue" class="dropdown-menu"> placeholder="Search options..."
<li ngOption value="option1">Option 1</li> class="select-trigger"
<li ngOption value="option2">Option 2</li> />
<ng-template ngComboboxPopup [combobox]="combobox">
<ul
ngComboboxWidget
ngListbox
#listbox="ngListbox"
[(value)]="selectedValue"
[activeDescendant]="listbox.activeDescendant()"
class="dropdown-menu"
>
<li ngOption value="option1" label="Option 1" class="option">Option 1</li>
<li ngOption value="option2" label="Option 2" class="option">Option 2</li>
</ul> </ul>
</ng-template> </ng-template>
</div> </div>
<!-- Example 2: Select Component (Applied directly to a div trigger) -->
<div ngCombobox #select="ngCombobox" [(expanded)]="selectExpanded" class="select-trigger">
<span class="select-text">{{ selectedValue() ?? 'Choose an option' }}</span>
<span class="icon"></span>
</div>
<ng-template ngComboboxPopup [combobox]="select">
<ul
ngComboboxWidget
ngListbox
#selectListbox="ngListbox"
[(value)]="selectedValues"
[activeDescendant]="selectListbox.activeDescendant()"
(click)="onCommit()"
(keydown.enter)="onCommit()"
class="dropdown-menu"
>
<li ngOption value="option1" label="Option 1" class="option">Option 1</li>
<li ngOption value="option2" label="Option 2" class="option">Option 2</li>
</ul>
</ng-template>
``` ```
**Styling Strategy:** **Styling Strategy:**
@ -186,22 +218,30 @@ For actions, commands, and context menus (not for form selection).
**Usage:** The Menubar is a high-level navigation pattern designed for building desktop-style application command bars (e.g., File, Edit, View) that stay persistent across an interface. It is best utilized for organizing complex commands into logical top-level categories with full horizontal keyboard support, but it should be avoided for simple standalone action lists or mobile-first layouts where horizontal space is constrained. **Usage:** The Menubar is a high-level navigation pattern designed for building desktop-style application command bars (e.g., File, Edit, View) that stay persistent across an interface. It is best utilized for organizing complex commands into logical top-level categories with full horizontal keyboard support, but it should be avoided for simple standalone action lists or mobile-first layouts where horizontal space is constrained.
**Imports:** `import {MenuBar, Menu, MenuContent, MenuItem} from '@angular/aria/menu';` **Imports:** `import {MenuBar, Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';`
**Directives:** `ngMenuBar`, `ngMenu`, `ngMenuItem`, `ngMenuTrigger`. **Directives:** `ngMenuBar`, `ngMenu`, `ngMenuItem`, `ngMenuTrigger`, `ngMenuContent`.
```html ```html
<!-- Menubar Example --> <!-- Menubar Example -->
<ul ngMenuBar class="menubar"> <div ngMenuBar class="menubar">
<li ngMenuItem value="file"> <div ngMenuItem value="file" [submenu]="fileMenu" class="menubar-item">File</div>
<button ngMenuTrigger [menu]="fileMenu">File</button> <div ngMenuItem value="edit" [submenu]="editMenu" class="menubar-item">Edit</div>
</li> </div>
</ul>
<ul ngMenu #fileMenu="ngMenu" class="menu"> <div ngMenu #fileMenu="ngMenu" class="menu">
<li ngMenuItem value="new">New</li> <ng-template ngMenuContent>
<li ngMenuItem value="open">Open</li> <div ngMenuItem value="new">New</div>
</ul> <div ngMenuItem value="open">Open</div>
</ng-template>
</div>
<div ngMenu #editMenu="ngMenu" class="menu">
<ng-template ngMenuContent>
<div ngMenuItem value="cut">Cut</div>
<div ngMenuItem value="copy">Copy</div>
</ng-template>
</div>
``` ```
**Styling Strategy:** **Styling Strategy:**
@ -239,7 +279,7 @@ Layered content sections where only one panel is visible.
```html ```html
<div ngTabs> <div ngTabs>
<ul ngTabList class="tab-list"> <ul ngTabList [(selectedTab)]="selectedTabValue" class="tab-list">
<li ngTab value="profile" class="tab-btn">Profile</li> <li ngTab value="profile" class="tab-btn">Profile</li>
<li ngTab value="security" class="tab-btn">Security</li> <li ngTab value="security" class="tab-btn">Security</li>
</ul> </ul>
@ -328,14 +368,17 @@ Displays hierarchical data (file systems, nested nav).
**Imports:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';` **Imports:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';`
**Directives:** `ngTree`, `ngTreeItem`, `ngTreeGroup`. **Directives:** `ngTree`, `ngTreeItem`, `ngTreeItemGroup`.
```html ```html
<ul ngTree class="tree"> <ul ngTree #tree="ngTree" [(value)]="selectedValues" class="tree">
<li ngTreeItem value="documents"> <li ngTreeItem [parent]="tree" value="documents" #docsItem="ngTreeItem">
<span class="tree-label">Documents</span> <span class="tree-label">Documents</span>
<ul ngTreeGroup class="tree-group"> <ul role="group">
<li ngTreeItem value="resume">Resume.pdf</li> <ng-template ngTreeItemGroup [ownedBy]="docsItem" #docsGroup="ngTreeItemGroup">
<li ngTreeItem [parent]="docsGroup" value="resume">Resume.pdf</li>
<li ngTreeItem [parent]="docsGroup" value="cover-letter">CoverLetter.pdf</li>
</ng-template>
</ul> </ul>
</li> </li>
</ul> </ul>
@ -403,8 +446,152 @@ Target `[aria-selected="true"]` for selected cells and `:focus-visible` for the
} }
``` ```
## 9. Testing with Component Harnesses
Angular Aria provides standard Component Harnesses (based on `@angular/cdk/testing`) to make unit testing clean, robust, and decoupled from DOM structural details.
**Imports:**
```ts
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {AccordionGroupHarness, AccordionHarness} from '@angular/aria/accordion/testing';
import {ListboxHarness, ListboxOptionHarness} from '@angular/aria/listbox/testing';
```
### Example: Testing an Accordion with Harnesses
```ts
describe('MyAccordionComponent', () => {
let fixture: ComponentFixture<MyAccordionComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
fixture = TestBed.createComponent(MyAccordionComponent);
await fixture.whenStable();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should expand accordion on toggle', async () => {
// Get the harness by its trigger title
const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'}));
expect(await accordion.isExpanded()).toBeFalse();
// Expand the accordion
await accordion.expand();
expect(await accordion.isExpanded()).toBeTrue();
});
});
```
## 10. Integration with Signal Forms
Because Angular Aria directives leverage Angular's modern `model()` signals for managing interactive values, they integrate **out-of-the-box** with Angular's new Signal Forms (`@angular/forms/signals`).
The `[formField]` directive automatically detects directives like `ngCombobox` or `ngListbox` as custom form controls because they expose a `value` model.
**Imports:**
```ts
import {form, schema, required} from '@angular/forms/signals';
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
```
### Example 1: Autocomplete Combobox inside a Form
Given a form model defined in your component:
```ts
protected readonly citySignal = signal({name: '', city: ''});
protected readonly myForm = form(this.citySignal, schema(f => {
required(f.city);
}));
```
You bind it directly using `[formField]`:
```html
<div>
<label for="city-input">Choose your city:</label>
<input
id="city-input"
ngCombobox
#combobox="ngCombobox"
[formField]="myForm.city"
[(expanded)]="isExpanded"
placeholder="Search cities..."
/>
<ng-template ngComboboxPopup [combobox]="combobox">
<ul
ngComboboxWidget
ngListbox
#listbox="ngListbox"
[(value)]="selectedValue"
[activeDescendant]="listbox.activeDescendant()"
class="dropdown-menu"
>
<li ngOption value="sfo" label="San Francisco">San Francisco</li>
<li ngOption value="nyc" label="New York">New York</li>
</ul>
</ng-template>
</div>
```
### Example 2: Select Component inside a Form
Apply `ngCombobox` directly to a focusable `div` trigger and bind to `[formField]`:
```html
<div>
<label for="city-select">Choose your city:</label>
<div
id="city-select"
ngCombobox
#select="ngCombobox"
[formField]="myForm.city"
[(expanded)]="isExpanded"
class="select-trigger"
>
<span class="select-text">{{ myForm.city.value() || 'Choose your city' }}</span>
<span class="icon"></span>
</div>
<ng-template ngComboboxPopup [combobox]="select">
<ul
ngComboboxWidget
ngListbox
#selectListbox="ngListbox"
[(value)]="selectedValues"
[activeDescendant]="selectListbox.activeDescendant()"
(click)="onCommit()"
(keydown.enter)="onCommit()"
class="dropdown-menu"
>
<li ngOption value="sfo" label="San Francisco">San Francisco</li>
<li ngOption value="nyc" label="New York">New York</li>
</ul>
</ng-template>
</div>
```
### Example 3: Standalone Listbox (Multi-select) inside a Form
You can bind a multi-selectable Listbox directly to a form array:
```html
<ul ngListbox [formField]="myForm.interests" [multi]="true" class="interest-list">
<li ngOption value="sports">Sports</li>
<li ngOption value="music">Music</li>
<li ngOption value="tech">Technology</li>
</ul>
```
## General Rules for Agents ## General Rules for Agents
1. **Never use native HTML elements like `<select>`** when asked to implement these specific Aria patterns. Use the `ng*` directives. 1. **Never use native HTML elements like `<select>`** when asked to implement these specific Aria patterns. Use the `ng*` directives.
2. **Handle CSS manually**: Remember that `Angular Aria` does NOT provide styles. You must write the CSS, targeting the native ARIA attributes (`aria-expanded`, `aria-selected`, etc.) that the directives automatically toggle. 2. **Handle CSS manually**: Remember that `Angular Aria` does NOT provide styles. You must write the CSS, targeting the native ARIA attributes (`aria-expanded`, `aria-selected`, etc.) that the directives automatically toggle.
3. **Lazy Loading**: Always use the provided structural directives (`ngAccordionContent`, `ngTabContent`) inside `ng-template` for heavy content panels to ensure they are lazily rendered. 3. **Lazy Loading**: Always use the provided structural directives (`ngAccordionContent`, `ngTabContent`, `ngMenuContent`, `ngComboboxPopup`, `ngTreeItemGroup`) inside `ng-template` for heavy content panels or nested groups to ensure they are lazily rendered.

2
.agents/skills/angular-developer/references/cli.md

@ -25,7 +25,7 @@ Always use the CLI to generate code to ensure it adheres to Angular standards an
| Target | Command | Notes | | Target | Command | Notes |
| :----------- | :-------------------- | :--------------------------------------------------------------------------------------------- | | :----------- | :-------------------- | :--------------------------------------------------------------------------------------------- |
| Component | `ng g c path/to/name` | Generates a component. Use `--inline-style` (`-s`) or `--inline-template` (`-t`) if requested. | | Component | `ng g c path/to/name` | Generates a component. Use `--inline-style` (`-s`) or `--inline-template` (`-t`) if requested. |
| Service | `ng g s path/to/name` | Generates an `@Injectable({providedIn: 'root'})` service. | | Service | `ng g s path/to/name` | Generates an `@Service` service. |
| Directive | `ng g d path/to/name` | Generates a directive. | | Directive | `ng g d path/to/name` | Generates a directive. |
| Pipe | `ng g p path/to/name` | Generates a pipe. | | Pipe | `ng g p path/to/name` | Generates a pipe. |
| Guard | `ng g g path/to/name` | Generates a functional route guard. | | Guard | `ng g g path/to/name` | Generates a functional route guard. |

6
.agents/skills/angular-developer/references/component-harnesses.md

@ -25,10 +25,8 @@ describe('MyButtonContainerComponent', () => {
let fixture: ComponentFixture<MyButtonContainerComponent>; let fixture: ComponentFixture<MyButtonContainerComponent>;
let loader: HarnessLoader; let loader: HarnessLoader;
beforeEach(async () => { beforeEach(() => {
await TestBed.configureTestingModule({ TestBed.configureTestingModule({});
imports: [MyButtonContainerComponent, MatButtonModule],
}).compileComponents();
fixture = TestBed.createComponent(MyButtonContainerComponent); fixture = TestBed.createComponent(MyButtonContainerComponent);
// Create a harness loader for the component's fixture // Create a harness loader for the component's fixture

24
.agents/skills/angular-developer/references/creating-services.md

@ -10,14 +10,12 @@ You can generate a service using the Angular CLI:
ng generate service my-data ng generate service my-data
``` ```
Or you can manually create a TypeScript class and decorate it with `@Injectable()`. Or you can manually create a TypeScript class and decorate it with `@Service()`.
```ts ```ts
import {Injectable} from '@angular/core'; import {Service} from '@angular/core';
@Injectable({ @Service()
providedIn: 'root',
})
export class BasicDataStore { export class BasicDataStore {
private data: string[] = []; private data: string[] = [];
@ -31,14 +29,18 @@ export class BasicDataStore {
} }
``` ```
### The `providedIn: 'root'` Option ### The `@Service` decorator
Using `providedIn: 'root'` is the recommended approach for most services. It tells Angular to: Using `@Service` is the recommended approach for most services. It tells Angular to:
- **Create a single instance (singleton)** for the entire application. - **Create a single instance (singleton)** for the entire application.
- **Make it available everywhere** automatically, without needing to list it in any `providers` array. - **Make it available everywhere** automatically, without needing to list it in any `providers` array.
- **Enable tree-shaking**, meaning the service is only included in the final JavaScript bundle if it is actually injected somewhere. - **Enable tree-shaking**, meaning the service is only included in the final JavaScript bundle if it is actually injected somewhere.
#### 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.
## Injecting a Service ## Injecting a Service
Once a service is created, you can inject it into components, directives, or other services using the `inject()` function. Once a service is created, you can inject it into components, directives, or other services using the `inject()` function.
@ -72,9 +74,7 @@ Services can inject other services in the exact same way.
import {Injectable, inject} from '@angular/core'; import {Injectable, inject} from '@angular/core';
import {AdvancedDataStore} from './advanced-data-store.service'; import {AdvancedDataStore} from './advanced-data-store.service';
@Injectable({ @Service()
providedIn: 'root',
})
export class BasicDataStore { export class BasicDataStore {
// Injecting another service // Injecting another service
private advancedDataStore = inject(AdvancedDataStore); private advancedDataStore = inject(AdvancedDataStore);
@ -90,8 +90,8 @@ export class BasicDataStore {
## Advanced Service Patterns ## Advanced Service Patterns
While `providedIn: 'root'` covers most scenarios, you may sometimes need: While `@Service` covers most scenarios, you may sometimes need:
- **Component-specific instances**: If a component needs its own isolated instance of a service, provide it directly in the component's `@Component({ providers: [MyService] })` array. - **Component-specific instances**: If a component needs its own isolated instance of a service, provide it directly in the component's `@Component({ providers: [MyService] })` array and set the `autoProvided: false` option: `@Service({autoProvided: false})`
- **Factory providers**: For dynamic creation. - **Factory providers**: For dynamic creation.
- **Value providers**: For injecting configuration objects. - **Value providers**: For injecting configuration objects.

6
.agents/skills/angular-developer/references/data-resolvers.md

@ -33,9 +33,9 @@ Add the resolver under the `resolve` key.
### 1. Via `ActivatedRoute` (Traditional) ### 1. Via `ActivatedRoute` (Traditional)
```ts ```ts
private route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
data = toSignal(this.route.data); protected readonly data = toSignal(this.route.data);
user = computed(() => this.data().user); protected readonly user = computed(() => this.data().user);
``` ```
### 2. Via Component Inputs (Modern) ### 2. Via Component Inputs (Modern)

22
.agents/skills/angular-developer/references/di-fundamentals.md

@ -13,18 +13,16 @@ Angular components, directives, and services automatically participate in DI.
## Services ## Services
A **service** is the most common way to share data and functionality across an application. It is a TypeScript class decorated with `@Injectable()`. A **service** is the most common way to share data and functionality across an application. It is a TypeScript class decorated with `@Service()`.
### Creating a Service ### Creating a Service
Use the `providedIn: 'root'` option in the `@Injectable` decorator to make the service a singleton available throughout the entire application. This is the recommended approach for most services. Use the `@Service()` decorator to make the service a singleton available throughout the entire application. This is the recommended approach for most services.
```ts ```ts
import {Injectable} from '@angular/core'; import {Service} from '@angular/core';
@Injectable({ @Service()
providedIn: 'root', // Makes this a singleton available everywhere
})
export class AnalyticsLogger { export class AnalyticsLogger {
trackEvent(category: string, value: string) { trackEvent(category: string, value: string) {
console.log('Analytics event logged:', {category, value}); console.log('Analytics event logged:', {category, value});
@ -59,8 +57,8 @@ import {AnalyticsLogger} from './analytics-logger.service';
}) })
export class Navbar { export class Navbar {
// Injecting dependencies using class field initializers // Injecting dependencies using class field initializers
private router = inject(Router); private readonly router = inject(Router);
private analytics = inject(AnalyticsLogger); private readonly analytics = inject(AnalyticsLogger);
navigateToDetail(event: Event) { navigateToDetail(event: Event) {
event.preventDefault(); event.preventDefault();
@ -82,13 +80,11 @@ Valid places to call `inject()`:
4. **Factory functions** used in providers 4. **Factory functions** used in providers
```typescript ```typescript
import {Component, Directive, Injectable, inject, ElementRef} from '@angular/core'; import {Component, Directive, Service, inject, ElementRef} from '@angular/core';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
// 1. In a Component (Field Initializer & Constructor) // 1. In a Component (Field Initializer & Constructor)
@Component({ @Component(/* ... */)
/*...*/
})
export class Example { export class Example {
private service1 = inject(MyService); // ✅ Field initializer private service1 = inject(MyService); // ✅ Field initializer
@ -107,7 +103,7 @@ export class MyDirective {
} }
// 3. In a Service // 3. In a Service
@Injectable({providedIn: 'root'}) @Service()
export class MyService { export class MyService {
private http = inject(HttpClient); // ✅ Field initializer private http = inject(HttpClient); // ✅ Field initializer
} }

73
.agents/skills/angular-developer/references/e2e-testing.md

@ -1,66 +1,39 @@
# End-to-End (E2E) Testing # End-to-End (E2E) Testing
This project uses [Cypress](https://www.cypress.io/) for end-to-end (E2E) testing, which simulates real user interactions in a browser. The E2E tests are located primarily within the `devtools/` package. > [!IMPORTANT]
> Only use the setup guidelines in this file if there is no existing E2E testing framework configured in the workspace, or if the user has explicitly requested to change or set up E2E testing.
## Running E2E Tests ## Setting Up and Running E2E Tests
The primary way to run E2E tests is through the `pnpm` script defined in the root `package.json`. Add supported E2E frameworks to the project using `ng add`:
1. **Build DevTools:** The E2E tests run against a built version of the devtools extension. You must build it first:
- **Playwright:**
```shell ```shell
pnpm -F ng-devtools-mcp build:dev ng add playwright-ng-schematics
``` ```
- **Cypress:**
2. **Run Cypress:** Use the `cy:open` or `cy:run` script:
- To open the interactive Cypress Test Runner:
```shell ```shell
pnpm -F ng-devtools-mcp cy:open ng add @cypress/schematic
``` ```
- To run the tests headlessly in the terminal (ideal for CI): - **Nightwatch:**
```shell ```shell
pnpm -F ng-devtools-mcp cy:run ng add @nightwatch/schematics
```
- **WebdriverIO:**
```shell
ng add @wdio/schematics
```
- **Puppeteer:**
```shell
ng add @puppeteer/ng-schematics
``` ```
## Test Structure Run E2E tests:
- **Configuration:** The main Cypress configuration is located at `devtools/cypress.json`.
- **Specs:** Test files (specs) are located in `devtools/cypress/integration/`.
- **Custom Commands:** Reusable custom commands and actions are defined in `devtools/cypress/support/`.
### Example E2E Test Snippet
A typical test might look like this:
```typescript
// in devtools/cypress/integration/profiler.spec.ts
describe('Profiler', () => {
beforeEach(() => {
cy.visit('/?e2e-app');
cy.wait(1000);
cy.get('ng-devtools-tabs').find('a').contains('Profiler').click();
});
it('should record and display profiling data', () => {
// Find the record button and click it
cy.get('button[aria-label="start-recording-button"]').click();
// Interact with the test application to generate profiling data
cy.get('body').find('#cards button').first().click();
cy.wait(500);
// Stop recording
cy.get('button[aria-label="stop-recording-button"]').click();
// Assert that the flame graph is now visible ```shell
cy.get('ng-devtools-recording-timeline').find('canvas').should('be.visible'); ng e2e [project] [options]
});
});
``` ```
### Best Practices ## Custom & Enterprise Testing Tools
- **Use `data-` attributes:** Whenever possible, use `data-cy` or similar attributes for selecting elements to make tests more resilient to CSS or structural changes. For custom enterprise runners (e.g., Katalon Studio, TestCafe, Selenium), define execution commands in `package.json` scripts.
- **Custom Commands:** Encapsulate common sequences of actions into custom commands in the `support` directory to keep tests clean and readable.
- **Wait for Application State:** Use `cy.wait()` for arbitrary waits sparingly. Prefer to wait for specific UI elements to appear or for network requests to complete to avoid flaky tests.

2
.agents/skills/angular-developer/references/effects.md

@ -24,7 +24,7 @@ import { Component, signal, effect } from '@angular/core';
@Component({...}) @Component({...})
export class Example { export class Example {
count = signal(0); protected readonly count = signal(0);
constructor() { constructor() {
// Effect must be created in an injection context (e.g., a constructor) // Effect must be created in an injection context (e.g., a constructor)

132
.agents/skills/angular-developer/references/environment-configuration.md

@ -0,0 +1,132 @@
# Environment configuration
## Configuration strategies
Angular supports two main configuration strategies:
- **Build-time configuration** using environment files
- **Runtime configuration** by loading values at application startup
Choose the approach based on your deployment requirements.
---
## Build-time configuration
Environment files define configuration values that are replaced at build time.
> **Security note:** Environment files are bundled into the client-side application.
> They are visible to anyone who can load the page.
> Never store sensitive information like API keys, secrets, or credentials in environment files.
> These values can be easily accessed by users.
Generate environment files using the CLI:
```bash
ng generate environments
```
This creates environment-specific files such as:
```ts
// environment.ts
export const environment = {
apiUrl: 'https://api.example.com',
};
```
```ts
// environment.development.ts
export const environment = {
apiUrl: 'http://localhost:3000',
};
```
Import the environment where needed:
```ts
import {environment} from '../environments/environment';
const apiUrl = environment.apiUrl;
```
The Angular CLI replaces the appropriate file based on the build configuration.
If you need a development-mode check, use `isDevMode()` from `@angular/core` instead of relying on a manually maintained `production` flag.
> Changes to environment files require rebuilding the application.
---
## Runtime configuration (advanced)
In some scenarios, applications need to load configuration at runtime instead of build time.
This allows the same build artifact to be deployed across multiple environments without rebuilding.
A common approach is to load a JSON configuration file from the `assets` folder during application
initialization.
### Example
```json
// src/assets/config.json
{
"apiUrl": "https://api.example.com"
}
```
Load the configuration before the application starts:
```ts
import {Service, inject} from '@angular/core';
import {HttpClient} from '@angular/common/http';
@Service()
export class AppConfigService {
private config!: {apiUrl: string};
private readonly http = inject(HttpClient);
loadConfig() {
return this.http.get<AppConfig>('/assets/config.json').pipe(
tap((data) => {
this.config = data;
}),
);
}
get apiUrl(): string {
return this.config.apiUrl;
}
}
```
Register the loader during application bootstrap:
```ts
import {provideAppInitializer, inject} from '@angular/core';
provideAppInitializer(() => {
const config = inject(AppConfigService);
return config.loadConfig();
});
```
This ensures configuration is available before the application renders.
> Runtime configuration is an advanced pattern and is not required for most applications.
---
## Choosing a strategy
| Criteria | Build-time | Runtime |
| ---------------------- | ---------- | ------------ |
| Change without rebuild | No | Yes |
| Startup performance | Faster | Slight delay |
| Complexity | Low | Moderate |
| Deployment flexibility | Limited | High |
Use build-time configuration for most applications, and runtime configuration when you need to
deploy the same build across multiple environments.

2
.agents/skills/angular-developer/references/hierarchical-injectors.md

@ -4,7 +4,7 @@ Angular's dependency injection system is hierarchical, meaning services can be s
## Types of Injector Hierarchies ## Types of Injector Hierarchies
1. **`EnvironmentInjector` Hierarchy**: Configured via `@Injectable({ providedIn: 'root' })` or `ApplicationConfig.providers` during bootstrap. These are global singletons. 1. **`EnvironmentInjector` Hierarchy**: Configured via `@Service()`, `@Injectable({ providedIn: 'root' })` or `ApplicationConfig.providers` during bootstrap. These are global singletons.
2. **`ElementInjector` Hierarchy**: Created implicitly at each DOM element. Configured via the `providers` or `viewProviders` array in `@Component()` or `@Directive()`. 2. **`ElementInjector` Hierarchy**: Created implicitly at each DOM element. Configured via the `providers` or `viewProviders` array in `@Component()` or `@Directive()`.
## Resolution Rules ## Resolution Rules

8
.agents/skills/angular-developer/references/host-elements.md

@ -19,10 +19,10 @@ Use the `host` property in the `@Component` decorator to bind properties, attrib
}, },
}) })
export class CustomSlider { export class CustomSlider {
value = 0; protected readonly value = 0;
disabled = false; protected readonly disabled = false;
isActive = signal(false); protected readonly isActive = signal(false);
color = signal('blue'); protected readonly color = signal('blue');
onKeyDown(event: KeyboardEvent) { onKeyDown(event: KeyboardEvent) {
/* ... */ /* ... */

108
.agents/skills/angular-developer/references/http-client.md

@ -0,0 +1,108 @@
# HTTP communication with `HttpClient` and `httpResource`
Use Angular's HTTP APIs for backend communication so requests participate in dependency injection, interceptors, transfer cache, and security features.
## Setup
In Angular v21 and later, `HttpClient` is available for injection by default. Add `provideHttpClient(...)` only when an app needs to configure HTTP features for a specific injector:
```ts
import {provideHttpClient, withInterceptors} from '@angular/common/http';
export const appConfig = {
providers: [provideHttpClient(withInterceptors([authInterceptor]))],
};
```
- `HttpClient` uses the fetch backend by default.
- Use `withXhr()` only when upload progress events are required. Do not use `withXhr()` for server-side rendering.
- Use `provideHttpClient(...)` for feature configuration such as interceptors, XSRF options, XHR, or parent-request delegation.
- Calling `provideHttpClient()` with no features is not required for basic HTTP requests, but it configures the default HTTP feature set for that injector, including Angular's XSRF interceptor.
- Prefer `provideHttpClient(...)` over `HttpClientModule` for feature configuration, especially with multiple injectors.
- Use `withRequestsMadeViaParent()` when a child injector should add interceptors while still delegating to the parent HTTP chain.
## `HttpClient`
Encapsulate backend calls in injectable services, not components:
```ts
import {HttpClient} from '@angular/common/http';
import {Service, inject} from '@angular/core';
@Service()
export class UserService {
private readonly http = inject(HttpClient);
getUser(id: string) {
return this.http.get<User>(`/api/users/${id}`);
}
}
```
Important rules:
- `HttpClient` requests are cold `Observable`s. No request is sent until the `Observable` is subscribed to. Multiple subscriptions send multiple backend requests.
- Subscribe to mutation requests (`post`, `put`, `patch`, `delete`) so they execute.
- The generic type parameter is a type assertion only. Validate unknown backend data at runtime when the shape is not trusted.
- Use literal values for `responseType` and `observe`; if options are extracted, write values like `responseType: 'text' as const`.
- `HttpHeaders` and `HttpParams` are immutable; use the returned instance from `.set()` or `.append()`.
- Fetch options such as `timeout`, `cache`, `priority`, `mode`, `redirect`, `credentials`, `keepalive`, `referrer`, `referrerPolicy`, and `integrity` are supported where the backend supports them. `withCredentials: true` overrides `credentials`.
- Handle failures through `HttpErrorResponse`. Network and timeout failures use status `0`; backend failures use the server status code.
- Prefer the `async` pipe or `toSignal` for component reads so subscriptions are cleaned up.
## Interceptors
Prefer functional interceptors configured with `withInterceptors`.
```ts
import {
HttpHandlerFn,
HttpRequest,
provideHttpClient,
withInterceptors,
} from '@angular/common/http';
export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
return next(req.clone({setHeaders: {Authorization: 'Bearer token'}}));
}
export const appConfig = {
providers: [provideHttpClient(withInterceptors([authInterceptor]))],
};
```
- Interceptors run in the order listed.
- Request and response objects are mostly immutable; clone before changing them.
- Request and response bodies are not deeply immutable. Avoid in-place body mutation because retries can run the same interceptor again.
- Use `inject()` inside functional interceptors for services.
- Use `HttpContextToken` for per-request metadata that interceptors need but the backend should not receive.
- Use DI-based interceptors only for existing code, and enable them with `withInterceptorsFromDi()`.
## Security
- `HttpClient` strips the XSSI prefix from JSON responses when present.
- `provideHttpClient()` configures XSRF protection by default for mutating relative and same-origin requests. It reads the `XSRF-TOKEN` cookie and sends the `X-XSRF-TOKEN` header.
- The backend must set the XSRF cookie and verify the header. Customize names with `withXsrfConfiguration(...)`; disable only deliberately with `withNoXsrfProtection()`.
## `httpResource`
Use `httpResource` to create an asynchronous derivation that fetches data over HTTP and exposes the result as reactive signals.
```ts
import {httpResource} from '@angular/common/http';
import {input} from '@angular/core';
export class UserProfile {
readonly userId = input.required<string>();
readonly user = httpResource(() => `/api/users/${this.userId()}`);
}
```
- `httpResource` is eager. It sends a request when its reactive request computation runs, not when an `Observable` is subscribed.
- When a dependency changes, it cancels the pending request and sends the next one.
- Return `undefined` from the request function to skip a backend request.
- Prefer `httpResource` for reads. Use `HttpClient` directly for mutations such as `POST`, `PUT`, `PATCH`, and `DELETE`.
- Guard `value()` reads with `hasValue()`; reading `value()` while the resource is in an error state throws.
- Use `httpResource.text`, `httpResource.blob`, or `httpResource.arrayBuffer` for non-JSON responses.
- Use the `parse` option to validate or transform responses with a runtime schema.
- Read `headers()`, `statusCode()`, and `progress()` when response metadata or download progress is needed. Set `reportProgress: true` for progress events.

6
.agents/skills/angular-developer/references/injection-context.md

@ -6,7 +6,7 @@ The `inject()` function can only be used when code is executing within an **inje
An injection context is automatically available in: An injection context is automatically available in:
1. **Field initializers** of classes instantiated by DI (`@Injectable`, `@Component`, `@Directive`, `@Pipe`). 1. **Field initializers** of classes instantiated by DI (`@Service`, `@Injectable`, `@Component`, `@Directive`, `@Pipe`).
2. **Constructors** of classes instantiated by DI. 2. **Constructors** of classes instantiated by DI.
3. **Factory functions** specified in `useFactory` or `InjectionToken` configurations. 3. **Factory functions** specified in `useFactory` or `InjectionToken` configurations.
4. **Functional APIs** executed by Angular (e.g., functional route guards, resolvers, interceptors). 4. **Functional APIs** executed by Angular (e.g., functional route guards, resolvers, interceptors).
@ -34,9 +34,9 @@ export class Example {
If you need to run a function within an injection context (often needed for dynamic component creation or testing), use `runInInjectionContext`. This requires access to an existing injector (like `EnvironmentInjector` or `Injector`). If you need to run a function within an injection context (often needed for dynamic component creation or testing), use `runInInjectionContext`. This requires access to an existing injector (like `EnvironmentInjector` or `Injector`).
```ts ```ts
import {Injectable, inject, EnvironmentInjector, runInInjectionContext} from '@angular/core'; import {inject, EnvironmentInjector, runInInjectionContext, Service} from '@angular/core';
@Injectable({providedIn: 'root'}) @Service()
export class MyService { export class MyService {
private injector = inject(EnvironmentInjector); private injector = inject(EnvironmentInjector);

14
.agents/skills/angular-developer/references/inputs.md

@ -11,17 +11,17 @@ import {Component, input, computed} from '@angular/core';
@Component({ @Component({
selector: 'app-user', selector: 'app-user',
template: `<p>User: {{ name() }} ({{ age() }})</p>`, template: `<p>{{ label() }} ({{ age() }})</p>`,
}) })
export class User { export class User {
// Optional input with default value // Optional input with default value
name = input('Guest'); readonly name = input('Guest');
// Required input // Required input
age = input.required<number>(); readonly age = input.required<number>();
// Inputs are reactive signals // Inputs are reactive signals
label = computed(() => `Name: ${this.name()}`); protected readonly label = computed(() => `Name: ${this.name()}`);
} }
``` ```
@ -44,10 +44,10 @@ import { input, booleanAttribute } from '@angular/core';
@Component({...}) @Component({...})
export class CustomButton { export class CustomButton {
// Alias example // Alias example
label = input('', { alias: 'btnLabel' }); readonly label = input('', { alias: 'btnLabel' });
// Transform example using built-in helper // Transform example using built-in helper
disabled = input(false, { transform: booleanAttribute }); readonly disabled = input(false, { transform: booleanAttribute });
} }
``` ```
@ -61,7 +61,7 @@ Use `model()` to create an input that supports two-way data binding.
template: `<button (click)="increment()">+</button>`, template: `<button (click)="increment()">+</button>`,
}) })
export class CustomCounter { export class CustomCounter {
value = model(0); readonly value = model(0);
increment() { increment() {
this.value.update((v) => v + 1); this.value.update((v) => v + 1);

8
.agents/skills/angular-developer/references/linked-signal.md

@ -13,11 +13,11 @@ import { Component, signal, linkedSignal } from '@angular/core';
@Component({...}) @Component({...})
export class ShippingMethodPicker { export class ShippingMethodPicker {
shippingOptions = signal(['Ground', 'Air', 'Sea']); protected readonly shippingOptions = signal(['Ground', 'Air', 'Sea']);
// Defaults to the first option. // Defaults to the first option.
// If shippingOptions changes, selectedOption resets to the new first option. // If shippingOptions changes, selectedOption resets to the new first option.
selectedOption = linkedSignal(() => this.shippingOptions()[0]); protected readonly selectedOption = linkedSignal(() => this.shippingOptions()[0]);
changeShipping(index: number) { changeShipping(index: number) {
// We can still manually update this signal! // We can still manually update this signal!
@ -37,11 +37,11 @@ interface ShippingMethod { id: number; name: string; }
@Component({...}) @Component({...})
export class ShippingMethodPicker { export class ShippingMethodPicker {
shippingOptions = signal<ShippingMethod[]>([ protected readonly shippingOptions = signal<ShippingMethod[]>([
{id: 0, name: 'Ground'}, {id: 1, name: 'Air'}, {id: 2, name: 'Sea'} {id: 0, name: 'Ground'}, {id: 1, name: 'Air'}, {id: 2, name: 'Sea'}
]); ]);
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({ protected readonly selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
source: this.shippingOptions, source: this.shippingOptions,
computation: (newOptions, previous) => { computation: (newOptions, previous) => {
// If the newly loaded options still contain the user's previously // If the newly loaded options still contain the user's previously

10
.agents/skills/angular-developer/references/outputs.md

@ -15,10 +15,10 @@ import {Component, output} from '@angular/core';
}) })
export class CustomSlider { export class CustomSlider {
// Output without event data // Output without event data
panelClosed = output<void>(); readonly panelClosed = output<void>();
// Output with event data (number) // Output with event data (number)
valueChanged = output<number>(); readonly valueChanged = output<number>();
changeValue(newValue: number) { changeValue(newValue: number) {
this.valueChanged.emit(newValue); this.valueChanged.emit(newValue);
@ -43,7 +43,7 @@ The `output` function accepts a config object to specify an alias.
export class CustomSlider { export class CustomSlider {
// The event is named 'valueChanged' in the template, // The event is named 'valueChanged' in the template,
// but accessed as 'changed' in the component class. // but accessed as 'changed' in the component class.
changed = output<number>({ alias: 'valueChanged' }); readonly changed = output<number>({ alias: 'valueChanged' });
} }
``` ```
@ -71,10 +71,10 @@ import { Component, Output, EventEmitter } from '@angular/core';
@Component({...}) @Component({...})
export class LegacyExample { export class LegacyExample {
@Output() valueChanged = new EventEmitter<number>(); @Output() readonly valueChanged = new EventEmitter<number>();
// With alias // With alias
@Output('customEventName') changed = new EventEmitter<void>(); @Output('customEventName') readonly changed = new EventEmitter<void>();
} }
``` ```

145
.agents/skills/angular-developer/references/pipes.md

@ -0,0 +1,145 @@
# Pipes
Pipes transform data declaratively inside Angular templates using the `|` operator.
## Using pipes in templates
Import the pipe class and add it to the component's `imports` array.
```ts
import {Component} from '@angular/core';
import {DatePipe, CurrencyPipe} from '@angular/common';
@Component({
selector: 'app-invoice',
imports: [DatePipe, CurrencyPipe],
template: `
<p>Date: {{ issuedOn | date: 'mediumDate' }}</p>
<p>Total: {{ amount | currency }}</p>
`,
})
export class Invoice {
issuedOn = new Date();
amount = 49.99;
}
```
## Using pipe logic outside templates
**Do NOT inject pipe classes into services or other classes.** Pipes are template operators, not injectable services. Injecting them causes DI errors in standalone contexts and creates unnecessary coupling.
### Custom pipes — extract the transformation function
Extract the logic into a plain function. The pipe delegates to it; services import the function directly.
```ts
// kebab-case.ts
export function toKebabCase(value: string): string {
return value.toLowerCase().replace(/ /g, '-');
}
```
```ts
// kebab-case.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
import {toKebabCase} from './kebab-case';
@Pipe({name: 'kebabCase'})
export class KebabCasePipe implements PipeTransform {
transform(value: string): string {
return toKebabCase(value);
}
}
```
```ts
// formatter.service.ts — import the function, NOT the pipe
import {Injectable} from '@angular/core';
import {toKebabCase} from './kebab-case';
@Injectable({providedIn: 'root'})
export class FormatterService {
toSlug(title: string): string {
return toKebabCase(title);
}
}
```
### Built-in locale-aware pipes — use standalone formatting functions
`@angular/common` exports a standalone function for each locale-aware built-in pipe:
| Pipe | Standalone function |
| -------------- | ------------------- |
| `DatePipe` | `formatDate` |
| `CurrencyPipe` | `formatCurrency` |
| `DecimalPipe` | `formatNumber` |
| `PercentPipe` | `formatPercent` |
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 {formatNumber} from '@angular/common';
@Injectable({providedIn: 'root'})
export class PriceService {
private locale = inject(LOCALE_ID);
formatQuantity(value: number): string {
return formatNumber(value, this.locale, '1.0-0');
}
}
```
```ts
// WRONG — do not inject pipe classes
import {Injectable} from '@angular/core';
import {DecimalPipe} from '@angular/common';
@Injectable({providedIn: 'root'})
export class PriceService {
// ❌ DecimalPipe is not designed to be injected
private pipe = inject(DecimalPipe);
}
```
## Creating custom pipes
Use the Angular CLI to generate a pipe:
```bash
ng generate pipe path/to/my-pipe
```
A pipe needs a `@Pipe` decorator with a `name` and a `transform` method implementing `PipeTransform`.
```ts
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({name: 'truncate'})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50): string {
return value.length > limit ? value.slice(0, limit) + '…' : value;
}
}
```
- **`name`**: camelCase. Do not use hyphens.
- **Class name**: PascalCase version of `name` with `Pipe` appended (e.g., `TruncatePipe`).
## Impure pipes
Mark a pipe `pure: false` only when you need to detect mutations inside arrays or objects. Impure pipes run on every change-detection cycle and can hurt performance.
```ts
@Pipe({name: 'filterItems', pure: false})
export class FilterItemsPipe implements PipeTransform {
transform(items: string[], query: string): string[] {
return items.filter((i) => i.includes(query));
}
}
```
IMPORTANT: Avoid impure pipes unless absolutely necessary.

26
.agents/skills/angular-developer/references/reactive-forms.md

@ -9,7 +9,7 @@ Reactive forms are built using these fundamental classes from `@angular/forms`:
- `FormControl`: Manages the value and validity of an individual input. - `FormControl`: Manages the value and validity of an individual input.
- `FormGroup`: Manages a group of controls (an object-like structure). - `FormGroup`: Manages a group of controls (an object-like structure).
- `FormArray`: Manages a numerically indexed array of controls. - `FormArray`: Manages a numerically indexed array of controls.
- `FormBuilder`: A service that provides factory methods for creating control instances. - `FormBuilder`/`NonNullableFormBuilder`: A service that provides factory methods for creating control instances.
## Setup ## Setup
@ -17,7 +17,7 @@ Import `ReactiveFormsModule` into your component.
```ts ```ts
import {Component, inject} from '@angular/core'; import {Component, inject} from '@angular/core';
import {ReactiveFormsModule, FormGroup, FormControl, Validators, FormBuilder} from '@angular/forms'; import {ReactiveFormsModule, NonNullableFormBuilder, Validators} from '@angular/forms';
@Component({ @Component({
selector: 'app-profile-editor', selector: 'app-profile-editor',
@ -25,20 +25,20 @@ import {ReactiveFormsModule, FormGroup, FormControl, Validators, FormBuilder} fr
templateUrl: './profile-editor.component.html', templateUrl: './profile-editor.component.html',
}) })
export class ProfileEditor { export class ProfileEditor {
private fb = inject(FormBuilder); private readonly fb = inject(NonNullableFormBuilder);
// Using FormBuilder for concise definition // Using FormBuilder for concise definition
profileForm = this.fb.group({ protected readonly profileForm = this.fb.group({
firstName: ['', Validators.required], firstName: ['', Validators.required],
lastName: [''], lastName: '',
address: this.fb.group({ address: this.fb.group({
street: [''], street: '',
city: [''], city: '',
}), }),
aliases: this.fb.array([this.fb.control('')]), aliases: this.fb.array([this.fb.control('')]),
}); });
onSubmit() { protected onSubmit() {
console.warn(this.profileForm.value); console.warn(this.profileForm.value);
} }
} }
@ -63,7 +63,7 @@ Use directives to bind the model to the view:
</div> </div>
<div formArrayName="aliases"> <div formArrayName="aliases">
@for (alias of aliases.controls; track $index) { @for (alias of profileForm.controls.aliases.controls; track alias) {
<input type="text" [formControlName]="$index" /> <input type="text" [formControlName]="$index" />
} }
</div> </div>
@ -74,15 +74,11 @@ Use directives to bind the model to the view:
## Accessing Controls ## Accessing Controls
Use getters for easy access to controls, especially for `FormArray`. Use `.controls` for easy access to controls.
```ts ```ts
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
addAlias() { addAlias() {
this.aliases.push(this.fb.control('')); this.profileForm.controls.aliases.push(this.fb.control(''));
} }
``` ```

9
.agents/skills/angular-developer/references/resource.md

@ -1,8 +1,5 @@
# Async Reactivity with `resource` # Async Reactivity with `resource`
> [!IMPORTANT]
> The `resource` API is currently experimental in Angular.
A `Resource` incorporates asynchronous data fetching into Angular's signal-based reactivity. It executes an async loader function whenever its dependencies change, exposing the status and result as synchronous signals. A `Resource` incorporates asynchronous data fetching into Angular's signal-based reactivity. It executes an async loader function whenever its dependencies change, exposing the status and result as synchronous signals.
## Basic Usage ## Basic Usage
@ -17,9 +14,9 @@ import { Component, resource, signal, computed } from '@angular/core';
@Component({...}) @Component({...})
export class UserProfile { export class UserProfile {
userId = signal('123'); protected readonly userId = signal('123');
userResource = resource({ protected readonly userResource = resource({
// Reactively tracking userId // Reactively tracking userId
params: () => ({ id: this.userId() }), params: () => ({ id: this.userId() }),
@ -32,7 +29,7 @@ export class UserProfile {
}); });
// Use the resource value in computed signals // Use the resource value in computed signals
userName = computed(() => { protected readonly userName = computed(() => {
if (this.userResource.hasValue()) { if (this.userResource.hasValue()) {
return this.userResource.value()?.name; return this.userResource.value()?.name;
} else { } else {

4
.agents/skills/angular-developer/references/router-testing.md

@ -22,7 +22,7 @@ describe('Dashboard Component Routing', () => {
beforeEach(async () => { beforeEach(async () => {
// 1. Configure TestBed with test routes // 1. Configure TestBed with test routes
await TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
// Use provideRouter with your test-specific routes // Use provideRouter with your test-specific routes
provideRouter([ provideRouter([
@ -30,7 +30,7 @@ describe('Dashboard Component Routing', () => {
{path: 'heroes/:id', component: HeroDetail}, {path: 'heroes/:id', component: HeroDetail},
]), ]),
], ],
}).compileComponents(); });
// 2. Create the RouterTestingHarness // 2. Create the RouterTestingHarness
harness = await RouterTestingHarness.create(); harness = await RouterTestingHarness.create();

38
.agents/skills/angular-developer/references/signal-forms.md

@ -45,7 +45,7 @@ import {form, FormField} from '@angular/forms/signals';
}) })
export class Example { export class Example {
// 1. Define your model with initial values (avoid undefined) // 1. Define your model with initial values (avoid undefined)
userModel = signal({ protected readonly userModel = signal({
name: '', // CRITICAL: NEVER use null or undefined as initial values name: '', // CRITICAL: NEVER use null or undefined as initial values
email: '', email: '',
age: 0, // Use 0 for numbers, NOT null age: 0, // Use 0 for numbers, NOT null
@ -64,7 +64,7 @@ export class Example {
// }); // });
// 2. Create the form // 2. Create the form
userForm = form(this.userModel); protected readonly userForm = form(this.userModel);
} }
``` ```
@ -145,10 +145,10 @@ import {disabled, readonly, hidden} from '@angular/forms/signals';
userForm = form(this.userModel, (schemaPath) => { userForm = form(this.userModel, (schemaPath) => {
// Conditionally disabled // Conditionally disabled
disabled(schemaPath.password, ({valueOf}) => !valueOf(schemaPath.createAccount)); disabled(schemaPath.password, {when: ({valueOf}) => !valueOf(schemaPath.createAccount)});
// Conditionally hidden (does NOT remove from model, just marks as hidden) // Conditionally hidden (does NOT remove from model, just marks as hidden)
hidden(schemaPath.shippingAddress, ({valueOf}) => valueOf(schemaPath.sameAsBilling)); hidden(schemaPath.shippingAddress, {when: ({valueOf}) => valueOf(schemaPath.sameAsBilling)});
// Readonly // Readonly
readonly(schemaPath.username); readonly(schemaPath.username);
@ -170,10 +170,20 @@ Do _NOT_ bind the `name` field.
When using `[formField]`, you MUST NOT set the following attributes in the template (either static or bound): When using `[formField]`, you MUST NOT set the following attributes in the template (either static or bound):
- `min`, `max` (Use validators in the schema instead) - `min`, `max` (Use validators in the schema instead)
- `value`, `[value]`, `[attr.value]` (Already handled by `[formField]`) - `value`, `[value]`, `[attr.value]` on **text/number/date inputs** (Already handled by `[formField]`)
- `[attr.min]`, `[attr.max]` - `[attr.min]`, `[attr.max]`
- `[disabled]`, `[readonly]` (Already handled by `[formField]`) - `[disabled]`, `[readonly]` (Already handled by `[formField]`)
**Exception**: Static `value` on `<input type="radio">` and `<input type="checkbox">` is **allowed and required** — it identifies which option the input represents, not the bound field value.
```html
<!-- CORRECT: value on radio specifies which option this button represents -->
<input type="radio" value="economy" [formField]="bookingForm.package.tier" />
<!-- WRONG: value binding on a regular input -->
<input [value]="someVar" [formField]="form.name" />
```
Do NOT do this: `<input min="1" [formField]>` or `<input [value]="val" [formField]>`. Do NOT do this: `<input min="1" [formField]>` or `<input [value]="val" [formField]>`.
```html ```html
@ -507,7 +517,7 @@ form(
## Common Pitfalls (DO NOT DO THESE) ## Common Pitfalls (DO NOT DO THESE)
| Error Scenario | WRONG (Common Mistake) | RIGHT (Correct Way) | | Error Scenario | WRONG (Common Mistake) | RIGHT (Correct Way) |
| :--------------------- | :-------------------------------------------- | :---------------------------------------------------------- | | :--------------------- | :-------------------------------------------- | :------------------------------------------------------------------------------- |
| **Accessing Flags** | `form.field.valid()` | `form.field().valid()` | | **Accessing Flags** | `form.field.valid()` | `form.field().valid()` |
| **Accessing value** | `form.field.value()` | `form.field().value()` | | **Accessing value** | `form.field.value()` | `form.field().value()` |
| **Setting value** | `form.field.set(x)` | Update model signal: `this.model.update(...)` | | **Setting value** | `form.field.set(x)` | Update model signal: `this.model.update(...)` |
@ -520,7 +530,7 @@ form(
| **Multi-select array** | `<select [formField]="form.tags">` (string[]) | Use checkboxes for array fields | | **Multi-select array** | `<select [formField]="form.tags">` (string[]) | Use checkboxes for array fields |
| **readonly attribute** | `<input readonly [formField]>` | Use `readonly()` rule in schema | | **readonly attribute** | `<input readonly [formField]>` | Use `readonly()` rule in schema |
| **min/max attributes** | `<input min="1" max="10">` | Use `min()` and `max()` rules in schema | | **min/max attributes** | `<input min="1" max="10">` | Use `min()` and `max()` rules in schema |
| **value binding** | `<input [value]="val">` | Do NOT use `[value]` with `[formField]` | | **value binding** | `<input [value]="val">` | Do NOT use `[value]` with `[formField]` (static `value` on radio/checkbox is OK) |
| **when option** | `pattern(p.x, /.../, {when: ...})` | `when` only works with `required()` | | **when option** | `pattern(p.x, /.../, {when: ...})` | `when` only works with `required()` |
| **Submit callback** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` | | **Submit callback** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` |
| **Async params** | `params: s.field` | `params: ({ value }) => value()` | | **Async params** | `params: s.field` | `params: ({ value }) => value()` |
@ -538,7 +548,7 @@ form(
### `src/app/app.ts` ### `src/app/app.ts`
```ts ```ts
import {Component, signal, ChangeDetectionStrategy} from '@angular/core'; import {Component, signal} from '@angular/core';
import { import {
form, form,
FormField, FormField,
@ -553,13 +563,11 @@ import {
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true,
imports: [FormField], imports: [FormField],
templateUrl: './app.html', templateUrl: './app.html',
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class App { export class App {
model = signal({ protected readonly model = signal({
personalInfo: { personalInfo: {
firstName: '', firstName: '',
lastName: '', lastName: '',
@ -577,7 +585,7 @@ export class App {
companions: [] as Array<{name: string; relation: string}>, companions: [] as Array<{name: string; relation: string}>,
}); });
bookingForm = form(this.model, (s) => { protected readonly bookingForm = form(this.model, (s) => {
required(s.personalInfo.firstName, {message: 'First name is required'}); required(s.personalInfo.firstName, {message: 'First name is required'});
required(s.personalInfo.lastName, {message: 'Last name is required'}); required(s.personalInfo.lastName, {message: 'Last name is required'});
required(s.personalInfo.email, {message: 'Email is required'}); required(s.personalInfo.email, {message: 'Email is required'});
@ -598,7 +606,7 @@ export class App {
}); });
// valueOf is used to access values of other fields in rules // valueOf is used to access values of other fields in rules
hidden(s.package.extras, ({valueOf}) => valueOf(s.package.tier) === 'economy'); hidden(s.package.extras, {when: ({valueOf}) => valueOf(s.package.tier) === 'economy'});
applyEach(s.companions, (companion) => { applyEach(s.companions, (companion) => {
required(companion.name, {message: 'Companion name required'}); required(companion.name, {message: 'Companion name required'});
@ -822,7 +830,7 @@ min(s.age, 18); max(s.age, 99); // Then just:
</select> </select>
<!-- OR - Map to boolean fields in the model --> <!-- OR - Map to boolean fields in the model -->
model = signal({ hasWifi: false, hasGym: false }); protected readonly model = signal({ hasWifi: false, hasGym: false });
<input type="checkbox" [formField]="form.hasWifi" /> <input type="checkbox" [formField]="form.hasWifi" />
``` ```
@ -875,7 +883,7 @@ import {FormState} from '@angular/forms/signals';
{{ totalPrice() | number:'1.2-2' }} {{ totalPrice() | number:'1.2-2' }}
<!-- RIGHT - format in the component --> <!-- RIGHT - format in the component -->
totalPriceFormatted = computed(() => this.totalPrice().toFixed(2)); protected readonly totalPriceFormatted = computed(() => this.totalPrice().toFixed(2));
<!-- then: --> <!-- then: -->
{{ totalPriceFormatted() }} {{ totalPriceFormatted() }}
``` ```

9
.agents/skills/angular-developer/references/testing-fundamentals.md

@ -24,13 +24,10 @@ describe('MyComponent', () => {
let fixture: ComponentFixture<MyComponent>; let fixture: ComponentFixture<MyComponent>;
let h1: HTMLElement; let h1: HTMLElement;
beforeEach(async () => { beforeEach(() => {
// 1. Configure the test module TestBed.configureTestingModule({});
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();
// 2. Create the component fixture // Create the component fixture
fixture = TestBed.createComponent(MyComponent); fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1'); h1 = fixture.nativeElement.querySelector('h1');

2
skills-lock.json

@ -5,7 +5,7 @@
"source": "angular/skills", "source": "angular/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "angular-developer/SKILL.md", "skillPath": "angular-developer/SKILL.md",
"computedHash": "28eb592b92e5a24c4e3a1c0229a854069f0b8c49bed7b8d2bf6b852812dbe214" "computedHash": "ded1e95fb8d75d60901201665c6ab7e348eec3b79a6c53824c4a30a487d99f58"
}, },
"karpathy-guidelines": { "karpathy-guidelines": {
"source": "multica-ai/andrej-karpathy-skills", "source": "multica-ai/andrej-karpathy-skills",

Loading…
Cancel
Save