Skip to main content

🧩 Modules and Components

Components are the atoms of every Angular app. This lesson shows how modern Angular composes them with standalone imports (and where the older NgModule still fits), what lives inside a component, how its lifecycle unfolds, and how components share data through signal inputs, outputs, and content projection.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain how standalone components declare their own dependencies with imports, and what an NgModule did before them
  • Break a component into its three parts — template, class, and metadata
  • Trace a component through its lifecycle hooks and know which to use when
  • Pass data down with signal inputs and back up with outputs
  • Build a reusable component using content projection

Estimated Time: 30–40 minutes  •  Difficulty: Intermediate

Hands-on: Build a reusable <app-card> with projected content and a signal input.

In This Lesson

Standalone Components First

In modern Angular, a component is standalone: it declares everything it needs directly, through its own imports array. There is no separate registration step and, for most apps, no NgModule at all.

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeroBadgeComponent } from './hero-badge.component';

@Component({
  selector: 'app-hero-detail',
  standalone: true,
  imports: [CommonModule, HeroBadgeComponent], // this component's dependencies
  templateUrl: './hero-detail.component.html',
  styleUrl: './hero-detail.component.css',
})
export class HeroDetailComponent {}

Everything a template uses — other components, directives, pipes — must appear in that imports list. This makes each component honest about its dependencies: read the top of the file and you know exactly what it relies on.

💡 Analogy: A standalone component is like a self-contained appliance that ships with its own power cord and plug. There's no need to visit a central fuse box (a module) to register it before it works — plug it in and go.

✅ The modern default

Since Angular 17, ng new and ng generate component produce standalone components automatically. This is the style you should reach for. NgModules still exist and you will meet them in older code, so the next section explains what they did.

What NgModules Were For

Before standalone components, every component, directive, and pipe had to be declared inside an NgModule — a class decorated with @NgModule that grouped related pieces and wired their dependencies. You will still see this in many codebases.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HeroDetailComponent } from './hero-detail.component';

@NgModule({
  declarations: [AppComponent, HeroDetailComponent], // components owned by this module
  imports: [BrowserModule],                          // other modules it depends on
  providers: [],                                      // services it contributes
  bootstrap: [AppComponent],                          // root component to launch
})
export class AppModule {}
NgModule propertyPurpose
declarationsThe components, directives, and pipes this module owns. Each could belong to only one module.
importsOther modules whose exported pieces this module needs.
exportsDeclarations made visible to modules that import this one.
providersServices this module adds to the injector.
bootstrapThe root component to launch — set only in the root module.

⚠️ The pain standalone components solved

NgModules added ceremony: create a component and you also had to remember to declare it, decide which module owned it, and manage exports so other modules could see it. Forgetting a step produced cryptic "not a known element" errors. Standalone components remove that whole layer — dependencies live on the component itself.

💡 Analogy: An NgModule was like a company department that owned a roster of staff (declarations), borrowed help from other departments (imports), and loaned out specialists (exports). Standalone components are like freelancers who bring their own toolkit and don't need a department at all.

Anatomy of a Component

Every component is three things bound together by the @Component decorator:

flowchart LR A[Component] --> B["Template (HTML view)"] A --> C["Class (TypeScript logic)"] A --> D["Metadata (@Component decorator)"]

The metadata tells Angular how to find and render the component. The most common options:

OptionWhat it does
selectorThe CSS selector Angular looks for in templates, e.g. app-hero-detail.
standalonetrue for modern components (no owning module).
importsComponents, directives, and pipes this template uses.
template / templateUrlInline HTML, or a path to an HTML file.
styles / styleUrlInline CSS, or a path to a CSS file (scoped to this component).
changeDetectionOptional strategy, e.g. ChangeDetectionStrategy.OnPush for performance.
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-product-detail',   // metadata
  standalone: true,
  template: `                        <!-- template -->
    <h2>{{ name() }}</h2>
    <button (click)="favorite()">★ Favorite</button>
  `,
})
export class ProductDetailComponent { // class
  name = signal('Wireless Mouse');
  favorite(): void {
    console.log('Favorited', this.name());
  }
}

Styles are scoped by default

CSS you put in a component's styleUrl or styles only affects that component's own view. Angular achieves this view encapsulation by adding unique attributes to the elements and scoping your rules to them, so a .title class here never bleeds into another component's .title.

💡 Analogy: Scoped styles are like painting a room with painter's tape carefully applied — the paint stays exactly where you intend and never smudges the hallway (the rest of the app).

The Component Lifecycle

A component is born when Angular creates its class and renders its view, and it dies when Angular removes it from the DOM. Between those moments Angular calls a series of lifecycle hooks — methods you can implement to run code at the right time.

flowchart TB A[constructor] --> B[ngOnChanges] B --> C[ngOnInit] C --> D[ngAfterViewInit] D --> E{Running...} E -->|inputs change| B E -->|destroyed| F[ngOnDestroy]

The handful you will use most:

📖 The hooks that matter

constructor — runs first; use it only for injecting dependencies, not for real work.

ngOnInit — runs once after the first render; the right place to fetch initial data.

ngOnChanges — runs whenever an input value changes; receives the previous and current values.

ngAfterViewInit — runs once the component's view and children exist; use it to touch DOM/child references.

ngOnDestroy — runs just before removal; the place to unsubscribe and clear timers.

import { Component, OnInit, OnDestroy, inject, signal } from '@angular/core';
import { HeroService, Hero } from './hero.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  template: `<p>{{ heroes().length }} heroes loaded</p>`,
})
export class HeroListComponent implements OnInit, OnDestroy {
  private heroService = inject(HeroService);
  private sub?: Subscription;
  heroes = signal<Hero[]>([]);

  ngOnInit(): void {
    // Fetch initial data once, after the component is set up
    this.sub = this.heroService.getHeroes()
      .subscribe(list => this.heroes.set(list));
  }

  ngOnDestroy(): void {
    // Always clean up subscriptions to avoid memory leaks
    this.sub?.unsubscribe();
  }
}
⚠️ Don't forget ngOnDestroy. Any manual subscription, interval, or event listener you create must be torn down here, or it will keep running after the component is gone — a classic source of memory leaks. (Angular's takeUntilDestroyed() and the async pipe can automate this, but knowing the hook is essential.)

Inputs & Outputs

Components form a tree, and data flows through it in a predictable shape: down through inputs and up through outputs.

Inputs flow down, events flow up A parent component passes data to a child via an input, and the child notifies the parent via an output event. Parent holds the state Child displays & reports @Input data ↓ event ↑
Figure 1 — Data flows down as inputs; the child talks back up by emitting output events. Keeping this one-way discipline makes state easy to reason about.

Signal inputs

Modern Angular exposes inputs as signals via the input() function. The child reads them like any other signal, and the value updates reactively when the parent changes it.

import { Component, input, output } from '@angular/core';
import { Hero } from './hero';

@Component({
  selector: 'app-hero-detail',
  standalone: true,
  template: `
    <h2>{{ hero().name }}</h2>
    <button (click)="save.emit(hero())">Save</button>
    <button (click)="remove.emit(hero())">Delete</button>
  `,
})
export class HeroDetailComponent {
  // Required input, read as a signal: hero()
  hero = input.required<Hero>();

  // Outputs the parent can listen to
  save = output<Hero>();
  remove = output<Hero>();
}

The parent wires them up in its template — square brackets to pass an input, parentheses to listen for an output:

<app-hero-detail
  [hero]="selectedHero()"
  (save)="onSave($event)"
  (remove)="onRemove($event)">
</app-hero-detail>
💡 Analogy: Inputs and outputs are like an assembly line. Materials (inputs) are handed down from one worker to the next, and status reports (outputs) are called back up the line so earlier stations can react.

Content Projection

Sometimes a component should provide a frame and let its parent fill in the contents — think of a card, a dialog, or a layout shell. Angular does this with content projection using the <ng-content> element. (It's the same idea as slots in Vue or children in React.)

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

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <header class="card-header">{{ title() }}</header>
      <div class="card-body">
        <ng-content></ng-content>   <!-- parent content lands here -->
      </div>
    </div>
  `,
  styleUrl: './card.component.css',
})
export class CardComponent {
  title = input('Untitled');
}

The parent uses it like a wrapper, and whatever sits between the tags is projected into the slot:

<app-card title="User Profile">
  <p>Name: {{ user().name }}</p>
  <p>Email: {{ user().email }}</p>
  <button (click)="edit()">Edit</button>
</app-card>

Multiple slots with select

You can offer several named slots by giving each <ng-content> a select attribute that matches part of the projected markup:

<!-- app-card template -->
<div class="card">
  <header><ng-content select="[card-title]"></ng-content></header>
  <div class="body"><ng-content></ng-content></div>
  <footer><ng-content select="[card-actions]"></ng-content></footer>
</div>

<!-- usage -->
<app-card>
  <h3 card-title>Wireless Mouse</h3>
  <p>Ergonomic, 6-button, USB-C.</p>
  <button card-actions>Add to cart</button>
</app-card>
💡 Analogy: Content projection is a modular furniture frame. The component supplies the shelving unit with labeled slots; the parent slides in whatever inserts it likes, so the same frame can hold books, plants, or dishes.

Communication Patterns

Inputs and outputs handle parent-child talk, but real apps also need components that aren't directly related to share state. Here is the full toolkit:

flowchart TB A[Component communication] --> B["Parent → Child (input)"] A --> C["Child → Parent (output)"] A --> D["Any ↔ Any (shared service)"] A --> E["Parent → Child method (viewChild)"]

For components with no parent-child link, a shared service holding a signal is the cleanest option. Both components inject the same singleton and read/write the same reactive value:

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class MessageService {
  // A single source of truth any component can read or update
  readonly message = signal('Welcome');

  setMessage(text: string): void {
    this.message.set(text);
  }
}
// sender.component.ts
import { Component, inject } from '@angular/core';
import { MessageService } from './message.service';

@Component({
  selector: 'app-sender',
  standalone: true,
  template: `<button (click)="send()">Send greeting</button>`,
})
export class SenderComponent {
  private messages = inject(MessageService);
  send(): void {
    this.messages.setMessage('Hello from the sender!');
  }
}

// receiver.component.ts
@Component({
  selector: 'app-receiver',
  standalone: true,
  template: `<p>{{ messages.message() }}</p>`,
})
export class ReceiverComponent {
  messages = inject(MessageService);
}

Because both components read the same signal, the receiver's template updates the instant the sender writes to it — no manual subscriptions required.

✅ Choosing a pattern

  • Direct parent/child? Use input() and output().
  • Distant or sibling components? Share a service holding a signal.
  • Parent needs to call a child's method? Use viewChild() to grab a reference.

Hands-on Exercise

🏋️ Build a Reusable Alert Card

Objective: Combine a signal input, scoped styles, and content projection into one reusable component.

Instructions:

  1. Generate a standalone AlertComponent with a signal input kind that can be 'info', 'success', or 'warning'.
  2. Give it a single <ng-content> slot so callers can supply any message markup.
  3. Bind a CSS class from kind() so the alert's color reflects its type.
  4. Use it twice from a parent with two different kind values and different projected content.
💡 Hint

Bind the class with [class]="kind()" (or [ngClass]). The projected message goes between the <app-alert> tags in the parent. Keep the color rules in the component's own styleUrl so they stay scoped.

✅ Example solution
import { Component, input } from '@angular/core';

type AlertKind = 'info' | 'success' | 'warning';

@Component({
  selector: 'app-alert',
  standalone: true,
  template: `
    <div class="alert" [class]="kind()">
      <ng-content></ng-content>
    </div>
  `,
  styles: [`
    .alert { padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid; }
    .info    { background: #eff6ff; border-color: #3b82f6; }
    .success { background: #ecfdf5; border-color: #10b981; }
    .warning { background: #fffbeb; border-color: #f59e0b; }
  `],
})
export class AlertComponent {
  kind = input<AlertKind>('info');
}
<!-- parent template -->
<app-alert kind="success">
  <strong>Saved!</strong> Your profile is up to date.
</app-alert>

<app-alert kind="warning">
  Your session expires in 5 minutes.
</app-alert>

🎯 Quick Quiz

Question 1: In modern Angular, how does a standalone component get access to another component it uses in its template?

Question 2: Which lifecycle hook is the right place to fetch a component's initial data?

Question 3: A child component needs to notify its parent that the user clicked "Save". What should it use?

Summary & Quiz

🎉 Key Takeaways

  • Standalone components declare their dependencies in their own imports — no NgModule required. NgModules still appear in legacy code and grouped declarations/imports/exports/providers.
  • A component is template + class + metadata, and its styles are scoped to its own view by default.
  • Lifecycle hooks — especially ngOnInit and ngOnDestroy — let you run code at the right moment and clean up after yourself.
  • Data flows down via input() and up via output(); distant components share a service holding a signal.
  • Content projection with <ng-content> makes flexible, reusable wrapper components.

📚 Further Reading

🚀 What's Next?

You can build and connect components — next we dive into what makes their templates come alive: templates and data binding, including interpolation, property and event binding, the new control flow, and pipes.

🎉 Solid work!

Components and their connections are the skeleton of every Angular app. Let's give them behavior next.