Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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. If you think of a web page as a LEGO structure, each component is an individual LEGO brick: self-contained, reusable, and snapped together to form the complete UI. A button is a component. A navigation bar is a component. An entire page is a component made of smaller components. 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. These are the bridges between your TypeScript logic and what users see and interact with on screen. Understanding them is essential — you will use at least one in every single template you write.
┌─────────────────────────────────────────────────────────────────┐
│                    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 — this trips up almost everyone: An HTML attribute is what you write in your markup (<input value="hello">). A DOM property is the live JavaScript value on the element object (inputEl.value). They start the same but diverge immediately — if the user types in the input, the property changes but the attribute stays as "hello". Angular binds to properties, not attributes. This is why [value] works but you need [attr.colspan] for attributes that have no corresponding DOM property.
<!-- Attribute binding (special syntax for HTML attributes with no DOM property) -->
<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: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' }
  ];
}

@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. This is the recommended approach for new code — it integrates with Angular’s signal-based reactivity system, giving you better performance and a cleaner mental model. The key difference: signal inputs are read by calling them as functions (e.g., user() instead of user), and they are automatically tracked by Angular’s change detection.
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. Content projection is Angular’s version of React’s children prop or Vue’s <slot>. It lets you build “wrapper” components (cards, modals, tabs) where the parent decides what goes inside, and the child decides where and how to display it.

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

Interview Deep-Dive

Strong Answer: This distinction trips up almost everyone. An HTML attribute is what you write in markup — it is the initial value. A DOM property is the live JavaScript value on the element object. They start the same but immediately diverge. For example, if you write <input value=“hello”> and the user types “world,” the attribute is still “hello” but the property is “world.” Angular binds to DOM properties by default because they reflect the current state.The catch: some HTML attributes have no corresponding DOM property. colspan, rowspan, aria-label, data-* attributes, and SVG attributes fall into this category. When you write [colspan]=“2”, Angular looks for a DOM property called colspan — and it does not exist on the element. So you get a runtime error. You need [attr.colspan]=“2” to tell Angular “set the HTML attribute, not a DOM property.”In practice, I use property binding 95% of the time. I reach for attr. only when dealing with table attributes (colspan, rowspan), ARIA attributes (aria-label, aria-expanded), custom data attributes, and certain SVG properties.Follow-up: What about class and style binding — are those property or attribute bindings? Answer: Angular provides special shorthand for both. [class.active]=“isActive” toggles a single CSS class, while [ngClass] handles multiple. [style.color]=“myColor” sets a single style property, and [style.font-size.px]=“size” handles units. Under the hood, Angular uses the DOM property API (element.classList, element.style), not setAttribute. This is important because style properties set via the property API are inline styles that override stylesheet rules.
Strong Answer: First, I would diagnose by opening Angular DevTools and running the Profiler during a scroll interaction. This shows exactly which components run change detection and how long each takes. I can also add a console.log in ngDoCheck of the child component to confirm it runs on every cycle.The fix involves multiple layers. First, I ensure the @for loop (or *ngFor) uses track with a unique identifier like item.id. Without tracking, Angular destroys and recreates every DOM element on any list change. Second, I switch the child component to ChangeDetectionStrategy.OnPush, so it only re-renders when its inputs change by reference. Third, if 10,000 items is the real dataset size, I implement virtual scrolling with cdk-virtual-scroll-viewport, which only renders the 20-30 items visible in the viewport. Fourth, I make sure the parent is not creating new object references on every change detection cycle — for example, calling a method in the template that returns a new array each time.The combination of OnPush + track + virtual scrolling can take a list from rendering 10,000 components to rendering only 30, each checked only when their specific data changes. That is a 300x reduction in work.Follow-up: What if the child component needs data that changes frequently but the list itself does not? Answer: I would separate the frequently-changing data from the list data. For example, if each item has a “last seen” timestamp that updates every second, I would not pass it as an @Input to an OnPush component (since the reference changes constantly). Instead, I would have the child component inject a service that exposes that data as a signal, or use the async pipe with a per-item observable. This way the list itself stays stable while each child independently updates its volatile data.
Strong Answer: Signal inputs are the recommended approach for all new code in Angular 17+. The key advantages: they integrate with Angular’s signal reactivity system, so you can use computed() to derive values from inputs without lifecycle hooks. They are type-safe by default — input.required<User>() will not compile if the parent does not provide it. And they work seamlessly with OnPush change detection because signals automatically notify the framework when they change.The main case where I might still use @Input is when integrating with existing codebases or third-party libraries that expect decorator-based inputs. Also, if you need the transform option with complex logic, @Input transform is slightly more flexible in edge cases. Another scenario: if you are writing a library that needs to support Angular 14-16 consumers, you cannot use signal inputs since they require Angular 17+.In practice, I treat this like the migration from var to let/const in JavaScript — signal inputs are strictly better for new code, but there is no urgent need to rewrite every existing @Input in a codebase.Follow-up: How do you handle the transition from ngOnChanges to signal-based reactivity? Answer: With decorator inputs, you use ngOnChanges to react to input changes. With signal inputs, you replace that with either computed() for derived values or effect() for side effects. For example, instead of checking changes[‘userId’] in ngOnChanges and fetching data, you write an effect that reads the userId signal input and triggers a fetch. The effect automatically re-runs whenever the input changes. This is cleaner because you do not need to check which input changed or compare previous and current values.

Next Steps

Next: Directives & Pipes

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