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,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dynamic-components
|
|
3
|
+
description: Create and mount Angular components dynamically at runtime using createComponent() and ApplicationRef, and tear them down safely (used for modals, popovers, toasts).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dynamic Component Creation
|
|
7
|
+
|
|
8
|
+
This skill covers programmatically creating and mounting Angular components outside the normal template tree — the foundation of overlay services (modal, popover, toast, tooltip).
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Building a service that opens a modal/dialog/toast/popover imperatively (`.open(SomeComponent, config)`).
|
|
12
|
+
- Any UI that must be rendered detached from its "logical" parent component in the DOM (e.g. attached to `document.body` to escape `overflow: hidden`/z-index contexts).
|
|
13
|
+
|
|
14
|
+
## Creating a component
|
|
15
|
+
```typescript
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class ModalService {
|
|
18
|
+
private readonly applicationRef = inject(ApplicationRef);
|
|
19
|
+
private readonly injector = inject(Injector);
|
|
20
|
+
|
|
21
|
+
create(component: Type<unknown>, config?: unknown): DialogRef {
|
|
22
|
+
const dialogRef = new DialogRef(/* ... */);
|
|
23
|
+
|
|
24
|
+
const componentRef = createComponent(component, {
|
|
25
|
+
environmentInjector: this.applicationRef.injector,
|
|
26
|
+
elementInjector: Injector.create({
|
|
27
|
+
providers: [{ provide: DIALOG_REF, useValue: dialogRef }],
|
|
28
|
+
parent: this.injector,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Attach the host view to change detection
|
|
33
|
+
this.applicationRef.attachView(componentRef.hostView);
|
|
34
|
+
|
|
35
|
+
// Mount into the DOM (typically document.body for overlays)
|
|
36
|
+
document.body.appendChild(componentRef.location.nativeElement);
|
|
37
|
+
|
|
38
|
+
dialogRef.componentRef = componentRef;
|
|
39
|
+
return dialogRef;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Key pieces
|
|
45
|
+
- `createComponent(Component, options)` — instantiates the component and its view without a host template.
|
|
46
|
+
- `ApplicationRef.attachView(hostView)` — registers the view with Angular's change detection so signal/input changes are picked up. **Required**, otherwise the component silently never updates.
|
|
47
|
+
- `elementInjector` (via `Injector.create`) — the mechanism for providing per-instance tokens (like a `DIALOG_REF` the created component can `inject()` optionally — see `injection-tokens-pattern`).
|
|
48
|
+
- `componentRef.location.nativeElement` — the actual DOM element to insert manually into the document.
|
|
49
|
+
|
|
50
|
+
## Passing data in and getting data out
|
|
51
|
+
- **In**: provide via `elementInjector`, or set `componentRef.setInput('propName', value)` for signal `input()`s.
|
|
52
|
+
- **Out**: expose a `Subject`/`Observable` (`afterClosed$`) on the returned ref object; the created component calls `dialogRef.close(result)` which the ref translates into emitting that subject.
|
|
53
|
+
|
|
54
|
+
## Teardown
|
|
55
|
+
```typescript
|
|
56
|
+
destroy(componentRef: ComponentRef<unknown>): void {
|
|
57
|
+
this.applicationRef.detachView(componentRef.hostView);
|
|
58
|
+
componentRef.destroy();
|
|
59
|
+
componentRef.location.nativeElement.remove();
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
Always: detach the view **before** destroying, and remove the DOM node explicitly (Angular does not do this automatically for manually-appended elements).
|
|
63
|
+
|
|
64
|
+
## Coordinating with route changes
|
|
65
|
+
Overlay services in this codebase inject `Router` and subscribe to navigation events to auto-close the dialog if the user navigates away, preventing orphaned overlay elements:
|
|
66
|
+
```typescript
|
|
67
|
+
private readonly router = inject(Router);
|
|
68
|
+
this.router.events.pipe(filter(e => e instanceof NavigationStart)).subscribe(() => this.close());
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Pitfalls
|
|
72
|
+
- Forgetting `attachView`/`detachView` causes either “ghost” components that never update, or memory leaks after destroy.
|
|
73
|
+
- Not removing the DOM node on destroy leaves stale, invisible overlay elements accumulating in `document.body` (a common source of flaky tests — see the `vitest-angular-components` skill's overlay cleanup notes).
|
|
74
|
+
- Always destroy in a `finally`/guaranteed path (e.g. also on component `DestroyRef.onDestroy`) so a thrown error during open doesn't leak the component.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modern-host-bindings
|
|
3
|
+
description: Bind dynamic classes, attributes, and ARIA properties via the @Component "host" object instead of @HostBinding/@HostListener decorators.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Modern Host Bindings
|
|
7
|
+
|
|
8
|
+
This skill covers declaring dynamic host classes/attributes/ARIA declaratively in the `@Component` decorator's `host` object, avoiding `@HostBinding`/`@HostListener`.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- A component's root element needs classes that depend on signal state (size, type, disabled, open/closed).
|
|
12
|
+
- A component needs ARIA roles/attributes reflecting internal state.
|
|
13
|
+
- You're reviewing a component that still uses `@HostBinding`/`@HostListener` decorators and want to modernize it.
|
|
14
|
+
|
|
15
|
+
## Dynamic classes from signals
|
|
16
|
+
```typescript
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'ui-button',
|
|
19
|
+
host: {
|
|
20
|
+
'[class]': '"ui-button-size-" + this.size() + " ui-button-type-" + this.type()'
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
export class ButtonComponent {
|
|
24
|
+
size = input<ButtonSize>('medium');
|
|
25
|
+
type = input<ButtonType>('primary');
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Conditional single classes
|
|
30
|
+
```typescript
|
|
31
|
+
host: {
|
|
32
|
+
'[class.ui-input-disabled]': 'isDisabledClassValue()',
|
|
33
|
+
'[class.ui-slot-left]': 'slotLeftClassValue()',
|
|
34
|
+
'[class.ui-slot-right]': 'slotRightClassValue()',
|
|
35
|
+
'[class.ui-input-has-value]': 'hasValueClassValue()',
|
|
36
|
+
'[class]': '"ui-input-size-" + size()'
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
Both a computed `[class]` string binding and multiple `[class.x]` boolean bindings can coexist on the same host object.
|
|
40
|
+
|
|
41
|
+
## ARIA / attribute bindings
|
|
42
|
+
```typescript
|
|
43
|
+
host: {
|
|
44
|
+
'[class]': '"ui-" + type() + " ui-dropdown-size-" + size()',
|
|
45
|
+
'[class.ui-dropdown-disabled]': 'isDisabled()',
|
|
46
|
+
'[attr.role]': '"listbox"',
|
|
47
|
+
'[attr.aria-multiselectable]': 'multiple()'
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
Use `[attr.x]` for ARIA/attribute reflection (attributes can be `null`/removed), and `[class.x]`/`[class]` for CSS classes.
|
|
51
|
+
|
|
52
|
+
## Event bindings on the host
|
|
53
|
+
```typescript
|
|
54
|
+
host: {
|
|
55
|
+
'(click)': 'onHostClick($event)',
|
|
56
|
+
'(keydown.escape)': 'onEscape()'
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
This replaces `@HostListener('click', ['$event'])`.
|
|
60
|
+
|
|
61
|
+
## Migration from decorators
|
|
62
|
+
| Legacy | Modern |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `@HostBinding('class.disabled') get isDisabled() {...}` | `host: { '[class.disabled]': 'isDisabled()' }` |
|
|
65
|
+
| `@HostBinding('attr.role') role = 'listbox';` | `host: { '[attr.role]': '"listbox"' }` |
|
|
66
|
+
| `@HostListener('click', ['$event']) onClick(e) {...}` | `host: { '(click)': 'onClick($event)' }` |
|
|
67
|
+
|
|
68
|
+
## Pitfalls
|
|
69
|
+
- Expressions in the `host` object are evaluated in the component's context, not a template — signals must still be called with `()`.
|
|
70
|
+
- Don't mix a `[class]` string binding that already contains a class with an unrelated `[class.foo]` binding that toggles the *same* class name — Angular merges them, but it's easy to introduce conflicting logic.
|
|
71
|
+
- Keep expressions in `host` small; if logic gets complex, compute it in a `computed()` signal and reference that signal's name in the binding.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: viewchild-contentchild-signals
|
|
3
|
+
description: Query view and content children with the signal-based viewChild(), viewChildren(), contentChild(), and contentChildren() functions instead of decorators.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Signal-based ViewChild / ContentChild Queries
|
|
7
|
+
|
|
8
|
+
This skill covers reading template references and projected content using the functional query API.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- A component needs a reference to a native element or child component/directive in its own template (`viewChild`).
|
|
12
|
+
- A component needs to detect content the consumer projected into it via `<ng-content>` (`contentChild`), e.g. optional header/footer templates.
|
|
13
|
+
- Multiple matching elements need to be queried at once (`viewChildren`/`contentChildren`).
|
|
14
|
+
|
|
15
|
+
## `viewChild()` / `viewChild.required()`
|
|
16
|
+
```typescript
|
|
17
|
+
// Optional — returns Signal<ElementRef<HTMLInputElement> | undefined>
|
|
18
|
+
inputElementMaybe = viewChild<ElementRef<HTMLInputElement>>('input');
|
|
19
|
+
|
|
20
|
+
// Required — throws if not found, returns Signal<ElementRef<HTMLInputElement>>
|
|
21
|
+
inputElement = viewChild.required<ElementRef<HTMLInputElement>>('input');
|
|
22
|
+
slotLeft = viewChild.required<ElementRef<HTMLDivElement>>('slotLeft');
|
|
23
|
+
```
|
|
24
|
+
Use `.required()` whenever the template reference is guaranteed to exist for the lifetime of the component (e.g. it's not behind an `@if`). Use the plain (optional) form when the target may be conditionally rendered.
|
|
25
|
+
|
|
26
|
+
## `contentChild()`
|
|
27
|
+
```typescript
|
|
28
|
+
headerTmpl = contentChild<DropdownHeaderTmplDirective>(DropdownHeaderTmplDirective);
|
|
29
|
+
itemTmpl = contentChild<DropdownItemTmplDirective>(DropdownItemTmplDirective);
|
|
30
|
+
noItemsTmpl = contentChild<DropdownNoItemsTmplDirective>(DropdownNoItemsTmplDirective);
|
|
31
|
+
```
|
|
32
|
+
Typical pattern: define a structural/attribute directive (e.g. `*uiDropdownItemTmpl`) that consumers apply to an `<ng-template>`, then read it with `contentChild()` to know whether a custom template was provided and to get its `TemplateRef`.
|
|
33
|
+
|
|
34
|
+
## `viewChildren()` / `contentChildren()`
|
|
35
|
+
Return a `Signal<ReadonlyArray<T>>` that updates automatically when the matched set changes (e.g. items added/removed via `@for`).
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
rows = viewChildren<ElementRef<HTMLTableRowElement>>('row');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Reading query signals reactively
|
|
42
|
+
Because these are signals, they can be safely read inside `computed()` or `effect()`/`afterRenderEffect()`:
|
|
43
|
+
```typescript
|
|
44
|
+
constructor() {
|
|
45
|
+
effect(() => {
|
|
46
|
+
if (this.slotLeft().nativeElement.children.length > 0) {
|
|
47
|
+
this.slotLeftClassValue.set(true);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Migration from decorators
|
|
54
|
+
| Legacy | Signal-based |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `@ViewChild('input') inputElement!: ElementRef;` | `inputElement = viewChild.required<ElementRef>('input');` |
|
|
57
|
+
| `@ViewChildren(Item) items!: QueryList<Item>;` | `items = viewChildren(Item);` |
|
|
58
|
+
| `@ContentChild(Tmpl) tmpl?: Tmpl;` | `tmpl = contentChild(Tmpl);` |
|
|
59
|
+
| `@ContentChildren(Tmpl) tmpls!: QueryList<Tmpl>;` | `tmpls = contentChildren(Tmpl);` |
|
|
60
|
+
|
|
61
|
+
All reads change from `this.inputElement` to `this.inputElement()`.
|
|
62
|
+
|
|
63
|
+
## Pitfalls
|
|
64
|
+
- Query signals are only populated after the component's view/content has been initialized — reading them in the constructor body (outside `effect()`) will return `undefined`/throw for `.required()`.
|
|
65
|
+
- Don't use `ngAfterViewInit`/`ngAfterContentInit` alongside signal queries just to "wait for them" — use `effect()`/`afterRenderEffect()`, which already re-run once the query resolves.
|
|
66
|
+
- `QueryList`-specific APIs (`.changes` Observable) don't exist on signal queries — read the signal reactively instead of subscribing.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-versioning
|
|
3
|
+
description: Update the version of a specific library and manage its peer dependencies accordingly.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Update Library Version Skill
|
|
7
|
+
|
|
8
|
+
This skill allows the user to update the version of a specific library in the repository and automatically manages its usage in other libraries' `peerDependencies`.
|
|
9
|
+
|
|
10
|
+
## Parameters
|
|
11
|
+
The user must provide:
|
|
12
|
+
1. **Library Name**: The name of the library to update (e.g., `button`, `core`, `input`).
|
|
13
|
+
2. **Update Type**: `patch`, `minor`, or `major`.
|
|
14
|
+
|
|
15
|
+
## Process Instructions
|
|
16
|
+
|
|
17
|
+
### 1. Identify and Read the Target Library
|
|
18
|
+
1. Locate the library's `package.json` file. Based on the file structure, it is typically in `projects/[project-name]/[library-name]/package.json`.
|
|
19
|
+
2. Read the file to obtain the current `version`.
|
|
20
|
+
|
|
21
|
+
### 2. Calculate New Version
|
|
22
|
+
Calculate the new version string based on the **Update Type**:
|
|
23
|
+
- **Patch**: Increment the third number (z). Reset nothing. (e.g., `1.0.1` -> `1.0.2`)
|
|
24
|
+
- **Minor**: Increment the second number (y). Reset the third number (z) to `0`. (e.g., `1.0.5` -> `1.1.0`)
|
|
25
|
+
- **Major**: Increment the first number (x). Reset the second (y) and third (z) numbers to `0`. (e.g., `1.2.3` -> `2.0.0`)
|
|
26
|
+
|
|
27
|
+
### 3. Update the Target Library
|
|
28
|
+
1. The skill must not use any scripts or commands to update the version. Instead, it should directly edit the `package.json` file.
|
|
29
|
+
2. Edit `projects/[project-name]/[library-name]/package.json`.
|
|
30
|
+
3. Replace the old `version` value with the **New Version**.
|
|
31
|
+
|
|
32
|
+
### 4. Handle Peer Dependencies
|
|
33
|
+
**Condition**:
|
|
34
|
+
- **IF Update Type is PATCH**: Do **NOT** update references in any other files. `peerDependencies` are assumed to handle patch updates automatically (via ranges like `^` or `~`).
|
|
35
|
+
- **IF Update Type is MINOR or MAJOR**:
|
|
36
|
+
1. Search through all other libraries in `projects/[project-name]/` (look for `projects/[project-name]/*/package.json`).
|
|
37
|
+
2. In each `package.json`, check the `peerDependencies` section.
|
|
38
|
+
3. If the *Target Library* is listed in `peerDependencies`:
|
|
39
|
+
- Update its version value to the **New Version**.
|
|
40
|
+
4. Update patch version of the affected libraries if the update type is minor or major, to ensure compatibility. For example, if a library depends on `button` and the `button` library is updated from `1.0.0` to `1.1.0`, the dependent library's version should be updated from `1.0.0` to `1.0.1` (patch update) to reflect the change without breaking compatibility.
|
|
41
|
+
|
|
42
|
+
### 5. Final Report
|
|
43
|
+
Generate and display a Markdown table summarizing the actions taken:
|
|
44
|
+
|
|
45
|
+
| Library Name | Old Version | New Version | Strategy | Dependencies Updated |
|
|
46
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
47
|
+
| `<Found Name>` | `<Old>` | `<New>` | `<Type>` | `<List of other libs updated>` |
|
|
48
|
+
|
|
49
|
+
*If no dependencies were updated (e.g., for a patch), state "None" or "Skipped (Patch)".*
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monorepo-ng-packagr
|
|
3
|
+
description: Structure and operate a multi-library Angular monorepo built with ng-packagr, including workspace path aliases and orchestrated build/publish/version scripts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Angular Monorepo with ng-packagr
|
|
7
|
+
|
|
8
|
+
This skill covers organizing a workspace containing many independently-buildable Angular libraries (a design-system style monorepo), and the tooling that ties them together.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Setting up or maintaining a workspace where each UI component lives in its own publishable library under `projects/<scope>/<name>/`.
|
|
12
|
+
- Writing/maintaining Node.js scripts that build, version, or publish all (or a subset of) libraries in one command.
|
|
13
|
+
- Diagnosing "cannot find module `@scope/lib`" errors caused by stale `dist/` output or missing path aliases.
|
|
14
|
+
|
|
15
|
+
## Workspace layout
|
|
16
|
+
```
|
|
17
|
+
angular.json # one "project" entry per library + per app
|
|
18
|
+
package.json # workspaces: ["dist/ui/*"] — consumers resolve built packages
|
|
19
|
+
tsconfig.json # path aliases mapping @scope/lib -> dist/scope/lib
|
|
20
|
+
scripts/
|
|
21
|
+
build-library.mjs # builds a single library via the Angular CLI builder
|
|
22
|
+
generate-library.mjs # scaffolds a new library folder structure
|
|
23
|
+
update-library-version.mjs # bumps a library's version + dependent peerDependencies
|
|
24
|
+
publish-library.mjs # npm publish for one library
|
|
25
|
+
publish-libraries.mjs # publish all libraries in dependency order
|
|
26
|
+
tests-libraries.mjs # run tests across all libraries
|
|
27
|
+
projects/
|
|
28
|
+
ui/
|
|
29
|
+
button/ input/ core/ ... # each an independent ng-packagr library
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Why libraries depend on `dist/`, not `src/`
|
|
33
|
+
```json
|
|
34
|
+
// tsconfig.json
|
|
35
|
+
"paths": {
|
|
36
|
+
"@ui/button": ["dist/ui/button"],
|
|
37
|
+
"@ui/core": ["dist/ui/core"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
Libraries import each other via their **built** output (`dist/ui/*`), matching exactly how an external consumer would resolve the published npm package. This catches "works in source, breaks when published" bugs early, at the cost of requiring a build step before you can consume a library's latest changes from a sibling library.
|
|
41
|
+
|
|
42
|
+
## Build ordering matters
|
|
43
|
+
Libraries that depend on others (e.g. `datepicker` depends on `popover`, `input`, `calendar`, `core`) must be built **after** their dependencies. A build orchestration script typically:
|
|
44
|
+
1. Reads each library's `package.json` `peerDependencies`/`dependencies` on other `@ui/*` packages.
|
|
45
|
+
2. Topologically sorts libraries so dependencies build first.
|
|
46
|
+
3. Invokes the Angular CLI build (`ng build <project>`) for each, writing to `dist/ui/<name>`.
|
|
47
|
+
|
|
48
|
+
## Versioning across the monorepo
|
|
49
|
+
When a library's version changes (minor/major), every other library that lists it in `peerDependencies` needs a compatible version bump too (see the `update-library-version` skill in this same skills folder for the exact algorithm used in this repo).
|
|
50
|
+
|
|
51
|
+
## Publishing
|
|
52
|
+
`publish-library.mjs` typically:
|
|
53
|
+
1. Builds the library fresh.
|
|
54
|
+
2. Copies/verifies `package.json` metadata into `dist/ui/<name>`.
|
|
55
|
+
3. Runs `npm publish` from the `dist/ui/<name>` folder (never from `projects/`, since that folder contains source + config files that shouldn't ship).
|
|
56
|
+
|
|
57
|
+
`publish-libraries.mjs` wraps this for multiple libraries, respecting the same dependency order as the build step.
|
|
58
|
+
|
|
59
|
+
## Adding a brand-new library to the monorepo
|
|
60
|
+
1. Scaffold it (see `standalone-component-library` skill for the folder layout).
|
|
61
|
+
2. Add a project entry to `angular.json`.
|
|
62
|
+
3. Add its `@ui/<name>` path alias to `tsconfig.json`.
|
|
63
|
+
4. Add it to any orchestration script's library list/const (e.g. `scripts/const/library.const.mjs`).
|
|
64
|
+
5. Build it once (`node scripts/build-library.mjs <name>` or equivalent) before any sibling library imports it.
|
|
65
|
+
|
|
66
|
+
## Pitfalls
|
|
67
|
+
- Editing a library's `src/` and expecting a sibling library (importing via `@ui/<name>`) to pick up the change without rebuilding — it won't, since imports resolve to `dist/`.
|
|
68
|
+
- Publishing out of dependency order can publish a library whose `peerDependencies` point to a version of another library that isn't published yet.
|
|
69
|
+
- Forgetting to update the `tsconfig.json` path alias when adding a library causes confusing "cannot find module" errors that look like a build problem but are actually a path-mapping problem.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: standalone-component-library
|
|
3
|
+
description: Scaffold and structure a standalone Angular component library (no NgModules) buildable independently with ng-packagr, ready for a multi-library workspace.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Standalone Component Library
|
|
7
|
+
|
|
8
|
+
This skill helps create a new Angular library that follows the standalone-only architecture (no `NgModule`), independently buildable and publishable.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Adding a brand new UI component (e.g. `badge`, `slider`) to a multi-library Angular workspace.
|
|
12
|
+
- Reviewing whether an existing library still relies on `NgModule` and should be converted.
|
|
13
|
+
|
|
14
|
+
## Folder structure
|
|
15
|
+
```
|
|
16
|
+
projects/ui/<component-name>/
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── lib/
|
|
19
|
+
│ │ ├── components/
|
|
20
|
+
│ │ │ └── <component-name>.component.ts
|
|
21
|
+
│ │ │ └── <component-name>.component.html
|
|
22
|
+
│ │ │ └── <component-name>.component.scss
|
|
23
|
+
│ │ ├── directives/
|
|
24
|
+
│ │ ├── services/
|
|
25
|
+
│ │ ├── interfaces/
|
|
26
|
+
│ │ ├── injectors/
|
|
27
|
+
│ │ ├── types/
|
|
28
|
+
│ │ ├── adapters/
|
|
29
|
+
│ │ └── helpers/
|
|
30
|
+
│ └── public-api.ts
|
|
31
|
+
├── ng-package.json
|
|
32
|
+
├── package.json
|
|
33
|
+
└── tsconfig.lib.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## `public-api.ts`
|
|
37
|
+
Only export what consumers are meant to use — component classes, public interfaces/types, directives, and injection tokens. Do not export internal helpers/adapters.
|
|
38
|
+
```typescript
|
|
39
|
+
export * from './lib/components/ui-button.component';
|
|
40
|
+
export * from './lib/types/ui-button-size.type';
|
|
41
|
+
export * from './lib/types/ui-button-type.type';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Component definition (standalone)
|
|
45
|
+
```typescript
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'ui-button',
|
|
48
|
+
standalone: true, // implicit/default in modern Angular, but explicit is fine
|
|
49
|
+
templateUrl: './ui-button.component.html',
|
|
50
|
+
styleUrls: ['./ui-button.component.scss'],
|
|
51
|
+
imports: [NgTemplateOutlet, IconComponent],
|
|
52
|
+
host: {
|
|
53
|
+
'[class]': '"ui-button-size-" + this.size() + " ui-button-type-" + this.type()'
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
export class ButtonComponent { }
|
|
57
|
+
```
|
|
58
|
+
```
|
|
59
|
+
Every dependency the template needs (other components, directives, pipes, `CommonModule` pieces like `NgTemplateOutlet`) must be listed explicitly in `imports`. There is no root `NgModule` providing them implicitly.
|
|
60
|
+
|
|
61
|
+
## `ng-package.json`
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
|
65
|
+
"dest": "../../../dist/ui/button",
|
|
66
|
+
"lib": {
|
|
67
|
+
"entryFile": "src/public-api.ts"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## `angular.json` project entry
|
|
73
|
+
Register the library as its own project with the `@angular/build:ng-packagr` builder so it can be built in isolation:
|
|
74
|
+
```json
|
|
75
|
+
"button": {
|
|
76
|
+
"projectType": "library",
|
|
77
|
+
"root": "projects/ui/button",
|
|
78
|
+
"sourceRoot": "projects/ui/button/src",
|
|
79
|
+
"architect": {
|
|
80
|
+
"build": {
|
|
81
|
+
"builder": "@angular/build:ng-packagr",
|
|
82
|
+
"options": { "project": "projects/ui/button/ng-package.json" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Path aliases for cross-library consumption
|
|
89
|
+
In the workspace `tsconfig.json`, map each library to its **built** output so other libraries/apps import the compiled package, not source:
|
|
90
|
+
```json
|
|
91
|
+
"paths": {
|
|
92
|
+
"@ui/button": ["dist/ui/button"],
|
|
93
|
+
"@ui/core": ["dist/ui/core"]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Checklist for a new component library
|
|
98
|
+
1. Create the folder structure above under `projects/ui/<name>/`.
|
|
99
|
+
2. Write `public-api.ts` exporting only the public surface.
|
|
100
|
+
3. Add `ng-package.json` and `package.json` (with `peerDependencies` on `@angular/core`/`@angular/common` and any `@ui/*` libs it depends on).
|
|
101
|
+
4. Register the project in `angular.json`.
|
|
102
|
+
5. Add the `@ui/<name>` path alias in `tsconfig.json`.
|
|
103
|
+
6. Build with the workspace's library build script (see `angular-monorepo-ng-packagr` skill) before consuming it from another library.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: control-flow-syntax
|
|
3
|
+
description: Use Angular's built-in template control flow (@if, @for, @switch) instead of the *ngIf/*ngFor/*ngSwitch structural directives.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Built-in Control Flow Syntax
|
|
7
|
+
|
|
8
|
+
This skill covers Angular's native `@if`/`@for`/`@switch` block syntax, which replaces the `*ngIf`/`*ngFor`/`*ngSwitch` structural directives.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Writing or reviewing any Angular template (`.html` or inline template).
|
|
12
|
+
- Migrating a component still using `*ngIf`, `*ngFor`, or `*ngSwitch`.
|
|
13
|
+
|
|
14
|
+
## `@if` / `@else if` / `@else`
|
|
15
|
+
```html
|
|
16
|
+
@if (!iconOnly()) {
|
|
17
|
+
<ng-container [ngTemplateOutlet]="labelTpl" />
|
|
18
|
+
} @else {
|
|
19
|
+
<ui-button class="calendar-button" [size]="'small'" [type]="'clear'">
|
|
20
|
+
<ui-icon [icon]="'ui-calendar'" />
|
|
21
|
+
</ui-button>
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
```html
|
|
25
|
+
@if (formatHours() === '24') {
|
|
26
|
+
<span class="format-hours">{{ formatHours() }}h</span>
|
|
27
|
+
} @else if (formatHours() === '12') {
|
|
28
|
+
<div class="format-hours-buttons">
|
|
29
|
+
<button class="format-hours-button">AM</button>
|
|
30
|
+
<button class="format-hours-button">PM</button>
|
|
31
|
+
</div>
|
|
32
|
+
} @else {
|
|
33
|
+
<span>Unknown format</span>
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
Bind the condition result to a local template variable to avoid recomputation, using `as`:
|
|
37
|
+
```html
|
|
38
|
+
@if (itemTmpl(); as tmpl) {
|
|
39
|
+
<ng-container [ngTemplateOutlet]="tmpl.templateRef" />
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## `@for` with mandatory `track`
|
|
44
|
+
```html
|
|
45
|
+
@for (day of weekDays(); track $index) {
|
|
46
|
+
<div class="day">{{ day }}</div>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@for (item of data(); track item.id; let i = $index) {
|
|
50
|
+
<div class="tree-node">{{ item.label }}</div>
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
`track` is **required** (unlike `*ngFor`'s optional `trackBy`). Prefer tracking by a stable unique key (`item.id`) over `$index` whenever the list can reorder/filter, since `$index` tracking causes Angular to treat every reordering as a full replace.
|
|
54
|
+
|
|
55
|
+
Available implicit variables: `$index`, `$first`, `$last`, `$even`, `$odd`, `$count`.
|
|
56
|
+
|
|
57
|
+
`@for` also supports an `@empty` block:
|
|
58
|
+
```html
|
|
59
|
+
@for (item of items(); track item.id) {
|
|
60
|
+
<div>{{ item.label }}</div>
|
|
61
|
+
} @empty {
|
|
62
|
+
<div class="no-items">No items found</div>
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## `@switch` / `@case` / `@default`
|
|
67
|
+
```html
|
|
68
|
+
@switch (currentView()) {
|
|
69
|
+
@case ('month') {
|
|
70
|
+
<gantt-month-view />
|
|
71
|
+
}
|
|
72
|
+
@case ('week') {
|
|
73
|
+
<gantt-week-view />
|
|
74
|
+
}
|
|
75
|
+
@default {
|
|
76
|
+
<gantt-day-view />
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Migration checklist
|
|
82
|
+
| Legacy | Modern |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `*ngIf="cond"` | `@if (cond) { ... }` |
|
|
85
|
+
| `*ngIf="cond; else tpl"` | `@if (cond) { ... } @else { ... }` |
|
|
86
|
+
| `*ngFor="let x of items; trackBy: fn"` | `@for (x of items; track x.id) { ... }` |
|
|
87
|
+
| `*ngSwitch` / `*ngSwitchCase` / `*ngSwitchDefault` | `@switch` / `@case` / `@default` |
|
|
88
|
+
|
|
89
|
+
Since these are compiler-level syntax (not directives), `CommonModule`/`NgIf`/`NgFor`/`NgSwitch` no longer need to be imported just to use them — only import `NgTemplateOutlet`/`NgClass`/etc. for directives you still explicitly use.
|
|
90
|
+
|
|
91
|
+
## Pitfalls
|
|
92
|
+
- Forgetting `track` in `@for` is a compile error — always pick a stable key, not an object reference that changes identity on every request.
|
|
93
|
+
- `@if...as` only binds within that block's scope, not in sibling `@else` blocks.
|
|
94
|
+
- Don't mix `*ngIf`/`*ngFor` and `@if`/`@for` in the same file inconsistently — migrate a whole template at once for readability.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: defer-blocks
|
|
3
|
+
description: Lazy-load template sections with Angular's @defer block, including triggers (viewport, idle, interaction), placeholder, loading, and error states.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `@defer` Blocks
|
|
7
|
+
|
|
8
|
+
This skill covers deferring the loading and rendering of a template section (and the component code it depends on) until a trigger condition is met.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- A heavy/rarely-visible component (charts, rich editors, large tables, secondary tabs) shouldn't be part of the initial bundle.
|
|
12
|
+
- Content only needs to render once it scrolls into the viewport, the browser is idle, or the user interacts with a trigger element.
|
|
13
|
+
- You want an explicit loading/placeholder/error UI for a lazily-loaded section.
|
|
14
|
+
|
|
15
|
+
## Basic syntax
|
|
16
|
+
```html
|
|
17
|
+
@defer {
|
|
18
|
+
<app-gantt-chart [data]="ganttData()" />
|
|
19
|
+
} @placeholder {
|
|
20
|
+
<div class="chart-placeholder">Chart will appear here</div>
|
|
21
|
+
} @loading (minimum 200ms) {
|
|
22
|
+
<app-spinner />
|
|
23
|
+
} @error {
|
|
24
|
+
<div class="chart-error">Failed to load chart</div>
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
- `@placeholder`: shown before the trigger fires (optional, defaults to nothing rendered).
|
|
28
|
+
- `@loading`: shown while the deferred dependencies are being fetched; supports `minimum`/`after` timing hints to avoid flicker.
|
|
29
|
+
- `@error`: shown if loading the deferred block's dependencies fails.
|
|
30
|
+
|
|
31
|
+
## Triggers
|
|
32
|
+
```html
|
|
33
|
+
<!-- Render when the placeholder scrolls into the viewport -->
|
|
34
|
+
@defer (on viewport) {
|
|
35
|
+
<app-heavy-table [data]="rows()" />
|
|
36
|
+
} @placeholder {
|
|
37
|
+
<div class="table-placeholder"></div>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
<!-- Render when the browser is idle -->
|
|
41
|
+
@defer (on idle) {
|
|
42
|
+
<app-recommendations />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
<!-- Render on user interaction with a referenced element -->
|
|
46
|
+
@defer (on interaction(triggerBtn)) {
|
|
47
|
+
<app-details-panel />
|
|
48
|
+
} @placeholder {
|
|
49
|
+
<button #triggerBtn>Show details</button>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
<!-- Render after another condition/signal becomes true -->
|
|
53
|
+
@defer (when isReady()) {
|
|
54
|
+
<app-report />
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
<!-- Combine a timer with another trigger -->
|
|
58
|
+
@defer (on timer(2s)) {
|
|
59
|
+
<app-promo-banner />
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
Other triggers: `on hover(ref)`, `on immediate`.
|
|
63
|
+
|
|
64
|
+
## Prefetching
|
|
65
|
+
Separate the trigger for *rendering* from the trigger for *fetching the code* ahead of time:
|
|
66
|
+
```html
|
|
67
|
+
@defer (on interaction; prefetch on idle) {
|
|
68
|
+
<app-modal-content />
|
|
69
|
+
} @placeholder {
|
|
70
|
+
<button>Open</button>
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
This downloads the deferred chunk during idle time but still waits for the interaction to actually render it.
|
|
74
|
+
|
|
75
|
+
## Guidelines for a component library
|
|
76
|
+
- Reserve `@defer` for genuinely optional/heavy UI (secondary panels, rarely-opened dialogs, charts) — don't wrap every component, since each `@defer` block becomes a separate lazy chunk with its own network round-trip.
|
|
77
|
+
- Always pair `on viewport`/`on interaction` triggers with a meaningful `@placeholder` sized close to the final content to avoid layout shift.
|
|
78
|
+
- `@defer` only affects the components/pipes/directives used **exclusively** inside the block — if a dependency is also used outside the block, it isn't deferred.
|
|
79
|
+
|
|
80
|
+
## Pitfalls
|
|
81
|
+
- Deferred content unmounts and loses state if its trigger condition becomes false again (depending on trigger) — don't rely on it for state that must persist.
|
|
82
|
+
- `on interaction`/`on hover` without an explicit element reference default to the placeholder itself — make sure a placeholder exists in that case.
|
|
83
|
+
- Testing `@defer` blocks requires explicitly flushing/triggering the deferred state in tests (e.g. Angular's `DeferBlockFixture` APIs) — see the `vitest-angular-components` skill.
|