Angular Components Skill
Create or modify Angular UI components in packages/shared/angular/ following project conventions.
Usage
/angular-components [component-name] [description]
Parameters
component-name - kebab-case name (e.g., hero-section, feature-section, pricing-table)
description - brief description of the component and its variants
Component Architecture (3-Layer Pattern)
Components follow a 2-layer pattern (Base + Wrapper). Add the optional Standard layer only when multiple visual variants are needed.
packages/shared/angular/src/lib/components/<component-name>/
├── index.ts # Barrel exports
├── <component-name>.component.ts # Wrapper (CreateDynamicComponent)
├── <component-name>.component.spec.ts
├── <component-name>.component.stories.ts
├── base/
│ ├── index.ts
│ ├── base.component.ts # @Directive() with abstract logic + signals
│ └── base.component.spec.ts
└── standard/ # OPTIONAL: only for multi-variant components
├── index.ts
├── standard.component.ts # Concrete implementation
├── standard.component.html # Template with @if/@for/@switch
└── standard.component.spec.ts
Layer 1: Base Component (base/base.component.ts)
Abstract directive with shared logic and signal-based state.
typescript
1import { Directive, input, InputSignal, signal, viewChild, ViewContainerRef, WritableSignal } from '@angular/core';
2import { DynamicComponentType, I<ComponentName>Options } from '../../../models';
3
4@Directive()
5export abstract class <ComponentName>BaseComponent {
6 static smartType: DynamicComponentType = '<component-name>';
7
8 options: InputSignal<I<ComponentName>Options> = input.required<I<ComponentName>Options>();
9 contentTpl = viewChild<ViewContainerRef>('contentTpl');
10
11 // Component-specific logic here
12}
Layer 2 (optional): Standard Component (standard/standard.component.ts)
Only needed when the component has multiple visual variants (e.g., standard form vs grouped form). If the component has a single variant, keep all logic and template in Base + Wrapper. Every component can be dynamically swapped via ng-template and CreateDynamicComponent.
typescript
1import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
2import { <ComponentName>BaseComponent } from '../base/base.component';
3
4@Component({
5 selector: 'lib-<component-name>-standard',
6 templateUrl: './standard.component.html',
7 encapsulation: ViewEncapsulation.None,
8 changeDetection: ChangeDetectionStrategy.OnPush,
9})
10export class <ComponentName>StandardComponent extends <ComponentName>BaseComponent {}
Layer 3: Wrapper Component (<component-name>.component.ts)
Dynamic component wrapper using CreateDynamicComponent.
typescript
1import { NgTemplateOutlet } from '@angular/common';
2import { ChangeDetectionStrategy, Component, effect, input, viewChild, viewChildren, ViewContainerRef, ViewEncapsulation } from '@angular/core';
3import { DynamicContentDirective } from '../../directives';
4import { I<ComponentName>Options } from '../../models';
5import { CreateDynamicComponent } from '../base';
6import { <ComponentName>BaseComponent } from './base/base.component';
7import { <ComponentName>StandardComponent } from './standard/standard.component';
8
9@Component({
10 selector: 'lib-<component-name>',
11 template: `
12 @if (template() === 'default') {
13 <lib-<component-name>-standard [options]="options()">
14 <ng-container [ngTemplateOutlet]="contentTpl"></ng-container>
15 </lib-<component-name>-standard>
16 }
17 <ng-template #contentTpl>
18 <ng-content></ng-content>
19 </ng-template>
20 <div class="dynamic-content"></div>
21 `,
22 encapsulation: ViewEncapsulation.None,
23 imports: [<ComponentName>StandardComponent, NgTemplateOutlet],
24 changeDetection: ChangeDetectionStrategy.OnPush,
25})
26export class <ComponentName>Component extends CreateDynamicComponent<<ComponentName>BaseComponent>('<component-name>') {
27 options = input.required<I<ComponentName>Options>();
28
29 override contentTpl = viewChild<ViewContainerRef>('contentTpl');
30 override dynamicContents = viewChildren<DynamicContentDirective>(DynamicContentDirective);
31
32 constructor() {
33 super();
34 effect(() => {
35 this.options();
36 this.refreshDynamicInstance();
37 });
38 }
39
40 override refreshProperties(): void {
41 this.baseInstance.options = this.options;
42 }
43}
Every component MUST accept an external class attribute that gets merged onto the main DOM element. Use input with alias: 'class':
typescript
1// In base component:
2cssClass: InputSignal<string> = input<string>('', { alias: 'class' });
3
4// In wrapper component:
5cssClass = input<string>('', { alias: 'class' });
6// Pass down to child: [cssClass]="cssClass()"
7// In refreshProperties: this.baseInstance.cssClass = this.cssClass;
In the shape/variant component's buttonClasses (or equivalent) computed, append the external class:
typescript
1buttonClasses = computed(() => {
2 const classes = [...this.variantClasses()];
3 // ... add size/shape classes ...
4 const extra = this.cssClass();
5 if (extra) classes.push(extra);
6 return classes.join(' ');
7});
Usage: <smart-button class="smart:mt-4 custom-class" [options]="opts">Click</smart-button>
Interface Pattern
Add the component options interface to packages/shared/angular/src/lib/models/interfaces.ts:
typescript
1export enum <ComponentName>Variant {
2 // Define variants based on HTML templates
3}
4
5export interface I<ComponentName>Options {
6 variant: <ComponentName>Variant;
7 // Component-specific properties
8}
Also add the DynamicComponentType union member: '<component-name>'.
MANDATORY: Plugin Sync
Every time you create or modify a component, you MUST update the plugin smart:
- Per-component skill —
packages/shared/claude-plugins/src/plugins/smart/skills/angular-components-<name>/SKILL.md (consumer-facing API docs)
- Agent —
packages/shared/claude-plugins/src/plugins/smart/agents/angular-components/AGENT.md (add/update "Available Components" table and "Skills to Use" list)
These files are distributed with @smartsoft001/angular and used by end-user projects to consume the components.
Execution Checklist
Execute each step in order. Use shared-tdd-developer agent for all code implementation (RED → GREEN → REFACTOR).
Styling Rules
- Tailwind CSS v4 with
smart: prefix for all utility classes (e.g., smart:bg-white, smart:text-gray-900, smart:mt-4)
- Dark mode: use
dark:smart: prefix (e.g., dark:smart:bg-gray-900, dark:smart:text-white)
- Light mode: default prefixed classes (e.g.,
smart:bg-white, smart:text-gray-900)
- ViewEncapsulation.None on all styled components
- No inline styles — use Tailwind utility classes only
Angular Patterns (Mandatory)
ChangeDetectionStrategy.OnPush on all components
input() / input.required() for inputs (no @Input())
signal() for internal state
computed() for derived values
effect() for side effects
@if / @for / @switch control flow (no *ngIf / *ngFor)
inject() for DI (no constructor injection)
- No explicit
standalone: true (Angular 19+ default)
track on all @for loops
Testing Requirements
- Jest with AAA pattern (Arrange-Act-Assert)
- Test file naming:
<name>.component.spec.ts alongside source
- Describe block:
@smartsoft001/angular: <ClassName>
- Test each variant renders correctly
- Test signal inputs/outputs
- Test user interactions (click handlers, etc.)
- Test dark/light mode class application
Storybook Requirements
Every component MUST have exactly 2 stories: Playground and AllVariants.
Critical configuration rules
- Use sub-components directly (e.g.,
ButtonStandardComponent) — NOT the wrapper component (ButtonComponent) which extends CreateDynamicComponent. The wrapper uses toObservable from @angular/core/rxjs-interop which is not compatible with Storybook webpack.
- Provide
TranslateModule.forRoot() via applicationConfig (not moduleMetadata) — using moduleMetadata with ModuleWithProviders causes ngModule errors on navigation between stories.
- Import sub-components via
moduleMetadata — standalone components go in moduleMetadata({ imports: [...] }).
- AllVariants must disable all Controls — use
argTypes: { propName: { table: { disable: true } } } for each arg.
typescript
1import { importProvidersFrom, signal, WritableSignal } from '@angular/core';
2import { TranslateModule } from '@ngx-translate/core';
3import type { Meta, StoryObj } from '@storybook/angular';
4import { applicationConfig, moduleMetadata } from '@storybook/angular';
5
6const meta: Meta = {
7 title: 'Components/<ComponentName>',
8 tags: ['autodocs'],
9 decorators: [
10 applicationConfig({
11 providers: [importProvidersFrom(TranslateModule.forRoot())],
12 }),
13 moduleMetadata({
14 imports: [
15 // Import sub-components directly (NOT the wrapper)
16 <ComponentName>StandardComponent,
17 <ComponentName>RoundedComponent,
18 // ... other sub-components
19 ],
20 }),
21 ],
22 argTypes: {
23 variant: {
24 control: 'select',
25 options: ['primary', 'secondary', 'soft'],
26 description: '...',
27 },
28 size: {
29 control: 'select',
30 options: ['xs', 'sm', 'md', 'lg', 'xl'],
31 description: '...',
32 },
33 disabled: { control: 'boolean', description: '...' },
34 // ... all configurable properties as Controls
35 },
36 args: {
37 variant: 'primary',
38 size: 'md',
39 disabled: false,
40 // ... default values
41 },
42};
Story 1: Playground
- Interactive story — all options configurable via Controls tab
render function builds the options object from args
- Uses sub-component selectors in template (e.g.,
<smart-button-standard>)
typescript
1export const Playground: Story = {
2 name: 'Playground',
3 render: (args: any) => {
4 const options = { click: () => {}, variant: args.variant, size: args.size, ... };
5 return {
6 props: { options, isDisabled: args.disabled, label: args.label },
7 template: `<smart-<component>-standard [options]="options" [disabled]="isDisabled">{{ label }}</smart-<component>-standard>`,
8 };
9 },
10};
Story 2: AllVariants
- Static showcase of ALL combinations in one HTML template
- All Controls disabled via
argTypes: { propName: { table: { disable: true } } }
- Organized into
<section> blocks with <h3> headings
- Sections: each shape × each variant, each shape × all sizes, icons, states
- Layout:
display: flex; flex-direction: column; gap: 32px for sections, display: flex; align-items: center; gap: 12px for items
typescript
1export const AllVariants: Story = {
2 name: 'All Variants',
3 argTypes: {
4 variant: { table: { disable: true } },
5 size: { table: { disable: true } },
6 // ... disable ALL args
7 },
8 render: () => ({
9 props: {
10 /* all option objects as separate props */
11 },
12 template: `
13 <div style="display: flex; flex-direction: column; gap: 32px;">
14 <section>
15 <h3 style="margin-bottom: 12px; font-weight: 600;">Standard</h3>
16 <div style="display: flex; align-items: center; gap: 12px;">
17 <smart-<component>-standard [options]="primary">Primary</smart-<component>-standard>
18 <smart-<component>-standard [options]="secondary">Secondary</smart-<component>-standard>
19 <smart-<component>-standard [options]="soft">Soft</smart-<component>-standard>
20 </div>
21 </section>
22 <!-- ... more sections: sizes, icons, states ... -->
23 </div>
24 `,
25 }),
26};
General rules
- File:
<component-name>.component.stories.ts
- All Tailwind classes with
smart: prefix in templates (Tailwind v4 syntax)
- Reference:
button/button.component.stories.ts as the canonical example
Agent Delegation
| Step | Agent |
|---|
| Code implementation (TDD) | shared-tdd-developer |
| Unit tests | angular-jest-test-writer |
| Style/lint check | shared-style-enforcer |
| Build verification | shared-build-verifier |
Reference Components
Study these existing components as patterns:
button/ — simple component with options interface