Components & Templates

Angular Components

Deep dive into Angular components — creation, lifecycle hooks, component interaction with @Input/@Output, standalone components, and best practices.

What Are Components?

Components are the fundamental building blocks of Angular applications. Every piece of UI you see is a component — the header, a product card, a form, a modal. Components are composed together to form the complete application.

A component consists of three parts:

  1. Class (TypeScript) — logic and data
  2. Template (HTML) — the view/markup
  3. Styles (CSS) — scoped styling

Creating a Component

Using the CLI

bash
ng generate component user-profile
# Shorthand: ng g c user-profile

This creates:

src/app/user-profile/
├── user-profile.component.ts
├── user-profile.component.html
├── user-profile.component.css
└── user-profile.component.spec.ts

Manual Creation

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

@Component({
  selector: 'app-user-profile',
  standalone: true,
  templateUrl: './user-profile.component.html',
  styleUrl: './user-profile.component.css',
})
export class UserProfileComponent {
  name = 'John Doe';
  role = 'Developer';
}

Inline vs External Templates

Inline Template

Best for small components:

typescript
@Component({
  selector: 'app-badge',
  standalone: true,
  template: `
    <span class="badge" [class]="type">
      {{ label }}
    </span>
  `,
  styles: [`
    .badge { padding: 4px 12px; border-radius: 20px; font-size: 12px; }
    .success { background: #d4edda; color: #155724; }
    .warning { background: #fff3cd; color: #856404; }
  `],
})
export class BadgeComponent {
  label = 'Active';
  type = 'success';
}

External Template

Best for larger components:

typescript
@Component({
  selector: 'app-dashboard',
  standalone: true,
  templateUrl: './dashboard.component.html',
  styleUrl: './dashboard.component.css',
})
export class DashboardComponent {
  // ...
}

Component Lifecycle Hooks

Angular components go through a lifecycle from creation to destruction. You can hook into these phases:

HookWhen CalledCommon Use
ngOnInitAfter first data-bound properties setFetch data, initialize logic
ngOnChangesWhen input properties changeReact to input changes
ngDoCheckEvery change detection runCustom change detection
ngAfterViewInitAfter view (DOM) is initializedAccess DOM elements
ngAfterContentInitAfter content projectionAccess projected content
ngOnDestroyBefore component is destroyedCleanup subscriptions, timers

Using Lifecycle Hooks

typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `<p>Elapsed: {{ seconds }}s</p>`,
})
export class TimerComponent implements OnInit, OnDestroy {
  seconds = 0;
  private subscription!: Subscription;

  ngOnInit() {
    console.log('Timer component initialized');
    this.subscription = interval(1000).subscribe(() => {
      this.seconds++;
    });
  }

  ngOnDestroy() {
    console.log('Timer component destroyed — cleaning up');
    this.subscription.unsubscribe();
  }
}

Component Interaction

Parent → Child with @Input

The parent passes data down to a child:

typescript
// child.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `,
})
export class UserCardComponent {
  @Input({ required: true }) user!: { name: string; email: string };
}
html
<!-- parent.component.html -->
<app-user-card [user]="selectedUser" />

Child → Parent with @Output

The child emits events up to the parent:

typescript
// child.component.ts
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: `
    <input #searchInput type="text" placeholder="Search...">
    <button (click)="search.emit(searchInput.value)">Search</button>
  `,
})
export class SearchBoxComponent {
  @Output() search = new EventEmitter<string>();
}
html
<!-- parent.component.html -->
<app-search-box (search)="onSearch($event)" />
typescript
// parent.component.ts
export class ParentComponent {
  onSearch(query: string) {
    console.log('Searching for:', query);
  }
}

Input Transforms (Angular 16+)

typescript
import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core';

@Component({
  selector: 'app-alert',
  standalone: true,
  template: `
    @if (visible) {
      <div class="alert" [class]="type">{{ message }}</div>
    }
  `,
})
export class AlertComponent {
  @Input() message = '';
  @Input() type: 'info' | 'warning' | 'error' = 'info';
  @Input({ transform: booleanAttribute }) visible = true;
  @Input({ transform: numberAttribute }) duration = 5000;
}
html
<!-- Usage: these string attributes are auto-transformed -->
<app-alert message="Saved!" visible duration="3000" />

Content Projection (ng-content)

Content projection lets a parent inject content into a child component's template:

typescript
// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[card-title]" />
      </div>
      <div class="card-body">
        <ng-content />
      </div>
      <div class="card-footer">
        <ng-content select="[card-actions]" />
      </div>
    </div>
  `,
})
export class CardComponent {}
html
<!-- Usage -->
<app-card>
  <h3 card-title>My Card</h3>
  <p>This is the card body content.</p>
  <div card-actions>
    <button>Save</button>
    <button>Cancel</button>
  </div>
</app-card>

ViewChild — Accessing Child Elements

typescript
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-focus-input',
  standalone: true,
  template: `<input #myInput type="text" placeholder="Auto-focused">`,
})
export class FocusInputComponent implements AfterViewInit {
  @ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    this.inputRef.nativeElement.focus();
  }
}

Component Best Practices

  1. Keep components small and focused — each component should do one thing well
  2. Use standalone components — they are the modern default (no NgModule needed)
  3. Use OnPush change detection for performance:
    typescript
    @Component({
      changeDetection: ChangeDetectionStrategy.OnPush,
      // ...
    })
  4. Unsubscribe from Observables in ngOnDestroy to prevent memory leaks
  5. **Use @Input({ required: true }) ** for mandatory inputs
  6. Prefix selectors — use app- prefix (or your org prefix) for all component selectors

Next Steps

Components need content to display. Next, we'll explore templates and data binding in depth — the powerful template syntax that connects your component class to the HTML view.