CPCODELAB
Practice

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
Score: 0 / 16
0/16 answered
  1. 1

    Which decorator marks a class as an Angular component?

  2. 2

    What syntax is used for interpolation in an Angular template?

  3. 3

    Which binding syntax sets a DOM property based on a component expression?

  4. 4

    Which binding syntax listens to a DOM event and calls a component method?

  5. 5

    What does [(ngModel)] syntax represent?

  6. 6

    Which structural directive conditionally includes or excludes an element from the DOM?

  7. 7

    Given <li *ngFor="let item of items; let i = index">, what does i hold?

  8. 8

    How do you make a service available application-wide without adding it to a module's providers array?

  9. 9

    Which Angular lifecycle hook is called once after the component's first ngOnChanges and after the view initializes?

  10. 10

    What does the async pipe do when used with an Observable in a template?

  11. 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. 12

    What is the correct way to create a functional HTTP interceptor in Angular 15+?

  13. 13

    A route guard returns a UrlTree. What does Angular do?

  14. 14

    Which @Component property enables OnPush change detection?

  15. 15

    What is the purpose of <ng-content select="[slot=footer]"> in a component's template?

  16. 16

    In Angular Signals, what does computed(() => count() * 2) return?

🛠️

Coding exercises

8 tasks
1

Counter Component

Easy

Create 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.

Starter
ts
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
ts
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--;
  }
}
2

Todo List with *ngFor and *ngIf

Easy

Build 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
ts
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 = "";
    }
  }
}
3

Temperature Converter Service

Medium

Create 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
ts
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);
  }
}
4

GitHub User Search with HttpClient and async pipe

Medium

Create 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
ts
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()}`
    );
  }
}
5

Paginated Data Table with Routing and Lifecycle Hooks

Hard

Build 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
ts
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();
  }
}
6

Auth Guard with Redirect

Medium

Implement 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.

Starter
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
ts
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)
  }
];
7

HTTP Interceptor: Bearer Token + Global Error Handling

Medium

Write 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
ts
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]))
  ]
});
8

Reusable Card with Multi-Slot Content Projection

Hard

Create 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
ts
// 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 {}