Practice Angular
You’ve read the lessons — now prove it. Take the quiz for instant feedback, then work the coding exercises. Try each one yourself before you reveal the solution.
Quick quiz
16 questions- 1
Which decorator marks a class as an Angular component?
- 2
What syntax is used for interpolation in an Angular template?
- 3
Which binding syntax sets a DOM property based on a component expression?
- 4
Which binding syntax listens to a DOM event and calls a component method?
- 5
What does
[(ngModel)]syntax represent? - 6
Which structural directive conditionally includes or excludes an element from the DOM?
- 7
Given
<li *ngFor="let item of items; let i = index">, what doesihold? - 8
How do you make a service available application-wide without adding it to a module's
providersarray? - 9
Which Angular lifecycle hook is called once after the component's first
ngOnChangesand after the view initializes? - 10
What does the
asyncpipe do when used with an Observable in a template? - 11
Which RxJS operator should you use for a live-search typeahead so that in-flight HTTP requests are cancelled when the user types a new character?
- 12
What is the correct way to create a functional HTTP interceptor in Angular 15+?
- 13
A route guard returns a
UrlTree. What does Angular do? - 14
Which
@Componentproperty enables OnPush change detection? - 15
What is the purpose of
<ng-content select="[slot=footer]">in a component's template? - 16
In Angular Signals, what does
computed(() => count() * 2)return?
Coding exercises
8 tasksCounter Component
EasyCreate an Angular component called CounterComponent with selector app-counter.
It should display a count (starting at 0) using interpolation, and have two buttons: one labelled Increment that increases the count by 1, and one labelled Decrement that decreases it by 1.
Use event binding (click) to wire the buttons to methods on the class.
import { Component } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<!-- your template here -->
`
})
export class CounterComponent {
count = 0;
// add increment() and decrement() methods
}
💡 Show hint
Bind each button's (click) event to a method. Use {{count}} to display the value.
✅ Show solution
import { Component } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<p>Count: {{count}}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
`
})
export class CounterComponent {
count = 0;
increment(): void {
this.count++;
}
decrement(): void {
this.count--;
}
}
Todo List with *ngFor and *ngIf
EasyBuild a TodoListComponent (selector app-todo-list) that:
1. Has an array of todo strings, e.g. ["Buy milk", "Walk dog", "Read book"].
2. Renders each item in an <ul> using *ngFor.
3. Shows a paragraph No todos yet! using *ngIf when the array is empty.
4. Has an input bound with [(ngModel)] and an Add button that pushes the input value onto the array and clears the input.
Remember to import FormsModule in your module (or add it to imports if using standalone components) for ngModel to work.
💡 Show hint
Use [(ngModel)]="newTodo" on the input. In the Add button's click handler, call this.todos.push(this.newTodo) then reset this.newTodo = "".
✅ Show solution
import { Component } from "@angular/core";
@Component({
selector: "app-todo-list",
template: `
<ul>
<li *ngFor="let todo of todos">{{todo}}</li>
</ul>
<p *ngIf="todos.length === 0">No todos yet!</p>
<input [(ngModel)]="newTodo" placeholder="New todo" />
<button (click)="addTodo()">Add</button>
`
})
export class TodoListComponent {
todos: string[] = ["Buy milk", "Walk dog", "Read book"];
newTodo = "";
addTodo(): void {
const trimmed = this.newTodo.trim();
if (trimmed) {
this.todos.push(trimmed);
this.newTodo = "";
}
}
}
Temperature Converter Service
MediumCreate a TemperatureService with providedIn: 'root' that exposes two methods:
- celsiusToFahrenheit(c: number): number
- fahrenheitToCelsius(f: number): number
Then create a TemperatureConverterComponent (selector app-temp-converter) that injects TemperatureService through the constructor and provides a simple UI: a numeric input, two radio buttons to choose the conversion direction, and a Convert button that displays the result.
💡 Show hint
Inject the service via constructor(private tempService: TemperatureService) {}. The formulas are F = C * 9/5 + 32 and C = (F - 32) * 5/9.
✅ Show solution
import { Injectable } from "@angular/core";
import { Component } from "@angular/core";
@Injectable({ providedIn: "root" })
export class TemperatureService {
celsiusToFahrenheit(c: number): number {
return c * 9 / 5 + 32;
}
fahrenheitToCelsius(f: number): number {
return (f - 32) * 5 / 9;
}
}
@Component({
selector: "app-temp-converter",
template: `
<input type="number" [(ngModel)]="inputValue" placeholder="Enter value" />
<label>
<input type="radio" name="dir" value="cToF" [(ngModel)]="direction" /> °C → °F
</label>
<label>
<input type="radio" name="dir" value="fToC" [(ngModel)]="direction" /> °F → °C
</label>
<button (click)="convert()">Convert</button>
<p *ngIf="result !== null">Result: {{result | number:"1.2-2"}}</p>
`
})
export class TemperatureConverterComponent {
inputValue: number | null = null;
direction: "cToF" | "fToC" = "cToF";
result: number | null = null;
constructor(private tempService: TemperatureService) {}
convert(): void {
if (this.inputValue === null) return;
this.result =
this.direction === "cToF"
? this.tempService.celsiusToFahrenheit(this.inputValue)
: this.tempService.fahrenheitToCelsius(this.inputValue);
}
}
GitHub User Search with HttpClient and async pipe
MediumCreate a GithubSearchComponent (selector app-github-search) that:
1. Has a text input for a GitHub username, bound with [(ngModel)].
2. On a Search button click, calls the GitHub public API https://api.github.com/users/{username} using HttpClient.get<any>().
3. Stores the result as an Observable assigned to a property user$.
4. Displays the user's avatar_url (as an <img>), login, and public_repos in the template using the async pipe — no manual subscription.
5. Shows a loading indicator while the request is in flight using *ngIf and the async pipe's as syntax.
Don't forget to provide HttpClientModule in your module (or provideHttpClient() for standalone).
💡 Show hint
Assign this.user$ = this.http.get<any>(url) in the search method. In the template use *ngIf="user$ | async as user" to unwrap once and access user.login, user.avatar_url, etc.
✅ Show solution
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
interface GithubUser {
login: string;
avatar_url: string;
public_repos: number;
}
@Component({
selector: "app-github-search",
template: `
<input [(ngModel)]="username" placeholder="GitHub username" />
<button (click)="search()">Search</button>
<div *ngIf="user$ | async as user; else loading">
<img [src]="user.avatar_url" [alt]="user.login" width="80" />
<h2>{{user.login}}</h2>
<p>Public repos: {{user.public_repos}}</p>
</div>
<ng-template #loading>
<p *ngIf="user$">Loading...</p>
</ng-template>
`
})
export class GithubSearchComponent {
username = "";
user$: Observable<GithubUser> | null = null;
constructor(private http: HttpClient) {}
search(): void {
if (!this.username.trim()) return;
this.user$ = this.http.get<GithubUser>(
`https://api.github.com/users/${this.username.trim()}`
);
}
}
Paginated Data Table with Routing and Lifecycle Hooks
HardBuild a ProductListComponent (selector app-product-list) that demonstrates several Angular concepts together:
1. Routing params — inject ActivatedRoute and read a page query parameter (default 1) using ngOnInit.
2. HttpClient — fetch products from https://dummyjson.com/products?limit=10&skip={(page-1)*10} in ngOnInit whenever page changes. Use switchMap from RxJS to cancel previous requests.
3. Template — display a table with columns id, title, and price (use the built-in currency pipe for price). Use *ngFor to iterate rows.
4. Pagination — show Previous and Next buttons that navigate to ?page=N using Router.navigate. Disable Previous on page 1.
5. Cleanup — unsubscribe in ngOnDestroy to avoid memory leaks.
Define a Product interface with id: number, title: string, and price: number.
💡 Show hint
Subscribe to this.route.queryParamMap in ngOnInit, use switchMap to derive the HTTP call, and store the subscription. In ngOnDestroy call this.sub.unsubscribe(). Use [disabled]="currentPage === 1" for the Previous button.
✅ Show solution
import { Component, OnInit, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { HttpClient } from "@angular/common/http";
import { Subscription } from "rxjs";
import { switchMap, map } from "rxjs/operators";
interface Product {
id: number;
title: string;
price: number;
}
interface ProductsResponse {
products: Product[];
total: number;
}
@Component({
selector: "app-product-list",
template: `
<table>
<thead>
<tr>
<th>ID</th><th>Title</th><th>Price</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let p of products">
<td>{{p.id}}</td>
<td>{{p.title}}</td>
<td>{{p.price | currency}}</td>
</tr>
</tbody>
</table>
<button [disabled]="currentPage === 1" (click)="goToPage(currentPage - 1)">Previous</button>
<span> Page {{currentPage}} </span>
<button (click)="goToPage(currentPage + 1)">Next</button>
`
})
export class ProductListComponent implements OnInit, OnDestroy {
products: Product[] = [];
currentPage = 1;
private sub!: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router,
private http: HttpClient
) {}
ngOnInit(): void {
this.sub = this.route.queryParamMap
.pipe(
map((params) => Number(params.get("page") ?? "1") || 1),
switchMap((page) => {
this.currentPage = page;
const skip = (page - 1) * 10;
return this.http.get<ProductsResponse>(
`https://dummyjson.com/products?limit=10&skip=${skip}`
);
})
)
.subscribe((data) => {
this.products = data.products;
});
}
goToPage(page: number): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page },
queryParamsHandling: "merge"
});
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
}
Auth Guard with Redirect
MediumImplement a functional route guard called authGuard (typed as CanActivateFn) that:
1. Injects AuthService (assume it has a isLoggedIn(): boolean method) and Router.
2. Returns true if the user is logged in.
3. Otherwise redirects to /login, preserving the intended URL as a returnUrl query parameter.
Also show how to register it on a dashboard route in app.routes.ts.
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "../services/auth.service";
export const authGuard: CanActivateFn = (route, state) => {
// implement guard here
};
💡 Show hint
Use router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }) to redirect and preserve the intended destination.
✅ Show solution
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "../services/auth.service";
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(["/login"], {
queryParams: { returnUrl: state.url }
});
};
// app.routes.ts
import { Routes } from "@angular/router";
import { authGuard } from "./guards/auth.guard";
export const routes: Routes = [
{ path: "", loadComponent: () => import("./home/home.component").then(m => m.HomeComponent) },
{ path: "login", loadComponent: () => import("./login/login.component").then(m => m.LoginComponent) },
{
path: "dashboard",
canActivate: [authGuard],
loadComponent: () => import("./dashboard/dashboard.component").then(m => m.DashboardComponent)
}
];
HTTP Interceptor: Bearer Token + Global Error Handling
MediumWrite two functional interceptors:
1. authInterceptor — clones every outgoing request and adds an Authorization: Bearer <token> header. Get the token from an injected TokenService with a getToken(): string | null method. Skip the header if getToken() returns null.
2. errorInterceptor — catches HttpErrorResponse errors. If status is 401, navigate to /login. Always re-throw the error so callers can still handle it.
Finally, show how to register both in main.ts using provideHttpClient(withInterceptors([...])).
💡 Show hint
Requests are immutable — use req.clone({ setHeaders: { ... } }). In the error interceptor, pipe next(req) through catchError.
✅ Show solution
import { HttpInterceptorFn, HttpErrorResponse } from "@angular/common/http";
import { inject } from "@angular/core";
import { Router } from "@angular/router";
import { catchError, throwError } from "rxjs";
import { TokenService } from "./services/token.service";
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(TokenService).getToken();
if (!token) return next(req);
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) router.navigate(["/login"]);
return throwError(() => err);
})
);
};
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideHttpClient, withInterceptors } from "@angular/common/http";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))
]
});
Reusable Card with Multi-Slot Content Projection
HardCreate a CardComponent (selector app-card, standalone) that supports three named content projection slots:
- slot="header" — rendered in a .card-header div
- slot="footer" — rendered in a .card-footer div
- Default slot — rendered in a .card-body div
Then create a ProfileCardComponent that uses CardComponent with all three slots populated: a heading in the header, a paragraph bio and an image in the body, and two buttons in the footer.
Finally, add ChangeDetectionStrategy.OnPush to CardComponent and explain in a comment why it is safe to do so here.
💡 Show hint
Use <ng-content select="[slot=header]" /> for named slots and bare <ng-content /> for the default slot. OnPush is safe because CardComponent has no mutable @Input() — its content is controlled entirely by projection.
✅ Show solution
// card.component.ts
import { Component, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: "app-card",
standalone: true,
// OnPush is safe: CardComponent has no @Input() bindings that mutate.
// Angular only needs to check it when projected content changes,
// which happens when the parent re-renders.
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[slot=header]" />
</div>
<div class="card-body">
<ng-content />
</div>
<div class="card-footer">
<ng-content select="[slot=footer]" />
</div>
</div>
`,
styles: [".card { border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }"]
})
export class CardComponent {}
// profile-card.component.ts
import { Component } from "@angular/core";
import { CardComponent } from "./card.component";
@Component({
selector: "app-profile-card",
standalone: true,
imports: [CardComponent],
template: `
<app-card>
<h2 slot="header">Jane Doe</h2>
<p>Full-stack developer specialising in Angular and Node.js.</p>
<img src="/assets/jane.jpg" alt="Jane Doe" width="120" />
<div slot="footer">
<button>Follow</button>
<button>Message</button>
</div>
</app-card>
`
})
export class ProfileCardComponent {}