Skip to main content
Angular Component Anatomy

Module Overview

Estimated Time: 4-5 hours | Difficulty: Beginner-Intermediate | Prerequisites: Module 1
Components are the fundamental building blocks of Angular applications. Every Angular app has at least one component—the root component—that connects a component tree to the page DOM. What You’ll Learn:
  • Component anatomy and architecture
  • Data binding (interpolation, property, event, two-way)
  • Component communication with @Input and @Output
  • Content projection with ng-content
  • ViewChild and ContentChild queries
  • Lifecycle hooks

Component Anatomy

Every Angular component consists of three parts:
┌─────────────────────────────────────────────────────────────────┐
│                    Angular Component                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌──────────────────┐                                          │
│   │  TypeScript      │  ← Logic, properties, methods            │
│   │  (.component.ts) │                                          │
│   └──────────────────┘                                          │
│            │                                                     │
│            ▼                                                     │
│   ┌──────────────────┐                                          │
│   │  Template        │  ← HTML structure with Angular syntax    │
│   │  (.component.html)│                                          │
│   └──────────────────┘                                          │
│            │                                                     │
│            ▼                                                     │
│   ┌──────────────────┐                                          │
│   │  Styles          │  ← Scoped CSS/SCSS                       │
│   │  (.component.scss)│                                          │
│   └──────────────────┘                                          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Component Decorator

import { Component } from '@angular/core';

@Component({
  // CSS selector for the component
  selector: 'app-user-card',
  
  // Standalone component (Angular 14+)
  standalone: true,
  
  // Components, directives, pipes this component uses
  imports: [CommonModule, FormsModule],
  
  // Template options (use ONE)
  templateUrl: './user-card.component.html',
  // OR inline:
  // template: `<h1>{{ title }}</h1>`,
  
  // Style options (use ONE)
  styleUrl: './user-card.component.scss',
  // OR inline:
  // styles: [`.card { padding: 1rem; }`],
  
  // Change detection strategy (advanced)
  changeDetection: ChangeDetectionStrategy.OnPush,
  
  // View encapsulation
  encapsulation: ViewEncapsulation.Emulated  // default
})
export class UserCardComponent {
  // Component logic here
}

Data Binding

Angular provides four types of data binding:
┌─────────────────────────────────────────────────────────────────┐
│                    Data Binding Types                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   1. INTERPOLATION  {{ expression }}                            │
│      Component ──────────────────────────► Template              │
│      Outputs data to the view                                   │
│                                                                  │
│   2. PROPERTY BINDING  [property]="expression"                  │
│      Component ──────────────────────────► Template              │
│      Binds to DOM properties                                    │
│                                                                  │
│   3. EVENT BINDING  (event)="handler()"                         │
│      Component ◄────────────────────────── Template              │
│      Responds to user actions                                   │
│                                                                  │
│   4. TWO-WAY BINDING  [(ngModel)]="property"                    │
│      Component ◄─────────────────────────► Template              │
│      Syncs data both ways                                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

1. Interpolation

Display component data in the template:
// user.component.ts
export class UserComponent {
  name = 'John Doe';
  age = 30;
  
  getFullName(): string {
    return `${this.name} (${this.age})`;
  }
}
<!-- user.component.html -->
<h1>Welcome, {{ name }}!</h1>
<p>Age: {{ age }}</p>
<p>{{ getFullName() }}</p>

<!-- Expressions are allowed -->
<p>Next year: {{ age + 1 }}</p>
<p>Uppercase: {{ name.toUpperCase() }}</p>

<!-- Conditional expression -->
<p>{{ age >= 18 ? 'Adult' : 'Minor' }}</p>
Avoid in interpolation:
  • Assignments ({{ name = 'test' }})
  • Multiple statements ({{ a; b }})
  • Increment/decrement ({{ i++ }})
  • new operator
  • Chained expressions

2. Property Binding

Bind to element properties (not HTML attributes):
<!-- Binding to native properties -->
<img [src]="imageUrl" [alt]="imageDescription">
<button [disabled]="isLoading">Submit</button>
<input [value]="username">
<div [hidden]="!isVisible">Content</div>

<!-- Binding to component inputs -->
<app-user-card [user]="currentUser"></app-user-card>

<!-- Binding to directive properties -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">
<div [ngStyle]="{'color': textColor, 'font-size': fontSize}">
Property vs Attribute: Properties are DOM node properties (JavaScript), while attributes are HTML markup attributes. Angular binds to properties!
<!-- Attribute binding (special syntax for attributes) -->
<td [attr.colspan]="colSpan">
<button [attr.aria-label]="label">

3. Event Binding

Respond to user interactions:
<!-- Basic event binding -->
<button (click)="onSave()">Save</button>
<input (input)="onInput($event)">
<form (submit)="onSubmit($event)">

<!-- Keyboard events -->
<input (keyup)="onKeyUp($event)">
<input (keydown.enter)="onEnter()">
<input (keydown.escape)="onEscape()">

<!-- Mouse events -->
<div (mouseenter)="onHover()">
<div (mouseleave)="onLeave()">
<div (dblclick)="onDoubleClick()">
export class FormComponent {
  onSave() {
    console.log('Saving...');
  }
  
  onInput(event: Event) {
    const input = event.target as HTMLInputElement;
    console.log('Input value:', input.value);
  }
  
  onSubmit(event: Event) {
    event.preventDefault();
    // Handle form submission
  }
  
  onKeyUp(event: KeyboardEvent) {
    console.log('Key pressed:', event.key);
  }
}

4. Two-Way Binding

Combine property and event binding:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [FormsModule],  // Required for ngModel
  template: `
    <input [(ngModel)]="searchTerm" placeholder="Search...">
    <p>Searching for: {{ searchTerm }}</p>
  `
})
export class SearchComponent {
  searchTerm = '';
}
How it works: [(ngModel)] is syntactic sugar for:
<input [ngModel]="searchTerm" (ngModelChange)="searchTerm = $event">

Component Communication

@Input - Parent to Child

Pass data from parent to child component:
// child: user-card.component.ts
import { Component, Input } from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class UserCardComponent {
  @Input() user!: User;  // Required input
  @Input() showEmail = true;  // Optional with default
  
  // Transform input
  @Input({ transform: booleanAttribute }) disabled = false;
  
  // Alias
  @Input({ alias: 'userData' }) user!: User;
}
// parent: users.component.ts
@Component({
  selector: 'app-users',
  standalone: true,
  imports: [UserCardComponent],
  template: `
    @for (user of users; track user.id) {
      <app-user-card 
        [user]="user" 
        [showEmail]="true">
      </app-user-card>
    }
  `
})
export class UsersComponent {
  users: User[] = [
    { id: 1, name: 'John', email: '[email protected]' },
    { id: 2, name: 'Jane', email: '[email protected]' }
  ];
}

@Output - Child to Parent

Emit events from child to parent:
// child: user-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <button (click)="onSelect()">Select</button>
      <button (click)="onDelete()">Delete</button>
    </div>
  `
})
export class UserCardComponent {
  @Input() user!: User;
  
  @Output() selected = new EventEmitter<User>();
  @Output() deleted = new EventEmitter<number>();
  
  onSelect() {
    this.selected.emit(this.user);
  }
  
  onDelete() {
    this.deleted.emit(this.user.id);
  }
}
// parent: users.component.ts
@Component({
  selector: 'app-users',
  standalone: true,
  imports: [UserCardComponent],
  template: `
    @for (user of users; track user.id) {
      <app-user-card 
        [user]="user" 
        (selected)="onUserSelected($event)"
        (deleted)="onUserDeleted($event)">
      </app-user-card>
    }
    
    @if (selectedUser) {
      <p>Selected: {{ selectedUser.name }}</p>
    }
  `
})
export class UsersComponent {
  users: User[] = [];
  selectedUser?: User;
  
  onUserSelected(user: User) {
    this.selectedUser = user;
  }
  
  onUserDeleted(userId: number) {
    this.users = this.users.filter(u => u.id !== userId);
  }
}

Signal-Based Inputs (Angular 17+)

Modern Angular supports signal-based inputs and outputs:
import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user().name }}</h3>  <!-- Note: calling as function -->
      <button (click)="select()">Select</button>
    </div>
  `
})
export class UserCardComponent {
  // Required signal input
  user = input.required<User>();
  
  // Optional signal input with default
  showEmail = input(true);
  
  // Signal output
  selected = output<User>();
  
  select() {
    this.selected.emit(this.user());
  }
}

Content Projection

Project content from parent into child component slots:

Basic Projection

// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <ng-content></ng-content>  <!-- Content goes here -->
    </div>
  `,
  styles: [`.card { border: 1px solid #ddd; padding: 1rem; }`]
})
export class CardComponent {}
<!-- Usage -->
<app-card>
  <h2>Card Title</h2>
  <p>This content is projected into the card.</p>
</app-card>

Multi-Slot Projection

// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[card-header]"></ng-content>
      </div>
      <div class="card-body">
        <ng-content></ng-content>  <!-- Default slot -->
      </div>
      <div class="card-footer">
        <ng-content select="[card-footer]"></ng-content>
      </div>
    </div>
  `
})
export class CardComponent {}
<!-- Usage -->
<app-card>
  <div card-header>
    <h2>Card Title</h2>
  </div>
  
  <p>This goes in the body (default slot).</p>
  <p>More body content.</p>
  
  <div card-footer>
    <button>Save</button>
  </div>
</app-card>

ViewChild & ContentChild

Query for elements or components in the view:

ViewChild

Access elements in the component’s own template:
import { 
  Component, 
  ViewChild, 
  ElementRef, 
  AfterViewInit 
} from '@angular/core';

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input #searchInput type="text">
    <button (click)="focusInput()">Focus</button>
    <app-results #resultsComponent></app-results>
  `
})
export class SearchComponent implements AfterViewInit {
  // Query for native element
  @ViewChild('searchInput') 
  searchInput!: ElementRef<HTMLInputElement>;
  
  // Query for component
  @ViewChild(ResultsComponent) 
  resultsComponent!: ResultsComponent;
  
  ngAfterViewInit() {
    // ViewChild is available here
    console.log(this.searchInput.nativeElement);
  }
  
  focusInput() {
    this.searchInput.nativeElement.focus();
  }
}

ContentChild

Access projected content:
@Component({
  selector: 'app-collapsible',
  standalone: true,
  template: `
    <button (click)="toggle()">Toggle</button>
    <div [hidden]="collapsed">
      <ng-content></ng-content>
    </div>
  `
})
export class CollapsibleComponent implements AfterContentInit {
  collapsed = false;
  
  @ContentChild('headerContent') 
  headerContent?: ElementRef;
  
  ngAfterContentInit() {
    // ContentChild is available here
    if (this.headerContent) {
      console.log('Header content found');
    }
  }
  
  toggle() {
    this.collapsed = !this.collapsed;
  }
}

Lifecycle Hooks

Angular Lifecycle Hooks

Complete Lifecycle

import {
  Component,
  OnInit,
  OnChanges,
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy,
  Input,
  SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-lifecycle',
  standalone: true,
  template: `<p>Lifecycle Demo</p>`
})
export class LifecycleComponent implements 
  OnInit, 
  OnChanges, 
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy {
  
  @Input() data = '';
  
  constructor() {
    console.log('1. Constructor - DI happens here');
  }
  
  ngOnChanges(changes: SimpleChanges) {
    console.log('2. ngOnChanges - Input changed', changes);
    // Access previous and current values
    if (changes['data']) {
      console.log('Previous:', changes['data'].previousValue);
      console.log('Current:', changes['data'].currentValue);
      console.log('First change:', changes['data'].firstChange);
    }
  }
  
  ngOnInit() {
    console.log('3. ngOnInit - Initialize data, fetch from API');
    // @Input values are available here
    // Good place to set up subscriptions
  }
  
  ngDoCheck() {
    console.log('4. ngDoCheck - Custom change detection');
    // Called on every change detection run
    // Use sparingly - performance intensive
  }
  
  ngAfterContentInit() {
    console.log('5. ngAfterContentInit - Projected content initialized');
    // @ContentChild available here
  }
  
  ngAfterContentChecked() {
    console.log('6. ngAfterContentChecked - Projected content checked');
  }
  
  ngAfterViewInit() {
    console.log('7. ngAfterViewInit - View initialized');
    // @ViewChild available here
    // Good place to interact with DOM
  }
  
  ngAfterViewChecked() {
    console.log('8. ngAfterViewChecked - View checked');
  }
  
  ngOnDestroy() {
    console.log('9. ngOnDestroy - Cleanup!');
    // Unsubscribe from observables
    // Clear timers/intervals
    // Disconnect from WebSocket
  }
}

Common Patterns

@Component({
  selector: 'app-data-fetcher',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (loading) {
      <p>Loading...</p>
    }
    @if (data$ | async; as data) {
      <pre>{{ data | json }}</pre>
    }
  `
})
export class DataFetcherComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  
  data$!: Observable<any>;
  loading = true;
  
  private dataService = inject(DataService);
  
  ngOnInit() {
    // Fetch data on init
    this.data$ = this.dataService.getData().pipe(
      tap(() => this.loading = false),
      takeUntil(this.destroy$)  // Cleanup on destroy
    );
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Best Practices

Smart vs Dumb Components

  • Smart: Handle logic, fetch data, contain business logic
  • Dumb: Receive data via @Input, emit events via @Output, purely presentational

Single Responsibility

Each component should do one thing well. If it’s getting complex, split it!

Use OnPush

Use ChangeDetectionStrategy.OnPush for better performance with immutable data

Cleanup Subscriptions

Always unsubscribe in ngOnDestroy or use takeUntil pattern

Practice Exercise

Exercise: Build a Todo List Component

Create a todo list with:
  1. Parent component managing the list
  2. Child component for individual todo items
  3. Add new todo functionality
  4. Mark as complete
  5. Delete todo
Use @Input and @Output for communication.
// todo-item.component.ts
@Component({
  selector: 'app-todo-item',
  standalone: true,
  template: `
    <div class="todo-item" [class.completed]="todo.completed">
      <input 
        type="checkbox" 
        [checked]="todo.completed"
        (change)="onToggle()">
      <span>{{ todo.text }}</span>
      <button (click)="onDelete()">×</button>
    </div>
  `
})
export class TodoItemComponent {
  @Input() todo!: { id: number; text: string; completed: boolean };
  @Output() toggle = new EventEmitter<number>();
  @Output() delete = new EventEmitter<number>();
  
  onToggle() { this.toggle.emit(this.todo.id); }
  onDelete() { this.delete.emit(this.todo.id); }
}

// todo-list.component.ts
@Component({
  selector: 'app-todo-list',
  standalone: true,
  imports: [TodoItemComponent, FormsModule],
  template: `
    <div class="todo-list">
      <div class="add-todo">
        <input [(ngModel)]="newTodoText" placeholder="Add todo...">
        <button (click)="addTodo()">Add</button>
      </div>
      
      @for (todo of todos; track todo.id) {
        <app-todo-item 
          [todo]="todo"
          (toggle)="toggleTodo($event)"
          (delete)="deleteTodo($event)">
        </app-todo-item>
      }
    </div>
  `
})
export class TodoListComponent {
  newTodoText = '';
  todos = [
    { id: 1, text: 'Learn Angular', completed: false },
    { id: 2, text: 'Build an app', completed: false }
  ];
  
  addTodo() {
    if (this.newTodoText.trim()) {
      this.todos.push({
        id: Date.now(),
        text: this.newTodoText,
        completed: false
      });
      this.newTodoText = '';
    }
  }
  
  toggleTodo(id: number) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.completed = !todo.completed;
  }
  
  deleteTodo(id: number) {
    this.todos = this.todos.filter(t => t.id !== id);
  }
}

Summary

In this module, you learned:
1

Component Structure

Components consist of TypeScript class, HTML template, and CSS styles
2

Data Binding

Four types: interpolation, property, event, and two-way binding
3

Component Communication

@Input for parent-to-child, @Output for child-to-parent
4

Content Projection

Using ng-content for flexible component composition
5

Lifecycle Hooks

Managing component initialization, changes, and cleanup

Next Steps

Next: Directives & Pipes

Learn about structural directives, attribute directives, and pipes for data transformation