| name | component-patterns |
| description | Use when creating, modifying, or refactoring Angular components. Triggers on "create component", "add input", "add output", "signal", "computed", "host binding", "content projection", "Spartan UI", or component structure questions. |
| allowed-tools | ["Read","Edit","Write","Glob","Grep"] |
Component Structure & Patterns
Quick Reference
Essential Rules
1. Use Separate Template Files (CRITICAL)
@Component({
selector: 'ai-message-bubble',
templateUrl: './message-bubble.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessageBubbleComponent {}
@Component({
selector: 'ai-message-bubble',
template: `<div>...</div>`,
})
2. Prefer Spartan UI Components
Always check if a Spartan UI component exists before building custom. Import from @angular-ai-kit/spartan-ui/*.
3. Standard Component Structure
import { cn } from '@angular-ai-kit/utils';
import {
ChangeDetectionStrategy,
Component,
ViewEncapsulation,
computed,
inject,
input,
output,
signal,
} from '@angular/core';
@Component({
selector: 'ai-component-name',
templateUrl: './component-name.component.html',
imports: [
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'[class]': 'hostClasses()',
'[attr.aria-label]': 'ariaLabel()',
'(click)': 'handleClick()',
},
})
export class ComponentName {
private service = inject(SomeService);
prop = input.required<Type>();
customClasses = input<string>('');
event = output<Type>();
containerClasses = computed(() =>
cn('base', { disabled: this.disabled() }, this.customClasses())
);
state = signal<Type>(initialValue);
handleClick() {
this.event.emit();
}
}
Component Organization Order
- Imports - Angular core, third-party, local
- Component decorator - Metadata
- Injected services - Using
inject()
- Inputs - Required first, then optional
- Outputs - Event emitters
- Computed signals - Derived state
- Regular signals - Local state
- Constructor - Only for effects
- Lifecycle methods - If needed
- Public methods - Component API
- Private methods - Internal helpers
Input Patterns
Required vs Optional
message = input.required<ChatMessage>();
showAvatar = input(false);
placeholder = input('Type a message...');
Input Transforms
disabled = input(false, {
transform: (value: boolean | string) => value === '' || value === true,
});
maxLength = input(1000, {
transform: (value: string | number) =>
typeof value === 'string' ? parseInt(value, 10) : value,
});
Custom Classes Input
customClasses = input<string>('');
containerClasses = computed(() => cn('base-classes', this.customClasses()));
Output Patterns
click = output<void>();
messageSubmit = output<string>();
handleSubmit() {
this.messageSubmit.emit(this.message());
}
Computed Signals
containerClasses = computed(() =>
cn(
'flex items-center gap-2 p-4',
{
'bg-primary': this.variant() === 'primary',
'opacity-50': this.disabled(),
},
this.customClasses()
)
);
displayText = computed(() => {
const text = this.text();
return text.length > 100 ? text.slice(0, 100) + '...' : text;
});
Host Bindings
@Component({
host: {
'[class]': 'hostClasses()',
'[class.disabled]': 'disabled()',
'[attr.role]': '"button"',
'[attr.aria-disabled]': 'disabled()',
'[attr.tabindex]': 'disabled() ? -1 : 0',
'(click)': 'handleClick()',
'(keydown.enter)': 'handleEnter()',
},
})
Template Control Flow
@if (loading()) {
<ai-spinner />
} @else {
<ai-content />
}
@for (item of items(); track item.id) {
<ai-item [data]="item" />
} @empty {
<p>No items</p>
}
@switch (role()) { @case ('user') { <ai-user-avatar /> } @case ('assistant') {
<ai-bot-avatar /> } }
Class Bindings
<div [class]="containerClasses()">Content</div>
<div [class.active]="isActive()" [class.disabled]="disabled()">
<div [ngClass]="{'active': isActive()}">Bad</div>
</div>
Additional Resources
- spartan-ui.md - Complete Spartan UI component reference
- examples.md - Input transforms, output patterns, signal examples
- patterns.md - Container/presentational, content projection, advanced patterns