🧩 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 property | Purpose |
|---|---|
declarations | The components, directives, and pipes this module owns. Each could belong to only one module. |
imports | Other modules whose exported pieces this module needs. |
exports | Declarations made visible to modules that import this one. |
providers | Services this module adds to the injector. |
bootstrap | The 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:
The metadata tells Angular how to find and render the component. The most common options:
| Option | What it does |
|---|---|
selector | The CSS selector Angular looks for in templates, e.g. app-hero-detail. |
standalone | true for modern components (no owning module). |
imports | Components, directives, and pipes this template uses. |
template / templateUrl | Inline HTML, or a path to an HTML file. |
styles / styleUrl | Inline CSS, or a path to a CSS file (scoped to this component). |
changeDetection | Optional 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.
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.
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:
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()andoutput(). - 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:
- Generate a standalone
AlertComponentwith a signal inputkindthat can be'info','success', or'warning'. - Give it a single
<ng-content>slot so callers can supply any message markup. - Bind a CSS class from
kind()so the alert's color reflects its type. - Use it twice from a parent with two different
kindvalues 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 viaoutput(); 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.