Chapter 1 — What is Data Binding?
1.1 — The Problem Data Binding Solves
When you build a web application, you have two worlds that need to talk to each other constantly.
The first world is your TypeScript class — this is where all your data and logic lives. User names, product lists, whether a button is loading, what the current count is. All of that is in TypeScript.
The second world is your HTML template — this is what the user actually sees and interacts with on screen. Buttons, inputs, lists, text.
The problem is: how do these two worlds stay in sync?
When the user types their name into an input field, that value needs to get into your TypeScript so you can process it. When your TypeScript fetches a list of products from an API, those products need to show up on screen. When a button is clicked, TypeScript needs to know about it. When TypeScript marks something as "loading", the button on screen should show a spinner.
In plain JavaScript, you would do all of this manually. You would grab DOM elements with document.getElementById, read their values, update their text, add and remove classes. This gets messy and hard to manage very quickly.
Angular solves this with data binding — a clean, declarative way to connect your TypeScript class and your HTML template. Instead of manually writing code to push data back and forth, you just declare the connection in your template and Angular handles the rest automatically.
1.2 — The Four Types of Data Binding
Angular has four types of data binding. Each one handles a different direction of data flow:
TypeScript Class HTML Template
───────────── ─────────────
──── Interpolation {{ }} ────►
──── Property Binding [ ] ──►
◄─── Event Binding ( ) ───
◄──► Two-Way Binding [( )] ──►
Interpolation — TypeScript → Template. Displays a value from TypeScript in the HTML.
Property Binding — TypeScript → Template. Sets an HTML element's property to a value from TypeScript.
Event Binding — Template → TypeScript. Listens for an event in the HTML and calls a TypeScript method when it fires.
Two-Way Binding — Both directions at once. The HTML input and the TypeScript property stay perfectly in sync with each other.
Let's cover each one in complete depth.
Chapter 2 — Interpolation
2.1 — What is Interpolation?
Interpolation is the simplest form of data binding. You use double curly braces {{ }} to display a value from your TypeScript class directly in the HTML template.
Angular reads whatever is inside the curly braces, evaluates it, converts it to a string, and inserts it into the HTML at that exact spot.
import { Component } from '@angular/core';
@Component({
selector: 'app-profile',
imports: [],
templateUrl: './profile.html',
styleUrl: './profile.css'
})
export class Profile {
userName: string = 'Rahul Sharma';
age: number = 25;
city: string = 'Mumbai';
isVerified: boolean = true;
}
<div class="profile">
<h1>{{ userName }}</h1>
<p>Age: {{ age }}</p>
<p>City: {{ city }}</p>
<p>Verified: {{ isVerified }}</p>
</div>
Angular reads userName from the TypeScript class and puts "Rahul Sharma" in the <h1>. Reads age and puts "25" in the paragraph. And so on.
2.2 — Expressions Inside Interpolation
The {{ }} does not just display variables. It evaluates any valid TypeScript expression:
<!-- Math -->
<p>Next year I will be {{ age + 1 }} years old</p>
<!-- String methods -->
<p>{{ userName.toUpperCase() }}</p>
<p>{{ city.toLowerCase() }}</p>
<!-- Ternary operator -->
<p>Status: {{ isVerified ? 'Verified ✓' : 'Not Verified' }}</p>
<!-- Array length -->
<p>You have {{ notifications.length }} notifications</p>
<!-- Calling a method -->
<p>{{ getFullGreeting() }}</p>
export class Profile {
userName: string = 'Rahul Sharma';
age: number = 25;
city: string = 'Mumbai';
isVerified: boolean = true;
notifications: string[] = ['Message 1', 'Message 2', 'Message 3'];
getFullGreeting(): string {
return `Welcome back, ${this.userName}! You are in ${this.city}.`;
}
}
2.3 — What You Cannot Do in Interpolation
Interpolation is for displaying values only. It is not for executing complex logic or statements:
<!-- These will NOT work: -->
{{ let x = 5 }} ← variable declarations not allowed
{{ if (age > 18) { } }} ← if statements not allowed
{{ userName = 'New Name' }} ← assignments not allowed
If you need complex logic, put it in a method in your TypeScript class and call that method from the template.
Chapter 3 — Property Binding
3.1 — What is Property Binding?
Property binding lets you set an HTML element's property to a dynamic value from your TypeScript class. You use square brackets [ ] around the property name.
The key thing to understand is the difference between an HTML attribute and a DOM property:
An attribute is what you write in the HTML source code. Attributes are static text that the browser reads once when it parses the HTML. Example: <input type="text" value="hello"> — type and value are attributes.
A property is what exists on the actual DOM element object in JavaScript after the browser has parsed the HTML. Properties are dynamic and can change. When you do element.value = 'new value' in JavaScript, you are setting a property, not an attribute.
Property binding binds to DOM properties, not HTML attributes. In most cases they have the same name and this distinction does not matter. But in a few cases it matters a lot — we will see examples of this.
3.2 — Basic Property Binding
import { Component } from '@angular/core';
@Component({
selector: 'app-button-demo',
imports: [],
templateUrl: './button-demo.html',
styleUrl: './button-demo.css'
})
export class ButtonDemo {
isDisabled: boolean = true;
isLoading: boolean = false;
imageUrl: string = '/images/profile.jpg';
imageAlt: string = 'Profile photo';
inputPlaceholder: string = 'Enter your name...';
linkUrl: string = 'https://angular.dev';
}
<!-- Binding to the disabled property -->
<button [disabled]="isDisabled">Submit</button>
<!-- Binding to src and alt properties of an image -->
<img [src]="imageUrl" [alt]="imageAlt">
<!-- Binding to placeholder property of an input -->
<input [placeholder]="inputPlaceholder" type="text">
<!-- Binding to href property of a link -->
<a [href]="linkUrl">Visit Angular Docs</a>
With [disabled]="isDisabled", Angular reads the isDisabled value from TypeScript (which is true) and sets the button's disabled property to true. The button becomes disabled. If you later change isDisabled to false, Angular automatically updates the button and it becomes enabled.
3.3 — Property Binding vs String Interpolation vs Plain Attribute
This is a common point of confusion. There are three ways that look similar but behave differently:
<!-- 1. Plain HTML attribute — static string, never changes -->
<button disabled="true">Button 1</button>
<!-- 2. Interpolation — works for simple cases but is limited -->
<img src="{{ imageUrl }}">
<!-- 3. Property binding — the Angular way, evaluates TypeScript -->
<img [src]="imageUrl">
For setting properties dynamically, always use property binding with [ ]. Interpolation in attributes technically works for simple cases but property binding is the correct approach and gives you access to non-string values.
There is one important case where this distinction really matters — the disabled attribute:
<!-- This will ALWAYS disable the button — even when the value is false! -->
<!-- Because the attribute "disabled" exists, the button IS disabled -->
<button disabled="{{ isDisabled }}">Wrong way</button>
<!-- This correctly enables/disables based on the actual boolean value -->
<button [disabled]="isDisabled">Correct way</button>
HTML attributes are strings. When you write disabled="false", the attribute exists with the value "false" — but because the attribute exists, the button is disabled. The string "false" is truthy. This is a classic bug in Angular beginners' code. Property binding avoids this by working with actual JavaScript values.
3.4 — Binding to Class and Style
You can dynamically apply CSS classes and inline styles using property binding:
export class StyleDemo {
isActive: boolean = true;
hasError: boolean = false;
textColor: string = '#0070f3';
fontSize: number = 18;
}
<!-- Bind a single class conditionally -->
<div [class.active]="isActive">This div gets class 'active' when isActive is true</div>
<div [class.error]="hasError">This div gets class 'error' when hasError is true</div>
<!-- Bind inline styles dynamically -->
<p [style.color]="textColor">This text color comes from TypeScript</p>
<p [style.font-size.px]="fontSize">Font size in pixels from TypeScript</p>
<p [style.font-weight]="isActive ? 'bold' : 'normal'">Bold when active</p>
The [class.active]="expression" syntax adds the class active when the expression is true and removes it when false. This is clean and direct.
The [style.color]="value" syntax sets that specific style property. When the value has a unit, you can include the unit in the binding name itself: [style.font-size.px]="18" sets font-size: 18px.
3.5 — Binding to Non-Standard HTML Attributes with attr.
Some HTML attributes do not have a corresponding DOM property. For these, you need to use the attr. prefix:
<!-- ARIA attributes for accessibility -->
<button [attr.aria-label]="buttonLabel">Click me</button>
<!-- colspan on a table cell -->
<td [attr.colspan]="columnSpan">Cell content</td>
<!-- Custom data attributes -->
<div [attr.data-product-id]="product.id">Product</div>
When you try to use regular property binding with an attribute that has no DOM property equivalent, Angular will throw an error. The attr. prefix tells Angular: "bind to the HTML attribute directly, not a DOM property."
Chapter 4 — Event Binding
4.1 — What is Event Binding?
Event binding lets your HTML template tell TypeScript when something happens — a button is clicked, a key is pressed, a form is submitted, the mouse moves over an element. You use parentheses ( ) around the event name.
When that event fires, Angular calls the method or expression you specified in the template.
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
imports: [],
templateUrl: './counter.html',
styleUrl: './counter.css'
})
export class Counter {
count: number = 0;
increment(): void {
this.count++;
}
decrement(): void {
this.count--;
}
reset(): void {
this.count = 0;
}
}
<div class="counter">
<button (click)="decrement()">-</button>
<span>{{ count }}</span>
<button (click)="increment()">+</button>
<button (click)="reset()">Reset</button>
</div>
Every time the + button is clicked, Angular calls increment() in the TypeScript class. The count property increases. Angular detects the change and updates {{ count }} on screen. All of this happens automatically.
4.2 — The $event Object
When an event fires, the browser creates an event object containing information about what happened — where the mouse was, which key was pressed, what value was in the input. In Angular, you access this using the special $event variable:
import { Component } from '@angular/core';
@Component({
selector: 'app-event-demo',
imports: [],
templateUrl: './event-demo.html',
styleUrl: './event-demo.css'
})
export class EventDemo {
mousePosition = { x: 0, y: 0 };
typedText: string = '';
lastKey: string = '';
onMouseMove(event: MouseEvent): void {
this.mousePosition.x = event.clientX;
this.mousePosition.y = event.clientY;
}
onInputChange(event: Event): void {
const inputElement = event.target as HTMLInputElement;
this.typedText = inputElement.value;
}
onKeyPress(event: KeyboardEvent): void {
this.lastKey = event.key;
}
}
<div class="demo" (mousemove)="onMouseMove($event)">
<p>Mouse position: {{ mousePosition.x }}, {{ mousePosition.y }}</p>
</div>
<input
type="text"
(input)="onInputChange($event)"
placeholder="Type something...">
<p>You typed: {{ typedText }}</p>
<input
type="text"
(keydown)="onKeyPress($event)"
placeholder="Press any key...">
<p>Last key pressed: {{ lastKey }}</p>
$event is the native browser event object. The type depends on the event — MouseEvent for mouse events, KeyboardEvent for keyboard events, Event for general events. TypeScript knows what properties are available on each event type, so you get autocomplete.
4.3 — Common Events You Will Use
Here are all the events you will use regularly:
<!-- Mouse events -->
<button (click)="onClick()">Click me</button>
<div (dblclick)="onDoubleClick()">Double click me</div>
<div (mouseenter)="onHoverStart()">Hover start</div>
<div (mouseleave)="onHoverEnd()">Hover end</div>
<!-- Keyboard events -->
<input (keydown)="onKeyDown($event)"> ← fires when key is pressed down
<input (keyup)="onKeyUp($event)"> ← fires when key is released
<input (keyup.enter)="onEnterPressed()"> ← only fires when Enter is released
<!-- Form / input events -->
<input (input)="onTyping($event)"> ← fires on every character typed
<input (change)="onChange($event)"> ← fires when input loses focus with a changed value
<input (focus)="onFocused()"> ← fires when input gains focus
<input (blur)="onBlurred()"> ← fires when input loses focus
<form (submit)="onSubmit($event)"> ← fires when form is submitted
The (keyup.enter) syntax is an Angular-specific shortcut. Instead of checking if (event.key === 'Enter') inside your method, you can specify the key directly in the template.
4.4 — Calling Methods with Arguments
You can pass arguments directly to methods from the template:
export class ItemList {
items = ['Angular', 'TypeScript', 'RxJS', 'Node.js'];
deleteItem(itemName: string): void {
this.items = this.items.filter(item => item !== itemName);
}
selectItem(item: string, index: number): void {
console.log(`Selected ${item} at index ${index}`);
}
}
@for (item of items; track item; let i = $index) {
<div class="item">
<span>{{ item }}</span>
<button (click)="deleteItem(item)">Delete</button>
<button (click)="selectItem(item, i)">Select</button>
</div>
}
You can pass any expression as an argument — the item itself, the index, a computed value, or even a combination.
4.5 — Inline Event Expressions
For very simple cases, you can write a small expression directly in the template instead of creating a separate method:
<!-- Simple assignment directly in template -->
<button (click)="count = count + 1">Increment</button>
<button (click)="isMenuOpen = !isMenuOpen">Toggle Menu</button>
<button (click)="selectedTab = 'profile'">Go to Profile</button>
This is fine for truly simple, one-liner changes. But for anything more complex — validation, multiple operations, async work — always use a method in the TypeScript class. Keep your templates readable.
Chapter 5 — Two-Way Binding
5.1 — What is Two-Way Binding?
Interpolation and property binding are one-way — from TypeScript to the template. Event binding is one-way — from the template to TypeScript.
Two-way binding is both directions at once. The most common use case is form inputs — you want the input to display the current value from TypeScript, AND you want TypeScript to update whenever the user types something.
You use the syntax [(ngModel)] for two-way binding on form inputs. This syntax is sometimes jokingly called "banana in a box" because [()] looks like a banana inside a box.
5.2 — Setting Up FormsModule
To use ngModel, you need to import FormsModule in your component:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-form-demo',
imports: [FormsModule], // ← must import this
templateUrl: './form-demo.html',
styleUrl: './form-demo.css'
})
export class FormDemo {
userName: string = '';
email: string = '';
selectedColor: string = 'blue';
agreeToTerms: boolean = false;
favoriteFramework: string = 'angular';
}
5.3 — Two-Way Binding in Action
<div class="form-demo">
<div class="field">
<label>Name</label>
<input type="text" [(ngModel)]="userName" placeholder="Enter your name">
<p>Hello, {{ userName }}!</p>
</div>
<div class="field">
<label>Email</label>
<input type="email" [(ngModel)]="email" placeholder="Enter email">
<p>Your email: {{ email }}</p>
</div>
<div class="field">
<label>Favorite Color</label>
<select [(ngModel)]="selectedColor">
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="green">Green</option>
</select>
<p>You chose: {{ selectedColor }}</p>
</div>
<div class="field">
<label>
<input type="checkbox" [(ngModel)]="agreeToTerms">
I agree to the terms
</label>
<p>Agreed: {{ agreeToTerms }}</p>
</div>
<div class="field">
<label>Framework</label>
<label><input type="radio" [(ngModel)]="favoriteFramework" value="angular"> Angular</label>
<label><input type="radio" [(ngModel)]="favoriteFramework" value="react"> React</label>
<label><input type="radio" [(ngModel)]="favoriteFramework" value="vue"> Vue</label>
<p>You chose: {{ favoriteFramework }}</p>
</div>
</div>
As soon as you type in the Name input, userName in TypeScript updates instantly. The paragraph below the input updates at the same time because {{ userName }} reads from that same property. Both the input and the paragraph are always in sync — that is two-way binding.
5.4 — How Two-Way Binding Works Under the Hood
[(ngModel)]="userName" is actually just a shorthand for doing both property binding AND event binding at the same time:
<!-- Two-way binding shorthand -->
<input [(ngModel)]="userName">
<!-- Exactly equivalent to this: -->
<input [ngModel]="userName" (ngModelChange)="userName = $event">
[ngModel]="userName" — property binding sets the input's value to whatever userName holds.
(ngModelChange)="userName = $event" — event binding updates userName whenever the input value changes.
Angular combines these two into the [()] syntax as a convenience. Understanding this is useful for debugging and for building your own two-way bindable components.
Chapter 6 — Angular's Built-in Control Flow
6.1 — Why Control Flow Exists in Templates
Your templates need to be dynamic. Sometimes you want to show something only when a condition is true. Sometimes you want to repeat an element for every item in a list. Sometimes you want to show different things based on a value.
Angular provides built-in template syntax for all of this. In modern Angular, this is done with @if, @for, and @switch — clean, readable syntax that lives directly in your HTML template.
6.2 — @if — Conditional Rendering
@if shows or hides a block of HTML based on a condition. If the condition is true, the HTML is added to the DOM. If it is false, the HTML is completely removed from the DOM (not just hidden — actually removed).
import { Component } from '@angular/core';
@Component({
selector: 'app-auth-demo',
imports: [],
templateUrl: './auth-demo.html',
styleUrl: './auth-demo.css'
})
export class AuthDemo {
isLoggedIn: boolean = false;
userName: string = 'Rahul';
userRole: string = 'admin';
cartCount: number = 3;
login(): void {
this.isLoggedIn = true;
}
logout(): void {
this.isLoggedIn = false;
}
}
@if (isLoggedIn) {
<div class="welcome">
<h2>Welcome back, {{ userName }}!</h2>
<p>You have {{ cartCount }} items in your cart.</p>
<button (click)="logout()">Logout</button>
</div>
} @else {
<div class="login-prompt">
<h2>Please log in</h2>
<button (click)="login()">Login</button>
</div>
}
The @else block is optional. When the condition in @if is false, Angular renders the @else block instead.
6.3 — @if with @else if
For multiple conditions, you chain @else if:
@if (userRole === 'admin') {
<div class="admin-panel">
<h2>Admin Dashboard</h2>
<p>You have full access to all features.</p>
</div>
} @else if (userRole === 'moderator') {
<div class="mod-panel">
<h2>Moderator Panel</h2>
<p>You can manage content and users.</p>
</div>
} @else if (userRole === 'user') {
<div class="user-panel">
<h2>User Dashboard</h2>
<p>Welcome to your personal space.</p>
</div>
} @else {
<div class="guest-view">
<h2>Guest Access</h2>
<p>Please sign up for a full experience.</p>
</div>
}
Angular evaluates each condition in order and renders the first block whose condition is true.
6.4 — @for — Rendering Lists
@for loops through an array and renders a block of HTML for each item:
import { Component } from '@angular/core';
@Component({
selector: 'app-list-demo',
imports: [],
templateUrl: './list-demo.html',
styleUrl: './list-demo.css'
})
export class ListDemo {
fruits: string[] = ['Apple', 'Mango', 'Banana', 'Orange', 'Grapes'];
students = [
{ id: 1, name: 'Rahul Sharma', score: 92, passing: true },
{ id: 2, name: 'Priya Patel', score: 78, passing: true },
{ id: 3, name: 'Amit Kumar', score: 45, passing: false },
{ id: 4, name: 'Sneha Gupta', score: 88, passing: true }
];
}
<!-- Simple array of strings -->
<ul>
@for (fruit of fruits; track fruit) {
<li>{{ fruit }}</li>
}
</ul>
<!-- Array of objects -->
<div class="student-list">
@for (student of students; track student.id) {
<div class="student-item">
<strong>{{ student.name }}</strong>
<span>Score: {{ student.score }}</span>
@if (student.passing) {
<span class="pass">✓ Passing</span>
} @else {
<span class="fail">✗ Failing</span>
}
</div>
}
</div>
The track part is required in Angular's @for. It tells Angular how to uniquely identify each item in the list. This is important for performance — when the list changes, Angular uses track to figure out which items are new, which moved, and which were removed, so it can update only what changed instead of re-rendering the entire list.
For arrays of objects, always track by a unique ID: track student.id.
For arrays of simple values like strings, you can track by the value itself: track fruit.
Never track $index unless there is absolutely no other option — it negates the performance benefit.
6.5 — @for with Built-in Variables
Inside a @for block, Angular provides several useful variables you can use:
@for (student of students; track student.id; let i = $index; let isFirst = $first; let isLast = $last; let isEven = $even) {
<div class="student-item"
[class.highlighted]="isFirst"
[class.even-row]="isEven">
<span class="position">{{ i + 1 }}.</span>
<span>{{ student.name }}</span>
@if (isFirst) {
<span class="badge">🏆 Top Student</span>
}
@if (isLast) {
<span class="badge">📌 Last Entry</span>
}
</div>
}
Here are all the built-in variables available in @for:
$index — the current iteration index, starting from 0. Assign it with let i = $index.
$first — a boolean, true only for the very first item.
$last — a boolean, true only for the very last item.
$even — a boolean, true for items at even indexes (0, 2, 4...).
$odd — a boolean, true for items at odd indexes (1, 3, 5...).
$count — the total number of items in the array.
6.6 — @empty — Handling Empty Lists
Angular's @for has a built-in @empty block that renders when the array is empty:
export class TaskList {
tasks: string[] = []; // empty array
addTask(): void {
this.tasks.push('New Task ' + (this.tasks.length + 1));
}
}
<button (click)="addTask()">Add Task</button>
<div class="task-list">
@for (task of tasks; track task) {
<div class="task">{{ task }}</div>
} @empty {
<div class="empty-state">
<p>No tasks yet. Click "Add Task" to get started!</p>
</div>
}
</div>
When tasks is empty, the @empty block renders. When tasks are added, the @empty block disappears and the task items appear. This eliminates the need for a separate @if (tasks.length === 0) check.
6.7 — @switch — Multiple Conditions Based on One Value
When you want to show different things based on a single value and you have many possible cases, @switch is cleaner than a long chain of @else if:
export class OrderTracking {
orderStatus: string = 'processing';
}
<div class="order-status">
@switch (orderStatus) {
@case ('pending') {
<div class="status pending">
<span>⏳</span>
<p>Your order is pending confirmation</p>
</div>
}
@case ('processing') {
<div class="status processing">
<span>⚙️</span>
<p>Your order is being processed</p>
</div>
}
@case ('shipped') {
<div class="status shipped">
<span>🚚</span>
<p>Your order is on the way</p>
</div>
}
@case ('delivered') {
<div class="status delivered">
<span>✅</span>
<p>Your order has been delivered</p>
</div>
}
@case ('cancelled') {
<div class="status cancelled">
<span>❌</span>
<p>Your order was cancelled</p>
</div>
}
@default {
<div class="status unknown">
<p>Unknown status</p>
</div>
}
}
</div>
Angular evaluates the expression in @switch, then finds the matching @case and renders that block. If no case matches, @default renders. There is no fall-through behavior like in JavaScript's switch — only the matching case renders.
Chapter 7 — NgClass — Dynamic CSS Classes
7.1 — What is NgClass?
You already saw [class.className]="condition" for adding a single class conditionally. NgClass is more powerful — it lets you add and remove multiple classes at once based on different conditions.
To use NgClass, import it in your component:
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'app-ngclass-demo',
imports: [NgClass],
templateUrl: './ngclass-demo.html',
styleUrl: './ngclass-demo.css'
})
export class NgClassDemo {
isActive: boolean = true;
hasError: boolean = false;
isLarge: boolean = true;
buttonType: string = 'primary';
}
7.2 — NgClass with an Object
Pass an object where the keys are CSS class names and the values are boolean conditions:
<div [ngClass]="{
'active': isActive,
'error': hasError,
'large': isLarge
}">
This div gets active and large classes (not error because hasError is false)
</div>
Angular adds the class when the condition is true and removes it when false. You can have as many class/condition pairs as you need.
7.3 — NgClass with a Method
For complex logic, compute the class object in TypeScript and return it from a method:
export class StatusBadge {
status: string = 'success'; // could be: success, warning, error, info
getStatusClasses(): object {
return {
'badge': true, // always applied
'badge-success': this.status === 'success',
'badge-warning': this.status === 'warning',
'badge-error': this.status === 'error',
'badge-info': this.status === 'info'
};
}
}
<span [ngClass]="getStatusClasses()">{{ status }}</span>
7.4 — NgClass with an Array
You can also pass an array of class names:
<div [ngClass]="['card', 'shadow', 'rounded']">
This div gets all three classes
</div>
<!-- Or with dynamic values -->
<div [ngClass]="['btn', 'btn-' + buttonType]">
Button
</div>
When buttonType is 'primary', this renders with classes btn and btn-primary.
Chapter 8 — NgStyle — Dynamic Inline Styles
8.1 — What is NgStyle?
NgStyle lets you apply multiple inline styles dynamically to an element. Import it from @angular/common.
import { Component } from '@angular/core';
import { NgStyle } from '@angular/common';
@Component({
selector: 'app-ngstyle-demo',
imports: [NgStyle],
templateUrl: './ngstyle-demo.html',
styleUrl: './ngstyle-demo.css'
})
export class NgStyleDemo {
primaryColor: string = '#0070f3';
fontSize: number = 16;
isHighlighted: boolean = true;
opacity: number = 1;
}
<p [ngStyle]="{
'color': primaryColor,
'font-size': fontSize + 'px',
'opacity': opacity,
'background-color': isHighlighted ? '#fffde7' : 'transparent',
'font-weight': 'bold',
'padding': '12px'
}">
This paragraph has dynamic styles
</p>
8.2 — When to Use NgStyle vs Class Binding
Use [style.property] for a single dynamic style:
<p [style.color]="textColor">Simple single style</p>
Use NgStyle when you need to apply several styles at once based on complex conditions. But as a general rule, prefer using CSS classes over inline styles. Inline styles are harder to maintain and override. It is usually better to define CSS classes in your stylesheet and use NgClass to apply them conditionally.
Chapter 9 — ng-template, ng-container, ng-content
9.1 — ng-template
ng-template defines a block of HTML that Angular does NOT render by default. It sits in your template as a definition that Angular can use later — either you tell Angular to render it conditionally, or Angular uses it as a template reference.
The most common use is with @if @else:
@if (isLoading) {
<app-spinner></app-spinner>
} @else {
<div class="content">
<h2>Data loaded!</h2>
</div>
}
You can also use a template reference with the older *ngIf style (still valid):
<div *ngIf="isLoggedIn; else loginTemplate">
Welcome, {{ userName }}!
</div>
<ng-template #loginTemplate>
<p>Please log in to continue.</p>
</ng-template>
#loginTemplate is a template reference variable. When isLoggedIn is false, Angular renders the content of the ng-template marked with #loginTemplate. The ng-template itself never appears in the DOM — only its content is ever rendered.
9.2 — ng-container
ng-container is an invisible wrapper element. It lets you group elements and apply structural directives without adding any actual HTML element to the DOM.
The problem it solves — sometimes you need to apply a condition to a group of elements but you do not want a wrapper div:
<!-- This adds an unwanted div to the DOM -->
<div @if (isAdmin)>
<button>Edit</button>
<button>Delete</button>
<button>Manage Users</button>
</div>
<!-- This renders nothing extra in the DOM — ng-container disappears -->
@if (isAdmin) {
<ng-container>
<button>Edit</button>
<button>Delete</button>
<button>Manage Users</button>
</ng-container>
}
Actually with the modern @if syntax, you do not even need ng-container for this because @if blocks do not add wrapper elements. ng-container is more useful with the older *ngIf and *ngFor directive syntax where you need to apply two directives to the same element but HTML only allows one:
<!-- Can't put both *ngFor and *ngIf on the same element -->
<!-- This is wrong: -->
<div *ngFor="let item of items" *ngIf="item.isVisible">{{ item.name }}</div>
<!-- Use ng-container to separate them: -->
<ng-container *ngFor="let item of items">
<div *ngIf="item.isVisible">{{ item.name }}</div>
</ng-container>
<!-- Or better yet with modern syntax: -->
@for (item of items; track item.id) {
@if (item.isVisible) {
<div>{{ item.name }}</div>
}
}
9.3 — ng-content
We covered this deeply in Phase 3, but let's see it again here in context with data binding.
ng-content is a slot in a component's template where projected content (HTML passed in from outside) will appear:
import { Component } from '@angular/core';
@Component({
selector: 'app-alert',
imports: [],
template: `
<div class="alert" [class]="'alert-' + type">
<strong>{{ title }}</strong>
<ng-content></ng-content>
</div>
`,
styles: [`
.alert { padding: 16px; border-radius: 8px; margin: 8px 0; }
.alert-success { background: #d4edda; color: #155724; }
.alert-error { background: #f8d7da; color: #721c24; }
.alert-info { background: #d1ecf1; color: #0c5460; }
`]
})
export class Alert {
@Input() type: string = 'info';
@Input() title: string = '';
}
<!-- In parent template -->
<app-alert type="success" title="Success!">
Your profile has been updated successfully.
</app-alert>
<app-alert type="error" title="Error!">
Something went wrong. Please try again later.
</app-alert>
The Alert component receives type and title via @Input() for the parts it controls. The message body is completely flexible — whatever the parent puts between the tags goes into <ng-content>. This makes the component reusable for any kind of message.
Chapter 10 — Custom Directives
10.1 — What is a Directive?
A directive is a class that adds behavior to an existing DOM element. Components are actually a special type of directive that have their own template. Regular directives do not have templates — they just modify existing elements.
There are two types of directives you will build:
Attribute directives — change the appearance or behavior of an element. You add them as attributes on existing HTML elements.
Structural directives — change the DOM structure by adding or removing elements. @if and @for are structural directives built into Angular.
Let's build some real custom attribute directives.
10.2 — Building a Highlight Directive
Generate a directive:
ng generate directive highlight --skip-tests
This creates src/app/highlight/highlight.ts:
import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core';
@Directive({
selector: '[appHighlight]' // used as an attribute: <p appHighlight>
})
export class Highlight implements OnInit {
@Input() appHighlight: string = '#ffff00'; // default yellow
@Input() defaultColor: string = 'transparent';
constructor(private el: ElementRef) {
// el.nativeElement is the actual DOM element this directive is on
}
ngOnInit(): void {
this.el.nativeElement.style.backgroundColor = this.defaultColor;
}
@HostListener('mouseenter') onMouseEnter(): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave') onMouseLeave(): void {
this.el.nativeElement.style.backgroundColor = this.defaultColor;
}
}
Let's understand each part:
@Directive({ selector: '[appHighlight]' }) — the selector is in square brackets because it is used as an HTML attribute, not an element tag. When Angular sees appHighlight as an attribute on any element, it activates this directive on that element.
ElementRef — Angular injects this and it gives you direct access to the DOM element the directive is attached to. this.el.nativeElement is the raw DOM element.
@HostListener('mouseenter') — this decorator listens for events on the host element (the element the directive is attached to). When the mouseenter event fires on that element, the decorated method is called automatically.
@Input() appHighlight — the input name matches the directive selector. This is a convention — it lets you pass the highlight color directly on the same attribute: <p appHighlight="#ff0000">.
Now to use this directive, import it in any component:
import { Component } from '@angular/core';
import { Highlight } from './highlight/highlight';
@Component({
selector: 'app-home',
imports: [Highlight],
template: `
<p appHighlight>Hover me — highlights yellow by default</p>
<p appHighlight="#64ffda">Hover me — highlights teal</p>
<p appHighlight="#ff6b6b" defaultColor="#ffe0e0">Hover me — pink highlight, light default</p>
`
})
export class Home { }
10.3 — Building a Click Outside Directive
Here is a more practical directive — detecting when the user clicks outside of an element. This is commonly used for closing dropdowns or modals:
ng generate directive click-outside --skip-tests
src/app/click-outside/click-outside.ts:
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
@Directive({
selector: '[appClickOutside]'
})
export class ClickOutside {
@Output() clickOutside = new EventEmitter<void>();
constructor(private el: ElementRef) {}
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
const clickedInside = this.el.nativeElement.contains(event.target);
if (!clickedInside) {
this.clickOutside.emit();
}
}
}
@HostListener('document:click', ['$event']) — the document: prefix means we are listening on the entire document, not just the host element. This lets us detect clicks anywhere on the page.
this.el.nativeElement.contains(event.target) — this checks whether the element that was clicked (event.target) is inside our host element. If it is not inside, we emit the clickOutside event.
Using it:
import { Component } from '@angular/core';
import { ClickOutside } from './click-outside/click-outside';
@Component({
selector: 'app-dropdown',
imports: [ClickOutside],
template: `
<div class="dropdown" appClickOutside (clickOutside)="closeDropdown()">
<button (click)="toggleDropdown()">Menu ▾</button>
@if (isOpen) {
<div class="dropdown-menu">
<a href="#">Profile</a>
<a href="#">Settings</a>
<a href="#">Logout</a>
</div>
}
</div>
`,
styles: [`
.dropdown { position: relative; display: inline-block; }
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
min-width: 160px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dropdown-menu a {
display: block;
padding: 10px 16px;
color: #333;
text-decoration: none;
}
.dropdown-menu a:hover { background: #f5f5f5; }
`]
})
export class Dropdown {
isOpen: boolean = false;
toggleDropdown(): void {
this.isOpen = !this.isOpen;
}
closeDropdown(): void {
this.isOpen = false;
}
}
10.4 — Building an AutoFocus Directive
A directive that automatically focuses an input when it appears:
ng generate directive auto-focus --skip-tests
src/app/auto-focus/auto-focus.ts:
import { AfterViewInit, Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[appAutoFocus]'
})
export class AutoFocus implements AfterViewInit {
constructor(private el: ElementRef) {}
ngAfterViewInit(): void {
this.el.nativeElement.focus();
}
}
Using it:
<input type="text" appAutoFocus placeholder="This input is focused automatically">
As soon as this input appears in the DOM, the directive focuses it. No JavaScript needed in the component. The directive handles it completely.
Chapter 11 — A Complete Real-World Example
Let's build a Task Manager application that uses every concept from this phase together. It will have a task list with filtering, status toggling, priority badges, and a form to add new tasks.
Step 1 — Create the project
ng new task-manager --style=css
cd task-manager
ng g c task-list --skip-tests
ng g c task-card --skip-tests
ng g c task-form --skip-tests
ng g d priority-color --skip-tests
Step 2 — Task Card Component
src/app/task-card/task-card.ts:
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { NgClass } from '@angular/common';
import { PriorityColor } from '../priority-color/priority-color';
interface Task {
id: number;
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
}
@Component({
selector: 'app-task-card',
imports: [NgClass, PriorityColor],
templateUrl: './task-card.html',
styleUrl: './task-card.css'
})
export class TaskCard {
@Input({ required: true }) task!: Task;
@Output() toggled = new EventEmitter<number>();
@Output() deleted = new EventEmitter<number>();
onToggle(): void {
this.toggled.emit(this.task.id);
}
onDelete(): void {
this.deleted.emit(this.task.id);
}
}
src/app/task-card/task-card.html:
<div class="task-card" [ngClass]="{ 'completed': task.completed }">
<div class="task-header">
<input
type="checkbox"
[checked]="task.completed"
(change)="onToggle()">
<h3 [class.strikethrough]="task.completed">{{ task.title }}</h3>
<span class="priority-badge" [appPriorityColor]="task.priority">
{{ task.priority }}
</span>
</div>
<p class="description">{{ task.description }}</p>
<div class="task-footer">
<span class="status">
{{ task.completed ? '✓ Completed' : '○ Pending' }}
</span>
<button class="delete-btn" (click)="onDelete()">Delete</button>
</div>
</div>
src/app/task-card/task-card.css:
.task-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-left: 4px solid #0070f3;
transition: all 0.2s;
}
.task-card.completed {
opacity: 0.6;
border-left-color: #28a745;
}
.task-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.task-header h3 {
flex: 1;
font-size: 16px;
color: #1a1a2e;
}
.strikethrough {
text-decoration: line-through;
color: #999;
}
.priority-badge {
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.description {
color: #666;
font-size: 14px;
line-height: 1.5;
margin-bottom: 12px;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.status {
font-size: 13px;
color: #888;
}
.delete-btn {
background: none;
border: 1px solid #dc3545;
color: #dc3545;
padding: 4px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.delete-btn:hover {
background: #dc3545;
color: white;
}
Step 3 — Priority Color Directive
src/app/priority-color/priority-color.ts:
import { Directive, Input, ElementRef, OnInit } from '@angular/core';
@Directive({
selector: '[appPriorityColor]'
})
export class PriorityColor implements OnInit {
@Input() appPriorityColor: string = 'low';
constructor(private el: ElementRef) {}
ngOnInit(): void {
const colors: Record<string, { bg: string; text: string }> = {
low: { bg: '#d4edda', text: '#155724' },
medium: { bg: '#fff3cd', text: '#856404' },
high: { bg: '#f8d7da', text: '#721c24' }
};
const colorSet = colors[this.appPriorityColor] || colors['low'];
this.el.nativeElement.style.backgroundColor = colorSet.bg;
this.el.nativeElement.style.color = colorSet.text;
}
}
Step 4 — Task Form Component
src/app/task-form/task-form.ts:
import { Component, Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface NewTask {
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
}
@Component({
selector: 'app-task-form',
imports: [FormsModule],
templateUrl: './task-form.html',
styleUrl: './task-form.css'
})
export class TaskForm {
@Output() taskAdded = new EventEmitter<NewTask>();
newTask: NewTask = {
title: '',
description: '',
priority: 'medium'
};
onSubmit(): void {
if (this.newTask.title.trim() === '') {
return;
}
this.taskAdded.emit({ ...this.newTask });
this.newTask = {
title: '',
description: '',
priority: 'medium'
};
}
}
src/app/task-form/task-form.html:
<div class="form-container">
<h2>Add New Task</h2>
<div class="field">
<label>Title</label>
<input
type="text"
[(ngModel)]="newTask.title"
placeholder="What needs to be done?">
</div>
<div class="field">
<label>Description</label>
<textarea
[(ngModel)]="newTask.description"
placeholder="Add more details..."
rows="3">
</textarea>
</div>
<div class="field">
<label>Priority</label>
<select [(ngModel)]="newTask.priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<button
class="submit-btn"
(click)="onSubmit()"
[disabled]="newTask.title.trim() === ''">
Add Task
</button>
</div>
src/app/task-form/task-form.css:
.form-container {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 32px;
}
h2 {
font-size: 20px;
color: #1a1a2e;
margin-bottom: 20px;
}
.field {
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
font-weight: 600;
color: #555;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
input[type="text"],
textarea,
select {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 15px;
color: #333;
transition: border-color 0.2s;
font-family: inherit;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #0070f3;
}
textarea { resize: vertical; }
.submit-btn {
width: 100%;
padding: 12px;
background: #0070f3;
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.submit-btn:hover { background: #005ac1; }
.submit-btn:disabled { background: #ccc; cursor: not-allowed; }
Step 5 — Task List Component
src/app/task-list/task-list.ts:
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
import { TaskCard } from '../task-card/task-card';
import { TaskForm } from '../task-form/task-form';
interface Task {
id: number;
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
}
@Component({
selector: 'app-task-list',
imports: [TaskCard, TaskForm, NgClass],
templateUrl: './task-list.html',
styleUrl: './task-list.css'
})
export class TaskList {
activeFilter: string = 'all';
tasks: Task[] = [
{ id: 1, title: 'Learn Angular Data Binding', description: 'Study interpolation, property binding, event binding, and two-way binding.', priority: 'high', completed: true },
{ id: 2, title: 'Build a Task Manager App', description: 'Create a complete task manager using components, directives, and data binding.', priority: 'high', completed: false },
{ id: 3, title: 'Read RxJS Documentation', description: 'Understand Observables, Subjects, and common operators.', priority: 'medium', completed: false },
{ id: 4, title: 'Practice TypeScript Generics', description: 'Review generic functions, interfaces, and classes.', priority: 'low', completed: false }
];
get filteredTasks(): Task[] {
switch (this.activeFilter) {
case 'active':
return this.tasks.filter(t => !t.completed);
case 'completed':
return this.tasks.filter(t => t.completed);
default:
return this.tasks;
}
}
get completedCount(): number {
return this.tasks.filter(t => t.completed).length;
}
get pendingCount(): number {
return this.tasks.filter(t => !t.completed).length;
}
setFilter(filter: string): void {
this.activeFilter = filter;
}
onTaskAdded(newTask: { title: string; description: string; priority: 'low' | 'medium' | 'high' }): void {
const task: Task = {
id: Date.now(),
...newTask,
completed: false
};
this.tasks = [...this.tasks, task];
}
onTaskToggled(taskId: number): void {
this.tasks = this.tasks.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
);
}
onTaskDeleted(taskId: number): void {
this.tasks = this.tasks.filter(task => task.id !== taskId);
}
}
src/app/task-list/task-list.html:
<div class="task-manager">
<div class="header">
<h1>Task Manager</h1>
<div class="stats">
<span class="stat">{{ pendingCount }} Pending</span>
<span class="divider">|</span>
<span class="stat done">{{ completedCount }} Completed</span>
</div>
</div>
<app-task-form (taskAdded)="onTaskAdded($event)"></app-task-form>
<div class="filters">
@for (filter of ['all', 'active', 'completed']; track filter) {
<button
[ngClass]="{ 'active': activeFilter === filter }"
(click)="setFilter(filter)">
{{ filter | titlecase }}
</button>
}
</div>
<div class="tasks">
@for (task of filteredTasks; track task.id) {
<app-task-card
[task]="task"
(toggled)="onTaskToggled($event)"
(deleted)="onTaskDeleted($event)">
</app-task-card>
} @empty {
<div class="empty-state">
<p>No tasks found. Add one above!</p>
</div>
}
</div>
</div>
src/app/task-list/task-list.css:
.task-manager {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28px;
}
h1 {
font-size: 32px;
font-weight: 700;
color: #1a1a2e;
}
.stats {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.stat.done { color: #28a745; }
.divider { color: #ddd; }
.filters {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.filters button {
padding: 8px 20px;
border: 1px solid #ddd;
background: white;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
color: #555;
}
.filters button.active {
background: #0070f3;
color: white;
border-color: #0070f3;
}
.tasks {
display: flex;
flex-direction: column;
gap: 16px;
}
.empty-state {
text-align: center;
padding: 48px;
color: #999;
background: white;
border-radius: 12px;
}
Step 6 — Wire it in app.ts
src/app/app.ts:
import { Component } from '@angular/core';
import { TaskList } from './task-list/task-list';
@Component({
selector: 'app-root',
imports: [TaskList],
template: `<app-task-list></app-task-list>`
})
export class App { }
src/styles.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: #f0f2f5;
min-height: 100vh;
}
Run ng serve -o. You now have a fully working Task Manager that demonstrates:
Interpolation displaying task titles, descriptions, and counts. Property binding with [checked], [disabled], [class.strikethrough], and [ngClass]. Event binding with (click), (change) on checkboxes. Two-way binding with [(ngModel)] on the form inputs. @for rendering the task list and filter buttons. @if for showing the completed state. @empty for the empty state. @switch equivalent logic in the filter getter. A custom directive PriorityColor that dynamically styles priority badges. Parent-child communication using @Input() and @Output() throughout.
Phase 4 — Complete Summary
Here is everything you learned in this phase:
Interpolation {{ }} — Displays TypeScript values in the template. Can evaluate expressions, call methods, and use operators. One-way from TypeScript to template.
Property Binding [ ] — Sets DOM element properties to TypeScript values. Use square brackets when the value is dynamic. Works with any DOM property — [disabled], [src], [href], [class.name], [style.property]. Use [attr.name] for HTML attributes that have no DOM property equivalent.
Event Binding ( ) — Listens for browser events and calls TypeScript methods. Use $event to access the native event object. Works with any DOM event — (click), (keyup), (input), (submit), (mouseenter). Angular-specific key filters like (keyup.enter) are a useful shortcut.
Two-Way Binding [( )] — Combines property and event binding. [(ngModel)] keeps TypeScript properties and HTML form inputs perfectly in sync. Requires FormsModule in the component's imports.
@if — Conditionally renders HTML blocks. Supports @else if and @else blocks. Removes elements from the DOM completely when the condition is false.
@for — Renders a block for each item in an array. The track expression is required and should use a unique identifier. Built-in variables: $index, $first, $last, $even, $odd, $count.
@empty — Renders when the @for array is empty. Eliminates the need for a separate empty state check.
@switch — Renders based on one value matching multiple cases. Cleaner than a long chain of @else if when checking a single value.
NgClass — Applies multiple CSS classes conditionally using an object, array, or string.
NgStyle — Applies multiple inline styles dynamically using an object.
ng-template — Defines reusable template blocks that are not rendered by default. Used with template reference variables.
ng-container — An invisible grouping element that adds nothing to the DOM. Useful for applying directives without adding wrapper elements.
ng-content — Projects content from outside into a component's template. The foundation of reusable wrapper components.
Custom Directives — Add behavior to existing DOM elements. ElementRef gives access to the native element. @HostListener listens to events on the host element. @Input() passes configuration to the directive.
What's Next — Phase 5
In Phase 5 we go into Services and Dependency Injection — one of Angular's most powerful features:
What a service is and why you need it. How to create services and inject them into components. How Angular's Dependency Injection system works under the hood. How to share data between components using a service. How to scope services — app-wide, component-level. Using services as a simple state management solution with signals.