angular-agents-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/adapters/claude/index.ts +53 -0
- package/adapters/codex/index.ts +55 -0
- package/adapters/copilot/index.ts +45 -0
- package/adapters/cursor/index.ts +51 -0
- package/adapters/opencode/index.ts +63 -0
- package/agents/angular-architect/agent.md +143 -0
- package/agents/angular-architect/configs/claude.yaml +3 -0
- package/agents/angular-architect/configs/codex.yaml +2 -0
- package/agents/angular-architect/configs/opencode.yaml +5 -0
- package/agents/angular-migrator/agent.md +146 -0
- package/agents/angular-migrator/configs/claude.yaml +3 -0
- package/agents/angular-migrator/configs/codex.yaml +2 -0
- package/agents/angular-migrator/configs/opencode.yaml +5 -0
- package/agents/angular-reviewer/agent.md +74 -0
- package/agents/angular-reviewer/configs/claude.yaml +3 -0
- package/agents/angular-reviewer/configs/codex.yaml +2 -0
- package/agents/angular-reviewer/configs/opencode.yaml +5 -0
- package/dist/adapters/claude/index.js +45 -0
- package/dist/adapters/codex/index.js +46 -0
- package/dist/adapters/copilot/index.js +37 -0
- package/dist/adapters/cursor/index.js +43 -0
- package/dist/adapters/opencode/index.js +53 -0
- package/dist/src/cli.js +293 -0
- package/dist/src/index.js +6 -0
- package/dist/src/registry.js +13 -0
- package/dist/src/types.js +1 -0
- package/package.json +45 -0
- package/skills/architecture/injection-tokens/SKILL.md +82 -0
- package/skills/architecture/overlay-animation-lifecycle/SKILL.md +98 -0
- package/skills/components/content-projection-ng/SKILL.md +89 -0
- package/skills/components/dynamic-components/SKILL.md +74 -0
- package/skills/components/modern-host-bindings/SKILL.md +71 -0
- package/skills/components/viewchild-contentchild-signals/SKILL.md +66 -0
- package/skills/libraries/library-versioning/SKILL.md +49 -0
- package/skills/libraries/monorepo-ng-packagr/SKILL.md +69 -0
- package/skills/libraries/standalone-component-library/SKILL.md +103 -0
- package/skills/performance/control-flow-syntax/SKILL.md +94 -0
- package/skills/performance/defer-blocks/SKILL.md +83 -0
- package/skills/quality/pr-reviewer/SKILL.md +131 -0
- package/skills/quality/vitest-angular-components/SKILL.md +88 -0
- package/skills/reactivity/signals-effects/SKILL.md +63 -0
- package/skills/reactivity/signals-inputs-outputs/SKILL.md +76 -0
- package/skills/reactivity/signals-state-management/SKILL.md +70 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-reviewer
|
|
3
|
+
description: 'Review a Pull Request using the Azure DevOps MCP. Use this skill to fetch PR details, changed files, and provide a structured architectural review. Trigger on: "review PR", "revisa la PR", "PR review", "check pull request".'
|
|
4
|
+
|
|
5
|
+
argument-hint: 'PR number or URL'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Pull Request Reviewer Skill
|
|
9
|
+
|
|
10
|
+
You are a senior architect reviewing a Pull Request.
|
|
11
|
+
|
|
12
|
+
## 1. Context Injection (MANDATORY)
|
|
13
|
+
For all Azure DevOps MCP tool calls, you MUST explicitly use:
|
|
14
|
+
- **Organization:** `<ORGANIZATION>`
|
|
15
|
+
- **Project:** `<PROJECT>`
|
|
16
|
+
- **Repository:** `<REPOSITORY>`
|
|
17
|
+
|
|
18
|
+
> **Setup:** Replace the placeholders above with your Azure DevOps values before using this skill.
|
|
19
|
+
|
|
20
|
+
## 2. Output & Language Rules (STRICT)
|
|
21
|
+
- **SILENT BACKGROUND PROCESSING:** Run all data-fetching tools silently in the background. Do not output logs, intermediate thoughts, status updates, or tool execution steps.
|
|
22
|
+
- **INTERACTIVE GATEWAY:** Your very first output in the chat must be the complete review draft (following the Output Format) immediately followed by the approval question.
|
|
23
|
+
- **LANGUAGE:** All reviews, draft comments, and interactions must be written entirely in Spanish.
|
|
24
|
+
|
|
25
|
+
## 3. Step-by-Step Execution Flow
|
|
26
|
+
|
|
27
|
+
### Step 1 — Fetch Metadata and Changes
|
|
28
|
+
1. Call `mcp_ado_repo_pull_request` with `action: 'get'` to get basic PR details.
|
|
29
|
+
2. Call `mcp_ado_repo_pull_request` with `action: 'get_changes'`. The inline `lineDiffBlocks` inside this response is your **ONLY source of truth** for files and modified lines.
|
|
30
|
+
|
|
31
|
+
### Step 2 — Analyze Diffs and Generate Draft
|
|
32
|
+
Review each changed file using **ONLY** the inline diffs from Step 1.
|
|
33
|
+
- Present the review in the chat using the exact **Output Format** defined below.
|
|
34
|
+
- At the absolute end of your response, append this exact question:
|
|
35
|
+
> "¿Publico estos comentarios en la PR o quieres modificar algo primero?"
|
|
36
|
+
- **STOP HERE.** Do NOT call any write tools or threads yet.
|
|
37
|
+
|
|
38
|
+
### Step 3 — Publish Upon Explicit Confirmation
|
|
39
|
+
- Wait for the user's confirmation (e.g., "sí", "adelante", "ok", "publícalos").
|
|
40
|
+
- Once confirmed, publish each approved comment as an individual thread using `mcp_ado_repo_pull_request_thread_write` with `action: 'create'`.
|
|
41
|
+
- If modifications are requested, update the draft and re-ask before publishing.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 4. Scope and Forbidden Actions (CRITICAL)
|
|
46
|
+
- **Models are Classes:** If your project convention defines models as `class` (not `interface`), do NOT suggest converting a model class to an interface. Adapt this rule to your project's conventions.
|
|
47
|
+
- **Strict PR Scope:** Review only the lines changed in the PR. Do NOT read full files with `mcp_ado_repo_file` (`action: 'get_content'`) unless the inline diff context is completely unreadable (e.g., verifying a heavily broken import shape).
|
|
48
|
+
- **No Codebase Exploration:** Do NOT use global search tools (`search/*`). Do NOT read files from the target branch (`dev`/`main`). Do NOT follow import chains into unrelated files.
|
|
49
|
+
- **Exclusions:** Do not review or comment on test code, and do not suggest unrelated refactors outside the explicit PR diff scope.
|
|
50
|
+
- **Efficiency:** Process each file exactly once. Do not loop or re-read files. If a file has no issues, explicitly confirm it is correct; do not skip it.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 5. Comment Tone & Voice
|
|
55
|
+
- **Teammate Persona:** Colloquial, direct, short sentences. Avoid corporate or hyper-formal phrasing.
|
|
56
|
+
- **Suggestion-First:** Frame issues gently ("te sugiero", "podrías", "¿qué te parece si...?").
|
|
57
|
+
- **Concise Whys:** Maximum 1 sentence explaining the technical reason behind a suggestion.
|
|
58
|
+
- **Prose Over Bullets:** If a point takes 2-3 sentences, write it as a fluid paragraph instead of a bullet list.
|
|
59
|
+
- **Genuine Approvals:** Keep positive validations brief and authentic.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 6. Output Format
|
|
65
|
+
|
|
66
|
+
### 6.1 Header block (once, at the top of the full review)
|
|
67
|
+
|
|
68
|
+
```md
|
|
69
|
+
# Revisión de PR #[número] — [título de la PR]
|
|
70
|
+
|
|
71
|
+
**Autor:** [nombre]
|
|
72
|
+
**Rama:** `[source]` → `[target]`
|
|
73
|
+
**Archivos modificados:** [N]
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 6.2 Per-file block (repeat for every changed file)
|
|
79
|
+
|
|
80
|
+
```md
|
|
81
|
+
## 📄 `ruta/del/archivo-modificado.ts`
|
|
82
|
+
|
|
83
|
+
### ✅ [breve descripción de lo que cambió y está bien]
|
|
84
|
+
Bien hecho. [Una oración de por qué se ajusta a las convenciones del proyecto.]
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 🔴 Blocker: [título corto del problema]
|
|
89
|
+
|
|
90
|
+
**📍** `NombreClase.nombreMetodo` — línea ~[N]
|
|
91
|
+
|
|
92
|
+
[Máximo 2–3 oraciones: qué está mal, por qué rompe el contrato o la seguridad, y qué hacer.]
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// código corregido
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### 🟡 Sugerencia: [título corto]
|
|
101
|
+
|
|
102
|
+
**📍** `NombreClase.nombrePropiedad` — línea ~[N]
|
|
103
|
+
|
|
104
|
+
[Máximo 2–3 oraciones: qué desvío hay, por qué importa y cómo corregirlo.]
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// código sugerido
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 6.3 Summary block (once, at the end of the full review)
|
|
114
|
+
|
|
115
|
+
```md
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 📊 Resumen
|
|
119
|
+
|
|
120
|
+
| Nivel | Cantidad |
|
|
121
|
+
|---|---|
|
|
122
|
+
| 🔴 Blockers | N |
|
|
123
|
+
| 🟡 Sugerencias | N |
|
|
124
|
+
| 🟢 Sin problemas | N |
|
|
125
|
+
|
|
126
|
+
[1–2 oraciones de cierre: valoración global de la PR y siguiente paso recomendado.]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 6.4 Approval gate (last line, always)
|
|
130
|
+
|
|
131
|
+
> "¿Publico estos comentarios en la PR o quieres modificar algo primero?"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vitest-angular-components
|
|
3
|
+
description: Configure and write component tests for standalone Angular components using Vitest browser mode with Playwright, including TestBed setup and overlay cleanup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Angular Components with Vitest (Browser Mode)
|
|
7
|
+
|
|
8
|
+
This skill covers setting up and writing tests for standalone Angular components using Vitest's browser mode (real browser via Playwright) instead of Karma/Jasmine.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Writing unit/component tests for a standalone Angular component/directive/service in this kind of workspace.
|
|
12
|
+
- Setting up a new library's test configuration to match the workspace's existing Vitest setup.
|
|
13
|
+
- Debugging flaky tests caused by leftover overlay DOM elements between tests.
|
|
14
|
+
|
|
15
|
+
## `vitest.config.ts` essentials
|
|
16
|
+
```typescript
|
|
17
|
+
import { defineConfig } from 'vitest/config';
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
test: {
|
|
21
|
+
browser: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
provider: 'playwright',
|
|
24
|
+
instances: [{ browser: 'chromium' }],
|
|
25
|
+
viewport: { width: 1920, height: 1080 },
|
|
26
|
+
},
|
|
27
|
+
setupFiles: ['./setup-vitest.ts'],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## `setup-vitest.ts` — global TestBed initialization
|
|
33
|
+
```typescript
|
|
34
|
+
import '@analogjs/vitest-angular/setup-zone'; // or the workspace's equivalent zoneless setup
|
|
35
|
+
import { getTestBed } from '@angular/core/testing';
|
|
36
|
+
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
|
|
37
|
+
|
|
38
|
+
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
|
|
39
|
+
|
|
40
|
+
// Global ResizeObserver mock — required since components query element size in effects
|
|
41
|
+
global.ResizeObserver = class {
|
|
42
|
+
observe() {}
|
|
43
|
+
unobserve() {}
|
|
44
|
+
disconnect() {}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// Clean up any overlay elements dynamically appended to <body>
|
|
49
|
+
// (see dynamic-component-creation / overlay-animation-lifecycle skills)
|
|
50
|
+
document.querySelectorAll('.modal, .popover, .toast, .tooltip, .slide-panel, .overlay')
|
|
51
|
+
.forEach(el => el.remove());
|
|
52
|
+
document.body.classList.remove('overlay-open');
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
This cleanup step is critical: overlay services attach elements directly to `document.body` (outside the component fixture), so `TestBed`'s automatic teardown never removes them — leftover nodes leak state and z-index stacking into the next test.
|
|
56
|
+
|
|
57
|
+
## Writing a component test
|
|
58
|
+
```typescript
|
|
59
|
+
import { render, screen } from '@testing-library/angular'; // or TestBed.createComponent directly
|
|
60
|
+
import { ButtonComponent } from './ui-button.component';
|
|
61
|
+
|
|
62
|
+
describe('ButtonComponent', () => {
|
|
63
|
+
it('applies the size and type host classes', async () => {
|
|
64
|
+
const fixture = TestBed.createComponent(ButtonComponent);
|
|
65
|
+
fixture.componentRef.setInput('size', 'big');
|
|
66
|
+
fixture.componentRef.setInput('type', 'primary');
|
|
67
|
+
fixture.detectChanges();
|
|
68
|
+
|
|
69
|
+
const host = fixture.nativeElement as HTMLElement;
|
|
70
|
+
expect(host.classList.contains('ui-button-size-big')).toBe(true);
|
|
71
|
+
expect(host.classList.contains('ui-button-type-primary')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
Use `fixture.componentRef.setInput(name, value)` to set signal `input()`s from tests — direct property assignment (`component.size = 'big'`) does not work since inputs are read-only signals.
|
|
76
|
+
|
|
77
|
+
## Testing signal-driven effects/computed values
|
|
78
|
+
- Call `fixture.detectChanges()` (or `await fixture.whenStable()`) after changing an input to allow `effect()`/`afterRenderEffect()` to flush.
|
|
79
|
+
- For `computed()` values, just read `component.someComputed()` directly — no `detectChanges()` needed since computed signals update synchronously on read.
|
|
80
|
+
|
|
81
|
+
## Testing overlay-based components (modal/popover/toast)
|
|
82
|
+
- Trigger the service call (`modalService.create(...)`) inside the test, then query `document.body` (not the fixture) for the rendered overlay content, since it's mounted outside the fixture's DOM tree.
|
|
83
|
+
- Await the `afterOpened$`/`afterClosed$` observables exposed by the returned ref rather than arbitrary timeouts, to synchronize with the animation lifecycle (see `overlay-animation-lifecycle`).
|
|
84
|
+
|
|
85
|
+
## Pitfalls
|
|
86
|
+
- Forgetting the `afterEach` overlay cleanup causes intermittent failures when tests run in sequence (duplicate `role="dialog"` elements, click interception by a stale backdrop).
|
|
87
|
+
- Testing `@defer` blocks requires explicitly resolving the deferred state (e.g. via Angular's testing utilities for defer blocks) — a plain `detectChanges()` will not render the deferred content on its own.
|
|
88
|
+
- Since tests run in a real browser (Playwright/Chromium), avoid Node-only APIs in test files; use browser-safe utilities.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: signals-effects
|
|
3
|
+
description: Use effect() and afterRenderEffect() correctly for signal-driven side effects, replacing ngOnChanges, and avoiding infinite update loops.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Signal Effects (`effect()` / `afterRenderEffect()`)
|
|
7
|
+
|
|
8
|
+
This skill covers running side effects in response to signal changes, and choosing between `effect()` and `afterRenderEffect()`.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- You need to react to a signal/input change with a side effect that is **not** a derived value (DOM manipulation, calling a service, syncing to a non-signal API).
|
|
12
|
+
- You need logic that must run **after** the DOM has been updated/rendered (measuring elements, `requestAnimationFrame` work, `ResizeObserver` setup).
|
|
13
|
+
- You are replacing legacy `ngOnChanges` lifecycle logic in a component that has been migrated to signal inputs.
|
|
14
|
+
|
|
15
|
+
## `effect()`
|
|
16
|
+
Runs in the reactive context, tracking any signals read synchronously inside it. Must be created in an injection context (constructor, or field initializer of a component/directive/service).
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
constructor() {
|
|
20
|
+
effect(() => {
|
|
21
|
+
if (this.slotLeft().nativeElement.children.length > 0) {
|
|
22
|
+
this.slotLeftClassValue.set(true);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- Only for side effects — do not use it to compute a value another part of the component needs (use `computed()` instead).
|
|
30
|
+
- Avoid writing to a signal that the same effect also reads — this causes infinite loops. If you must write to a signal inside an effect, make sure it's not one of the effect's own dependencies.
|
|
31
|
+
- Effects run once immediately on creation, then again whenever a tracked signal changes.
|
|
32
|
+
|
|
33
|
+
## `afterRenderEffect()`
|
|
34
|
+
Same reactive semantics as `effect()`, but guaranteed to run after Angular has finished rendering the current change detection cycle. Use it for anything that reads the DOM or needs layout to be settled.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
constructor() {
|
|
38
|
+
afterRenderEffect(() => {
|
|
39
|
+
const displayedColumns: string[] = this.displayedColumns();
|
|
40
|
+
const lockedColumns: string[] = this.lockedColumns();
|
|
41
|
+
this.setTableElements();
|
|
42
|
+
requestAnimationFrame(() => {
|
|
43
|
+
this.minTableWidth.set(this.getColumnsWidth(true));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use this instead of `effect()` whenever the callback:
|
|
50
|
+
- Reads `ElementRef`/`nativeElement` geometry (`offsetWidth`, `getBoundingClientRect`).
|
|
51
|
+
- Needs to run after `viewChild()`/`contentChild()` results are guaranteed to reflect the latest template.
|
|
52
|
+
|
|
53
|
+
## Replacing `ngOnChanges`
|
|
54
|
+
| Legacy pattern | Signals replacement |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `ngOnChanges(changes)` reacting to one `@Input()` | `effect(() => { const v = this.myInput(); ... })` |
|
|
57
|
+
| `ngOnChanges` computing a derived value | `computed()` — no effect needed |
|
|
58
|
+
| `ngOnChanges` doing DOM work after an input change | `afterRenderEffect()` |
|
|
59
|
+
|
|
60
|
+
## Pitfalls
|
|
61
|
+
- Don't call `.set()` on the same signal you're reading in the same `effect()` without an `untracked()` guard — infinite loop risk.
|
|
62
|
+
- Don't use `effect()` for value derivation reachable via `computed()` — effects can't be used as inputs to templates and add unnecessary reactivity overhead.
|
|
63
|
+
- Remember `effect()`/`afterRenderEffect()` need an injection context; if created outside the constructor (e.g. in a method), pass `{ injector }` explicitly.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: signals-inputs-outputs
|
|
3
|
+
description: Migrate Angular @Input()/@Output() decorators to the modern signal-based input(), output(), and model() APIs with defaults, required fields, and transforms.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Signals-based Inputs & Outputs
|
|
7
|
+
|
|
8
|
+
This skill helps write or refactor Angular component inputs/outputs using the Angular 17+ functional API instead of the `@Input()`/`@Output()` decorators.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Creating a new standalone component that needs configurable properties or events.
|
|
12
|
+
- Refactoring a component that still uses `@Input()`/`@Output()` decorators.
|
|
13
|
+
- Needing two-way bound properties (replacement for `@Input() x` + `@Output() xChange`).
|
|
14
|
+
|
|
15
|
+
## Key APIs
|
|
16
|
+
|
|
17
|
+
### `input()`
|
|
18
|
+
```typescript
|
|
19
|
+
// Optional input with a default value
|
|
20
|
+
size = input<'small' | 'medium' | 'big'>('medium');
|
|
21
|
+
|
|
22
|
+
// Required input (no default, must be provided by the consumer)
|
|
23
|
+
label = input.required<string>();
|
|
24
|
+
|
|
25
|
+
// Input with a transform function (e.g. coerce string attribute to boolean)
|
|
26
|
+
disabled = input(false, { transform: booleanAttribute });
|
|
27
|
+
|
|
28
|
+
// Alias the public input name
|
|
29
|
+
value = input<string>('', { alias: 'inputValue' });
|
|
30
|
+
```
|
|
31
|
+
Inputs are read-only signals. Read them with `this.size()`, never assign directly.
|
|
32
|
+
|
|
33
|
+
### `output()`
|
|
34
|
+
```typescript
|
|
35
|
+
calendarOpened = output<void>();
|
|
36
|
+
searchEvent = output<string>();
|
|
37
|
+
rowSelected = output<any>();
|
|
38
|
+
|
|
39
|
+
// Emitting
|
|
40
|
+
this.calendarOpened.emit();
|
|
41
|
+
this.searchEvent.emit(this.searchText());
|
|
42
|
+
```
|
|
43
|
+
`output()` replaces `@Output() x = new EventEmitter<T>()`. No need to import `EventEmitter` for this case.
|
|
44
|
+
|
|
45
|
+
### `model()` (two-way binding)
|
|
46
|
+
```typescript
|
|
47
|
+
// Component
|
|
48
|
+
checked = model<boolean>(false);
|
|
49
|
+
|
|
50
|
+
// Usage in template: [(checked)]="isChecked"
|
|
51
|
+
this.checked.set(true); // updates the bound parent value automatically
|
|
52
|
+
```
|
|
53
|
+
Use `model()` only when the component genuinely needs to mutate a parent-bound value (e.g. checkbox, toggle). Prefer plain `input()` + `output()` when the parent should stay in control of the source of truth.
|
|
54
|
+
|
|
55
|
+
## Migration checklist
|
|
56
|
+
1. Replace `@Input() foo: T;` → `foo = input<T>();` (or `input.required<T>()` if it had no default and was always provided).
|
|
57
|
+
2. Replace `@Output() foo = new EventEmitter<T>();` → `foo = output<T>();`.
|
|
58
|
+
3. Remove `EventEmitter`/`Input`/`Output` imports that are no longer used.
|
|
59
|
+
4. Update every internal read of `this.foo` to `this.foo()` (inputs are signals now).
|
|
60
|
+
5. Update templates: input signals are read the same way in bindings (`[value]="foo()"` if read inside a getter/computed, but plain property bindings from the parent don't need `()`— only accessed from within the component's own TS/template as a signal call).
|
|
61
|
+
6. If the input value was watched via `ngOnChanges`, replace with `effect()` reading the signal, or `computed()` if it's a pure derivation (see the `signals-effects` and `signals-state-management` skills).
|
|
62
|
+
|
|
63
|
+
## Example
|
|
64
|
+
```typescript
|
|
65
|
+
@Component({ selector: 'ui-button', ... })
|
|
66
|
+
export class ButtonComponent {
|
|
67
|
+
size = input<ButtonSize>('medium');
|
|
68
|
+
type = input<ButtonType>('primary');
|
|
69
|
+
buttonType = input<'button' | 'submit' | 'reset'>('button');
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Pitfalls
|
|
74
|
+
- Do not use `input()` results as a mutable variable — they are read-only; use `signal()` for local mutable state instead.
|
|
75
|
+
- Don't wrap `input()` calls in `effect()` unnecessarily if a `computed()` would do — see `signals-state-management`.
|
|
76
|
+
- `model()` creates an implicit two-way contract with the parent; document it clearly since it's easy to miss in code review.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: signals-state-management
|
|
3
|
+
description: Manage local Angular component state reactively using signal(), computed(), and linkedSignal(), and know when to use each.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Signals State Management
|
|
7
|
+
|
|
8
|
+
This skill covers reactive local state in Angular components using the Signals primitives `signal()`, `computed()`, and `linkedSignal()`.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- A component needs mutable internal state (selection, open/closed, search text, loading flags).
|
|
12
|
+
- A value is fully derived from other signals/inputs (should never be set directly).
|
|
13
|
+
- A piece of state should reset/re-sync whenever an upstream signal (often an `input()`) changes, but can also be locally overridden afterward.
|
|
14
|
+
|
|
15
|
+
## Key APIs
|
|
16
|
+
|
|
17
|
+
### `signal()` — mutable state
|
|
18
|
+
```typescript
|
|
19
|
+
selectedItems = signal<any[]>([]);
|
|
20
|
+
searchText = signal<string>('');
|
|
21
|
+
isLoading = signal<boolean>(false);
|
|
22
|
+
|
|
23
|
+
// Mutating
|
|
24
|
+
this.searchText.set('new value');
|
|
25
|
+
this.selectedItems.update(items => [...items, newItem]);
|
|
26
|
+
```
|
|
27
|
+
Use `signal()` for state that is genuinely owned by the component and changes over time (event handlers, user interaction).
|
|
28
|
+
|
|
29
|
+
### `computed()` — derived, read-only state
|
|
30
|
+
```typescript
|
|
31
|
+
isAllSelected = computed<boolean>(() =>
|
|
32
|
+
!this.isLoading() && this.flatData().length > 0 &&
|
|
33
|
+
this.flatData().every(item =>
|
|
34
|
+
this.selectedItems().findIndex(x => x[this.keyValue()] === item[this.keyValue()]) !== -1
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
hasFooter = computed<boolean>(() =>
|
|
39
|
+
Object.keys(this.templates()).some(key => this.templates()[key].footerTemplate())
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
Rules:
|
|
43
|
+
- Never call `.set()`/`.update()` on a `computed()` — it has no setter.
|
|
44
|
+
- Keep the function pure — no side effects, no `console.log` mutating outside state.
|
|
45
|
+
- Prefer `computed()` over `effect()` whenever the goal is "produce a value", not "perform an action".
|
|
46
|
+
|
|
47
|
+
### `linkedSignal()` — resettable/derived-but-overridable state
|
|
48
|
+
```typescript
|
|
49
|
+
// Datepicker: selectedDate starts synced to the `date` input,
|
|
50
|
+
// but the user can pick a different date locally afterward.
|
|
51
|
+
date = signal<CalendarSelectedDatesNullable>(null);
|
|
52
|
+
selectedDate = linkedSignal<CalendarSelectedDatesNullable>(() => this.date());
|
|
53
|
+
```
|
|
54
|
+
Use `linkedSignal()` instead of `computed()` when:
|
|
55
|
+
- The value should re-initialize whenever the source signal changes, AND
|
|
56
|
+
- The component also needs to write a *different* value locally afterward (something `computed()` cannot do, since it's read-only).
|
|
57
|
+
|
|
58
|
+
Common use case: a component receives a value via `input()`, wants to let the user modify a local working copy, but reset that working copy whenever the input changes (e.g. date pickers, editable form drafts).
|
|
59
|
+
|
|
60
|
+
## Decision guide
|
|
61
|
+
| Need | Use |
|
|
62
|
+
|---|---|
|
|
63
|
+
| Mutable state owned by the component | `signal()` |
|
|
64
|
+
| Value 100% derived from other signals, never set directly | `computed()` |
|
|
65
|
+
| Value initialized from another signal, but can diverge afterward | `linkedSignal()` |
|
|
66
|
+
|
|
67
|
+
## Pitfalls
|
|
68
|
+
- Don't put expensive/impure logic inside `computed()` — it re-runs on every dependency change.
|
|
69
|
+
- Don't use `signal()` for something that's purely derived — duplicating state via `effect()` + `signal()` when `computed()` would work leads to sync bugs.
|
|
70
|
+
- Remember `linkedSignal()` still returns a writable signal — treat writes as deliberate local overrides, not permanent replacements of the source signal.
|