| name | angular-component |
| description | Angular 17+ component patterns with signal-based I/O, OnPush change detection, native control flow (@if/@for/@switch), and inject() function. Use when creating, modifying, or reviewing Angular components. ALWAYS use when writing .component.ts files, editing @Component decorators, or configuring component imports. |
Angular Component Patterns
Component Declaration
Components are standalone by default in Angular 17+. Do NOT set standalone: true.
@Component({
selector: 'app-entity-list',
imports: [CommonModule, RouterModule],
templateUrl: './entity-list.component.html',
styleUrl: './entity-list.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EntityListComponent {
private readonly service = inject(EntityService);
private readonly router = inject(Router);
readonly entityId = input.required<string>();
readonly status = input<string>('active');
readonly selected = output<Entity>();
readonly items = signal<Entity[]>([]);
readonly loading = signal(false);
readonly filteredItems = computed(() =>
this.items().filter(i => this.status() === 'all' || i.status === this.status())
);
}
Signal Inputs (not @Input)
readonly entityId = input.required<number>();
readonly pageSize = input(20);
readonly id = input(0, { transform: numberAttribute });
readonly label = input('', { alias: 'buttonLabel' });
Signal Outputs (not @Output)
readonly selected = output<Entity>();
readonly closed = output<void>();
this.selected.emit(entity);
this.closed.emit();
View Queries (not @ViewChild)
readonly nameInput = viewChild<ElementRef<HTMLInputElement>>('nameInput');
readonly items = viewChildren(ItemComponent);
this.nameInput()?.nativeElement.focus();
Control Flow (not *ngIf/*ngFor)
@if (loading()) {
<app-spinner />
} @else if (error()) {
<app-error [message]="error()" />
} @else {
@for (item of items(); track item.id) {
<app-item [data]="item" (click)="onSelect(item)" />
} @empty {
<p>No items found</p>
}
}
@switch (status()) {
@case ('active') { <span class="badge-active">Active</span> }
@case ('inactive') { <span class="badge-inactive">Inactive</span> }
@default { <span>Unknown</span> }
}
inject() Function (not constructor injection)
private readonly http = inject(HttpClient);
private readonly destroyRef = inject(DestroyRef);
private readonly route = inject(ActivatedRoute);
private readonly modalControl = inject(ModalControl, { optional: true });
Host Metadata
@Component({
host: {
class: 'page-content',
'[class.is-loading]': 'loading()',
'(window:resize)': 'onResize($event)',
},
})
Import Order
@angular/*
- Third-party libraries (rxjs, etc.)
- Project aliases (
@app/*)
- Relative imports
Key Rules
- Always
ChangeDetectionStrategy.OnPush
- Always
inject(), never constructor injection
- Signal inputs:
input() / input.required(), not @Input()
- Signal outputs:
output(), not @Output() / EventEmitter
- Native control flow:
@if/@for/@switch, not *ngIf/*ngFor
- Separate template and style files (except very simple components)
For page component patterns, see references/component-patterns.md.