Components
Components are the fundamental building blocks of every Angular application. A component controls a portion of the screen — called a view — and consists of:
- A TypeScript class that holds the component's data and logic
- An HTML template that defines what the user sees
- An optional CSS stylesheet that styles the component's view
- Metadata provided through the
@Componentdecorator
Every Angular app has at least one component — the root component (AppComponent). All other components are nested beneath it in a tree structure.
Anatomy of a Component
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card', // (1) The HTML tag: <app-user-card>
standalone: true, // (2) No NgModule needed
imports: [CommonModule], // (3) Other components/pipes/directives used in template
templateUrl: './user-card.component.html', // (4) External template file
styleUrl: './user-card.component.css', // (5) External style file
// OR inline:
// template: `<h2>{{ user.name }}</h2>`,
// styles: [`h2 { color: #333; }`],
})
export class UserCardComponent {
@Input({ required: true }) user!: { name: string; email: string };
@Output() selected = new EventEmitter<string>();
selectUser() {
this.selected.emit(this.user.email);
}
}The numbered comments map to these @Component metadata properties:
- selector — defines the custom HTML tag for this component
- standalone —
truemeans no NgModule is needed (Angular 17+ default) - imports — other standalone components, directives, and pipes used in the template
- templateUrl — path to an external HTML file (use
templatefor inline) - styleUrl — path to an external CSS file (use
stylesfor inline;styleUrlsfor multiple)
Generating a Component with the CLI
# Full command ng generate component features/product-card --standalone # Shorthand ng g c features/product-card # Inline template and styles (no separate files) ng g c features/product-card --inline-template --inline-style # Skip test file ng g c features/product-card --skip-tests
This creates four files:
product-card.component.ts— the component classproduct-card.component.html— the templateproduct-card.component.css— the stylesproduct-card.component.spec.ts— the unit test
The @Component Decorator — All Options
Property | Type | Description |
|---|---|---|
selector | string | Custom HTML element name (e.g., app-header) |
standalone | boolean | If true, no NgModule required |
template | string | Inline HTML template |
templateUrl | string | Path to external HTML file |
styles | string[] | Inline CSS styles (array of strings) |
styleUrl | string | Path to single external stylesheet (Angular 17+) |
styleUrls | string[] | Paths to multiple external stylesheets |
imports | any[] | Components, directives, pipes used in the template |
providers | any[] | Services provided at component scope |
changeDetection | ChangeDetectionStrategy | Default or OnPush |
encapsulation | ViewEncapsulation | Emulated, None, or ShadowDom |
host | object | Host element bindings and listeners |
animations | any[] | Component-level animation triggers |
Component Inputs — Receiving Data from a Parent
Use @Input() to pass data from a parent component down to a child. This is the primary mechanism for parent → child communication.
// child: badge.component.ts
import { Component, Input } from '@angular/core';
type BadgeColor = 'red' | 'green' | 'blue' | 'gray';
@Component({
selector: 'app-badge',
standalone: true,
template: `
<span [class]="'badge badge-' + color">{{ label }}</span>
`,
styles: [`
.badge { padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 0.75rem; }
.badge-red { background: #fee2e2; color: #dc2626; }
.badge-green { background: #dcfce7; color: #16a34a; }
.badge-blue { background: #dbeafe; color: #2563eb; }
.badge-gray { background: #f3f4f6; color: #374151; }
`],
})
export class BadgeComponent {
@Input({ required: true }) label!: string; // required — Angular will error if not passed
@Input() color: BadgeColor = 'gray'; // optional with default value
// Input with a transform function (Angular 16+)
@Input({ transform: (v: string) => v.trim().toUpperCase() })
code = '';
}<!-- parent template — using the Badge component --> <app-badge label="New" color="green" /> <app-badge label="Sale" color="red" /> <app-badge [label]="product.status" [color]="statusColor" />
Component Outputs — Emitting Events to a Parent
Use @Output() with EventEmitter to send data or notifications from a child component up to its parent. This is child → parent communication.
// child: like-button.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-like-button',
standalone: true,
template: `
<button (click)="onLike()" [class.liked]="liked">
{{ liked ? '❤️' : '🤍' }} {{ count }}
</button>
`,
})
export class LikeButtonComponent {
@Input() count = 0;
@Input() liked = false;
// EventEmitter<T> — T is the type of the emitted value
@Output() likeToggled = new EventEmitter<{ liked: boolean; count: number }>();
onLike() {
const newLiked = !this.liked;
const newCount = newLiked ? this.count + 1 : this.count - 1;
this.likeToggled.emit({ liked: newLiked, count: newCount });
}
}<!-- parent template --> <app-like-button [count]="post.likes" [liked]="post.isLiked" (likeToggled)="onLikeToggled(post.id, $event)" />
// parent component class
onLikeToggled(postId: number, event: { liked: boolean; count: number }) {
this.postService.updateLike(postId, event.liked);
}\$event in event binding refers to the value emitted by the EventEmitter. Its type is automatically inferred from EventEmitter<T>.Inline vs External Templates and Styles
Approach | When to Use | Pros | Cons |
|---|---|---|---|
Inline template | Small, simple components | Everything in one file | No HTML syntax highlighting in some editors |
External template | Complex templates (>10 lines) | Full editor support, better formatting | Separate file to maintain |
Inline styles | Minimal styling | Convenient for tiny tweaks | No SCSS support |
External styles | Any real styling | SCSS, full tooling, easy to maintain | Separate file |
// Inline — good for simple components
@Component({
selector: 'app-spinner',
standalone: true,
template: `<div class="spinner"></div>`,
styles: [`.spinner { width: 40px; height: 40px; border: 4px solid #ccc; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } }`],
})
export class SpinnerComponent {}
// External files — good for real components
@Component({
selector: 'app-dashboard',
standalone: true,
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss',
})
export class DashboardComponent {}Component Interaction Patterns
Angular components communicate in these patterns:
Pattern | Mechanism | Use Case |
|---|---|---|
Parent → Child | @Input() property binding | Pass configuration or data into a child |
Child → Parent | @Output() EventEmitter | Child notifies parent of user actions |
Parent → Child (template var) | @ViewChild() / template ref | Parent calls child methods directly |
Sibling communication | Shared service | Two unrelated components share state |
Any → Any (signals) | Signal in a service | App-wide reactive state |
Any → Any (RxJS) | BehaviorSubject in a service | Complex async state |
Two-Way Binding with Inputs and Outputs
When an @Input and @Output follow the naming pattern valueName / valueNameChange, Angular supports the banana-in-a-box [(banana)] two-way binding syntax as shorthand:
@Component({
selector: 'app-quantity-picker',
standalone: true,
template: `
<button (click)="decrement()">-</button>
<span>{{ value }}</span>
<button (click)="increment()">+</button>
`,
})
export class QuantityPickerComponent {
@Input() value = 1;
@Output() valueChange = new EventEmitter<number>(); // must be "valueChange"
increment() { this.valueChange.emit(this.value + 1); }
decrement() { if (this.value > 1) this.valueChange.emit(this.value - 1); }
}<!-- Both are equivalent: --> <app-quantity-picker [value]="quantity" (valueChange)="quantity = $event" /> <app-quantity-picker [(value)]="quantity" />
Host Element Bindings
You can bind classes, attributes, and event listeners to the host element (the <app-my-component> tag itself) using the host metadata property:
@Component({
selector: 'app-card',
standalone: true,
template: `<ng-content />`,
host: {
class: 'card', // always add this CSS class
'[class.card--loading]': 'isLoading', // conditional class
'[attr.aria-busy]': 'isLoading', // attribute binding
'(click)': 'onHostClick()', // host event
},
})
export class CardComponent {
@Input() isLoading = false;
onHostClick() {
console.log('Card clicked');
}
}@HostBinding and @HostListener decorators. The host object approach is preferred in modern Angular as it keeps all metadata in the decorator.Standalone Component Imports
Standalone components must declare every dependency in their imports array. This replaces the NgModule's imports:
import { Component } from '@angular/core';
import { CommonModule, NgClass, NgIf, DatePipe, CurrencyPipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { ProductCardComponent } from './product-card/product-card.component';
import { SpinnerComponent } from '../shared/spinner/spinner.component';
@Component({
selector: 'app-shop',
standalone: true,
imports: [
// You can import individual directives/pipes (tree-shakable)
NgClass,
NgIf,
DatePipe,
CurrencyPipe,
RouterLink,
FormsModule,
// Or the whole CommonModule (includes all common directives/pipes)
// CommonModule,
// Custom components
ProductCardComponent,
SpinnerComponent,
],
templateUrl: './shop.component.html',
})
export class ShopComponent {}NgIf, NgFor, DatePipe) over the entire CommonModule for better tree-shaking and smaller bundle sizes.Component Best Practices
Keep components small and focused — each component should do one thing well
Prefer smart/dumb (container/presentational) component pattern for complex UIs
Use @Input() / @Output() for component communication, not direct parent references
Mark @Input() as required: true if the component cannot work without that value
Use OnPush change detection for performance-sensitive lists and heavy UIs
Never access the DOM directly — use Angular bindings, @ViewChild, or Renderer2
Put business logic in services, not components — components handle only UI concerns
Unsubscribe from Observables in ngOnDestroy (or use the async pipe / takeUntilDestroyed)
document.querySelector() in Angular components. This breaks server-side rendering and bypasses Angular's change detection. Use template bindings, @ViewChild, or Renderer2 instead.