View on GitHub

SCRATCH PAD

No polish, just pulse

Angular Notes

Course Sections

  1. Getting Started
  2. Angular Essentials - Components, Templates, Services & More
  3. Angular Essentials - Working with Modules (The “Legacy” Way)
  4. Angular Essentials - Time to Practice
  5. Debugging Angular Apps
  6. Components & Templates - Deep Dive
  7. Directives - Deep Dive
  8. Transforming Values with Pipes - Deep Dive
  9. Understanding Services & Dependency Injection - Deep Dive
  10. Making Sense of Change Detection - Deep Dive
  11. Working with RxJS (Observables) - Deep Dive
  12. Sending HTTP Requests & Handling Responses
  13. Handling User Input & Working with Forms
  14. Routing & Building Multi-page Single Page Applications
  15. Code Splitting & Deferrable Views
  16. Authentication

Getting Started & The Component Basics

1. What is Angular?

2. Project Structure (The Essentials)

When you create a new project (via ng new), the core files are:


3. Creating a First Custom Component

Angular applications are built as a tree of components. To create a component manually, you follow these requirements from the course:

A. The Class

You define a normal TypeScript class to store data and logic.

export class HeaderComponent {
  // Logic goes here
}


B. The @Component Decorator

You must “decorate” the class to tell Angular it’s not just a class, but a component. This requires an import from @angular/core.

Example Syntax:

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

@Component({
  selector: 'app-header',
  standalone: true,
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {}


C. Using the Component

To use your new component in another component (like app.component.ts), you must:

  1. Import the class at the top of the file.
  2. Add it to the imports: [] array of the receiving component’s decorator.
  3. Use the selector in the HTML (e.g., <app-header />).

4. Working with the Angular CLI

The course emphasizes using the Command Line Interface to automate creation:


Outputting Dynamic Content & Binding

1. String Interpolation

Example:

// component.ts
export class UserComponent {
  selectedUser = { name: 'Jasmine' };
}


<p>Hello, </p>



2. Property Binding

Example:

<img [src]="userImagePath" [alt]="userName" />


Note: Using src="" also works, but [src] is the preferred “Property Binding” syntax taught in the course.


3. Binding to Event Listeners (Event Binding)

Example:

<button (click)="onSelectUser()">Click Me</button>



4. Working with Assets

The course explains how to manage static files (images, icons):


5. Concept: State & Reactivity (Intro)

The course introduces the idea that Angular needs to know when something changes to update the UI:


Section 2: Angular Essentials - Components, Templates, Services & More

1. Module Introduction

2. A New Starting Project & Analyzing The Project Structure

3. Understanding Components & How Content Ends Up On The Screen

4. Creating a First Custom Component

export class HeaderComponent {}


5. [Optional] JavaScript Refresher: Classes, Properties & More

6. Configuring the Custom Component

7. Using the Custom Component

8. Styling the Header Component & Adding An Image

9. Managing & Creating Components with the Angular CLI

10. Styling & Using Our Next Custom Component

11. Preparing User Data (To Output Dynamic Content)

export const DUMMY_USERS = [
  {
    id: 'u1',
    name: 'Jasmine Washington',
    avatar: 'user-1.jpg',
  },
  // ... more users
];


12. Storing Data in a Component Class

import { DUMMY_USERS } from './dummy-users';

const randomIndex = Math.floor(Math.random() * DUMMY_USERS.length);

export class UserComponent {
  selectedUser = DUMMY_USERS[randomIndex];
}


13. Outputting Dynamic Content with String Interpolation

<span></span>


14. Property Binding & Outputting Computed Values

<img [src]="'users/' + selectedUser.avatar" [alt]="selectedUser.name" />


15. Attribute Binding

16. Using Getters For Computed Values

get imagePath() {
  return 'assets/users/' + this.selectedUser.avatar;
}


<img [src]="imagePath" />


(Angular treats the getter like a property).

17. Listening to Events with Event Binding

<button (click)="onSelectUser()">User Name</button>


export class UserComponent {
  onSelectUser() {
    console.log('Clicked!');
  }
}


18. Managing State & Changing Data

onSelectUser() {
    const randomIndex = Math.floor(Math.random() * DUMMY_USERS.length);
    this.selectedUser = DUMMY_USERS[randomIndex];
}


19. A Look Behind The Scenes Of Angular’s Change Detection Mechanism


20. Introducing Signals

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

export class UserComponent {
  // Create a signal with an initial value
  selectedUser = signal(DUMMY_USERS[0]); 

  onSelectUser() {
    // Update the signal
    const randomIndex = Math.floor(Math.random() * DUMMY_USERS.length);
    this.selectedUser.set(DUMMY_USERS[randomIndex]);
  }
}


<p></p>


import { computed } from '@angular/core';
// ...
imagePath = computed(() => 'assets/users/' + this.selectedUser().avatar);


21. We Need More Flexible Components!

22. Defining Component Inputs (Standard Approach)

import { Input } from '@angular/core';

export class UserComponent {
  @Input() avatar!: string; // '!' tells TS this will be set eventually
  @Input() name!: string;
}


<app-user [avatar]="users[0].avatar" [name]="users[0].name" />


23. Required & Optional Inputs

@Input({ required: true }) avatar!: string;


If the parent fails to provide [avatar], Angular throws an error during compilation.

24. Using Signal Inputs (Modern Approach)

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

export class UserComponent {
  // Inputs are read-only signals
  avatar = input.required<string>(); 
  name = input.required<string>(); 

  // Computing based on input is easy
  imagePath = computed(() => 'assets/users/' + this.avatar());
}


25. We Need Custom Events! (Outputs)

26. Working with Outputs & Emitting Data (Standard Approach)

@Output() select = new EventEmitter<string>(); // Emits a string (id)


  1. Emit:
onSelectUser() {
  this.select.emit(this.id);
}


  1. Listen (Parent):
<app-user (select)="onSelectUser($event)" />


$event contains the data emitted (the ID).

27. Using the output() Function (Modern Approach)

import { output } from '@angular/core';

export class UserComponent {
  select = output<string>(); // No 'new EventEmitter' needed

  onSelectUser() {
    this.select.emit(this.id);
  }
}


28. TypeScript: Working With Potentially Undefined Values & Union Types

selectedUserId?: string; // string | undefined


29. Accepting Objects As Inputs & Adding Appropriate Typings

30. TypeScript: Type Aliases & Interfaces

export interface User {
  id: string;
  name: string;
  avatar: string;
}


@Input({ required: true }) user!: User;


export type User = {
  id: string;
  name: string;
  avatar: string;
};


31. Outputting List Content (@for)

<ul>
  @for (user of users; track user.id) {
    <li>
      <app-user [user]="user" (select)="onSelectUser($event)" />
    </li>
  }
</ul>


32. Outputting Conditional Content (@if)

@if (selectedUser) {
  <app-tasks [name]="selectedUser.name" />
} @else {
  <p>Select a user to see their tasks!</p>
}


33. Legacy Angular: Using ngFor & ngIf

34. Outputting User-specific Tasks

35. Storing Data Models in Separate Files

36. Dynamic CSS Styling with Class Bindings

<button [class.active]="selected" (click)="onSelectUser()">



37. More Component Communication: Deleting Tasks

38. Creating & Conditionally Rendering Another Component

@if (isAddingTask) {
  <app-new-task (cancel)="onCancelAddTask()" />
}


39. Using Directives & Two-Way-Binding

<input type="text" [(ngModel)]="enteredTitle" />


40. Signals & Two-Way-Binding

41. Handling Form Submission

<form (ngSubmit)="onSubmit()">
  <button type="submit">Create</button>
</form>


42. Content Projection with ng-content

<app-card>
  <form>...</form>
</app-card>


43. Transforming Template Data with Pipes

<p></p>



44. Getting Started with Services

45. Getting Started with Dependency Injection (DI)

@Injectable({ providedIn: 'root' })
export class TasksService { ... }


export class TasksComponent {
  constructor(private tasksService: TasksService) {}
}


46. Alternative Dependency Injection Mechanism (inject function)

import { inject } from '@angular/core';

export class TasksComponent {
  private tasksService = inject(TasksService);
}


47. Managing Data in the Service

48. Using localStorage for Data Storage


Section 3: Angular Essentials - Working with Modules

1. What are Modules?

2. The Root Module (AppModule)

3. Converting a Standalone Component to a Module-based Component

To make a component work with Modules, you must change its configuration:

  1. Remove standalone: true from the @Component decorator.
  2. Remove the imports: [...] array from the @Component decorator.
    • Why? Because imports will now be handled by the Module, not the component.

Example (Module-based Component):

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
  // No 'standalone: true'
  // No 'imports'
})
export class HeaderComponent {}


4. Anatomy of an NgModule

The @NgModule decorator takes a metadata object with four key arrays:

A. declarations

declarations: [AppComponent, HeaderComponent, UserComponent]


B. imports

imports: [BrowserModule, FormsModule]


C. exports

D. bootstrap

5. Creating a Shared Module

6. Main.ts Refactoring

7. Resolving Dependencies (The “Zone” of the Module)

8. Services in Modules


Section 4: Angular Essentials - Time to Practice

1. Module Introduction

2. Problem Statement & Challenge

3. Creating the Header Component

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

@Component({
  selector: 'app-header',
  standalone: true,
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {}


<header>
  <img src="assets/investment-calculator-logo.png" alt="Green graph" />
  <h1>Investment Calculator</h1>
</header>


4. Creating the User Input Component

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

@Component({
  selector: 'app-user-input',
  standalone: true,
  templateUrl: './user-input.component.html',
  styleUrls: ['./user-input.component.css']
})
export class UserInputComponent {}


5. Managing User Input State (Two-Way Binding)

import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-input',
  standalone: true,
  imports: [FormsModule], // Required for ngModel
  templateUrl: './user-input.component.html',
  styleUrls: ['./user-input.component.css']
})
export class UserInputComponent {
  // Using Signals for state
  enteredInitialInvestment = signal('0');
  enteredAnnualInvestment = signal('0');
  enteredExpectedReturn = signal('5');
  enteredDuration = signal('10');
}


<section id="user-input">
  <div class="input-group">
    <p>
      <label>Initial Investment</label>
      <input type="number" [(ngModel)]="enteredInitialInvestment" />
    </p>
    <p>
      <label>Annual Investment</label>
      <input type="number" [(ngModel)]="enteredAnnualInvestment" />
    </p>
    </div>
</section>


6. Submitting the Form

<form (ngSubmit)="onSubmit()">
   <button type="submit">Calculate</button>
</form>


onSubmit() {
  // Logic to handle submission will go here
  console.log('Submitted!');
  console.log(this.enteredInitialInvestment());
}


7. Defining a Data Model for the Investment Input

export interface InvestmentInput {
  initialInvestment: number;
  duration: number;
  expectedReturn: number;
  annualInvestment: number;
}


8. Outputting Data to the Parent Component (Option 1: Component Output)

import { Component, output, signal } from '@angular/core';
import { InvestmentInput } from './investment-input.model';

export class UserInputComponent {
  // Create an output event
  calculate = output<InvestmentInput>();

  // Signals for inputs...

  onSubmit() {
    // Emit the object converting strings to numbers
    this.calculate.emit({
      initialInvestment: +this.enteredInitialInvestment(),
      duration: +this.enteredDuration(),
      expectedReturn: +this.enteredExpectedReturn(),
      annualInvestment: +this.enteredAnnualInvestment()
    });
  }
}



9. Receiving Data in the App Component

<app-user-input (calculate)="onCalculateInvestmentResults($event)" />


import { Component } from '@angular/core';
import { InvestmentInput } from './investment-input.model';

@Component({ ... })
export class AppComponent {
  onCalculateInvestmentResults(data: InvestmentInput) {
    // Calculation logic will go here
    console.log(data);
  }
}


10. The Calculation Logic (Provided Code)

resultsData?: {
  year: number;
  interest: number;
  valueEndOfYear: number;
  annualInvestment: number;
  totalInterest: number;
  totalAmountInvested: number;
}[];

onCalculateInvestmentResults(data: InvestmentInput) {
  const { initialInvestment, annualInvestment, expectedReturn, duration } = data;
  const annualData = [];
  let investmentValue = initialInvestment;

  for (let i = 0; i < duration; i++) {
    const year = i + 1;
    const interestEarnedInYear = investmentValue * (expectedReturn / 100);
    investmentValue += interestEarnedInYear + annualInvestment;
    const totalInterest =
      investmentValue - annualInvestment * year - initialInvestment;

    annualData.push({
      year: year,
      interest: interestEarnedInYear,
      valueEndOfYear: investmentValue,
      annualInvestment: annualInvestment,
      totalInterest: totalInterest,
      totalAmountInvested: initialInvestment + annualInvestment * year,
    });
  }

  this.resultsData = annualData;
}


11. Creating the Investment Results Component

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

@Component({
  selector: 'app-investment-results',
  standalone: true,
  templateUrl: './investment-results.component.html',
  styleUrls: ['./investment-results.component.css']
})
export class InvestmentResultsComponent {
  // Receive the results array from the parent
  // Note: We use 'results' alias for cleaner template code if desired, 
  // but here we stick to the property name.
  results = input<{
    year: number;
    interest: number;
    valueEndOfYear: number;
    annualInvestment: number;
    totalInterest: number;
    totalAmountInvested: number;
  }[]>(); 
}


12. Displaying the Results with Control Flow (@for & @if)

@if (!results()) {
  <p class="center">Please enter some values and press "Calculate".</p>
} @else {
  <table>
    <thead>
      <tr>
        <th>Year</th>
        <th>Investment Value</th>
        <th>Interest (Year)</th>
        <th>Total Interest</th>
        <th>Invested Capital</th>
      </tr>
    </thead>
    <tbody>
      @for (result of results(); track result.year) {
        <tr>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      }
    </tbody>
  </table>
}


13. Formatting with Pipes (Currency Pipe)

<td></td>
<td></td>


14. Refactoring: Using a Service

Step A: Create the Service ng g s investment

import { Injectable, signal } from '@angular/core';
import { InvestmentInput } from './investment-input.model';

@Injectable({ providedIn: 'root' })
export class InvestmentService {
  // Store results as a signal so components can react
  resultData = signal<{...}[] | undefined>(undefined);

  calculateInvestmentResults(data: InvestmentInput) {
    // ... paste the calculation logic here ...
    // Update the signal at the end
    this.resultData.set(annualData);
  }
}


Step B: Update UserInputComponent (Send Data to Service) Instead of emitting an event to the parent, we call the service directly.

import { inject } from '@angular/core';
import { InvestmentService } from './investment.service';

export class UserInputComponent {
  private investmentService = inject(InvestmentService);

  onSubmit() {
    this.investmentService.calculateInvestmentResults({
      initialInvestment: +this.enteredInitialInvestment(),
      // ...
    });
  }
}


Step C: Update ResultsComponent (Read Data from Service) Instead of receiving data via @Input, we read the signal from the service.

import { inject, computed } from '@angular/core';
import { InvestmentService } from './investment.service';

export class InvestmentResultsComponent {
  private investmentService = inject(InvestmentService);
  
  // Create a computed property to expose the service's signal
  results = computed(() => this.investmentService.resultData());
  // Or simply access it directly in the template via a getter
}



Migrating to Angular Modules

1. Create the Root Module (app.module.ts)

You must create a new file src/app/app.module.ts. This will act as the central registry for your application.

Code Example:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { HeaderComponent } from './header.component';
import { UserInputComponent } from './user-input.component';
import { InvestmentResultsComponent } from './investment-results.component';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    UserInputComponent,
    InvestmentResultsComponent
  ],
  imports: [BrowserModule, FormsModule],
  bootstrap: [AppComponent]
})
export class AppModule {}


2. Update Component Decorators

You must go into every single component file (app.component.ts, header.component.ts, etc.) and strip out the “Standalone” configuration.

Before (Standalone):

@Component({
  selector: 'app-user-input',
  standalone: true,
  imports: [FormsModule],
  templateUrl: '...'
})
export class UserInputComponent {}


After (Module-based):

@Component({
  selector: 'app-user-input',
  // standalone: true <-- REMOVED
  // imports: [FormsModule] <-- REMOVED
  templateUrl: '...'
})
export class UserInputComponent {}


3. Update main.ts (Bootstrap Logic)

The entry point must change. Instead of bootstrapping a component, you now bootstrap a module.

Code Example:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);


4. Handling Common Directives & Pipes

In the Standalone version, the InvestmentResultsComponent likely imported CurrencyPipe (or CommonModule).


Section 5: Debugging Angular Apps

1. Module Introduction

2. Reading & Understanding Error Messages

3. Debugging Logic Errors with the Browser DevTools

4. Using the Angular DevTools Extension


Section 6: Components & Templates

1. Module Introduction

2. Splitting Apps into Components

3. Component Selectors - Deep Dive

selector: 'app-server',
// HTML: <app-server></app-server>


  1. Attribute Selector: (Useful if you want to enhance a standard HTML element).
selector: '[app-server]',
// HTML: <div app-server></div>


  1. Class Selector:
selector: '.app-server',
// HTML: <div class="app-server"></div>


4. Assigning an Alias to Custom Properties (Inputs)

// Inside ServerElementComponent
@Input('srvElement') element: { type: string, name: string, content: string };


<app-server-element [srvElement]="serverElement"></app-server-element>


5. Assigning an Alias to Custom Events (Outputs)

// Inside CockpitComponent
@Output('bpCreated') blueprintCreated = new EventEmitter<{ serverName: string, serverContent: string }>();


<app-cockpit (bpCreated)="onBlueprintAdded($event)"></app-cockpit>


6. View Encapsulation (CSS Scoping)

7. More on View Encapsulation

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

@Component({
  selector: 'app-server-element',
  templateUrl: './server-element.component.html',
  styleUrls: ['./server-element.component.css'],
  encapsulation: ViewEncapsulation.None // Styles here will affect the WHOLE app
})
export class ServerElementComponent {}


8. Styling the Host Element (:host)

:host {
  display: block;
  border: 1px solid black;
}


:host-context(.theme-blue) {
  /* Only applies if some parent has class="theme-blue" */
  color: blue;
}



9. Understanding the Component Lifecycle

10. Seeing Lifecycle Hooks in Action

To use a hook, you should implement the corresponding interface (e.g., OnInit, OnChanges) for type safety, though it technically works without it.

A. ngOnChanges

import { Component, OnChanges, SimpleChanges, Input } from '@angular/core';

@Component({ selector: 'app-demo', template: '' })
export class DemoComponent implements OnChanges {
  @Input() data: string;

  ngOnChanges(changes: SimpleChanges) {
    console.log('ngOnChanges called!', changes);
  }
}


B. ngOnInit

ngOnInit() {
  console.log('ngOnInit called!');
}


C. ngDoCheck

ngDoCheck() {
  console.log('ngDoCheck called!');
}


D. ngAfterContentInit

ngAfterContentInit() {
  console.log('ngAfterContentInit called!');
}


E. ngAfterContentChecked

ngAfterContentChecked() {
  console.log('ngAfterContentChecked called!');
}


F. ngAfterViewInit

ngAfterViewInit() {
  console.log('ngAfterViewInit called!');
}


G. ngAfterViewChecked

ngAfterViewChecked() {
  console.log('ngAfterViewChecked called!');
}


H. ngOnDestroy

ngOnDestroy() {
  console.log('ngOnDestroy called!');
}



11. Lifecycle Hooks and Template Access

12. Getting Access to the Template & DOM with Local References

<input type="text" #serverNameInput>

<button (click)="onAddServer(serverNameInput)">Add Server</button>


onAddServer(nameInput: HTMLInputElement) {
  console.log(nameInput.value);
}


13. Getting Access to the Template & DOM with @ViewChild

import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

export class CockpitComponent implements AfterViewInit {
  // 1. Select the element
  @ViewChild('serverNameInput') serverContentInput: ElementRef;

  // 2. Access it (Safest in AfterViewInit)
  ngAfterViewInit() {
     console.log(this.serverContentInput.nativeElement.value);
  }

  onAddServer() {
     // You can also access it here on click events
     console.log(this.serverContentInput.nativeElement.value);
  }
}



14. Projecting Content into Components with ng-content

<app-server-element *ngFor="let server of serverElements" [srvElement]="server">
    <p>
        <strong *ngIf="server.type === 'server'" style="color: red"></strong>
        <em *ngIf="server.type === 'blueprint'"></em>
    </p>
</app-server-element>


  1. Child Template (server-element.component.html): Use the <ng-content> hook.
<div class="panel-body">
    <ng-content></ng-content>
</div>


15. Understanding @ContentChild

<app-server-element ...>
    <p #contentParagraph>...</p>
</app-server-element>


import { Component, ContentChild, ElementRef, AfterContentInit } from '@angular/core';

export class ServerElementComponent implements AfterContentInit {
  // Select the projected content by its local reference name
  @ContentChild('contentParagraph') paragraph: ElementRef;

  ngAfterContentInit() {
      // This is the earliest moment you can access the content
      console.log('Text Content of Paragraph: ' + this.paragraph.nativeElement.textContent);
  }
}



Section Wrap-up

You have now completed the “Deep Dive” into Components.


16. Component Selectors: Attribute & Class Selectors

(I touched on this, but here is the specific detail often missed)

<div app-dashboard></div>


<div class="app-dashboard"></div>


17. Input Transforms

import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core';

@Component({ ... })
export class CardComponent {
  // "<app-card isFeatured />" becomes true (instead of "")
  @Input({ transform: booleanAttribute }) isFeatured!: boolean;

  // "<app-card width="50" />" becomes number 50
  @Input({ transform: numberAttribute }) width!: number;
}


18. Listening to Host Events (@HostListener)

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

@Component({ ... })
export class CardComponent {
  @HostListener('click') onClick() {
    console.log('Component was clicked!');
  }

  @HostListener('mouseenter') onEnter() {
     console.log('Mouse entered the component area');
  }
}


19. Binding to Host Properties (@HostBinding)

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

export class CardComponent {
  // Adds class="active" to <app-card> if isActive is true
  @HostBinding('class.active') isActive = false; 

  // Sets style="border: 1px solid red" on <app-card>
  @HostBinding('style.border') border = '1px solid red';
}


20. The Modern “host” Property

@Component({
  selector: 'app-card',
  template: '...',
  host: {
    'class': 'user-card', // Static class
    '(click)': 'onCardClick()', // Event listener
    '[class.active]': 'isActive' // Property binding
  }
})
export class CardComponent {
  isActive = true;
  onCardClick() { console.log('Clicked'); }
}


21. Understanding Two-Way Binding (Deep Dive)

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({ ... })
export class SizerComponent {
  @Input() size!: number;
  @Output() sizeChange = new EventEmitter<number>();

  resize(delta: number) {
    this.size = Math.min(40, Math.max(8, +this.size + delta));
    // Emit the new value
    this.sizeChange.emit(this.size);
  }
}


<app-sizer [(size)]="fontSizePx" />


22. Content Projection: Multi-Slot Projection

<app-card>
  <header ngProjectAs="header">My Header</header>
  <div class="body">My Body Content</div>
</app-card>


<div class="card">
  <div class="card-header">
    <ng-content select="header"></ng-content>
  </div>
  <div class="card-body">
    <ng-content select=".body"></ng-content>
  </div>
</div>


23. @ContentChild vs @ContentChildren

import { ContentChildren, QueryList, AfterContentInit } from '@angular/core';

export class CardComponent implements AfterContentInit {
  @ContentChildren('inputRef') inputs!: QueryList<ElementRef>;

  ngAfterContentInit() {
    // Iterate over all found elements
    this.inputs.forEach(input => console.log(input.nativeElement.value));
  }
}


24. New Lifecycle Hooks: afterRender & afterNextRender

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

export class DemoComponent {
  constructor() {
    afterNextRender(() => {
      console.log('rendered!');
      // Safe to access DOM or browser-specific APIs (window, document) here
    });
  }
}



Section 7: Directives

1. Module Introduction & Directive Types

2. Built-in Directives Recap (ngClass & ngStyle)

<div [ngClass]="{active: isActive}">...</div>


<div [ngStyle]="{backgroundColor: getColor()}">...</div>


3. Creating a Basic Attribute Directive

import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[appBasicHighlight]' // Attribute selector
})
export class BasicHighlightDirective implements OnInit {
  constructor(private elementRef: ElementRef) {}

  ngOnInit() {
    // Accessing the native element directly
    this.elementRef.nativeElement.style.backgroundColor = 'green';
  }
}


<p appBasicHighlight>Style me with basic directive!</p>


4. Better Directive: Using the Renderer2

import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core';

@Directive({
  selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
  constructor(private elRef: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    // Safe styling via Renderer
    this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'blue');
    this.renderer.setStyle(this.elRef.nativeElement, 'color', 'white');
  }
}


5. Using HostListener to Listen to Host Events

import { HostListener } from '@angular/core';

// inside the directive class
@HostListener('mouseenter') mouseover(eventData: Event) {
  this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'blue');
}

@HostListener('mouseleave') mouseleave(eventData: Event) {
  this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'transparent');
}


6. Using HostBinding to Bind to Host Properties

import { HostBinding } from '@angular/core';

export class BetterHighlightDirective {
  // Bind to the style.backgroundColor property of the host element
  @HostBinding('style.backgroundColor') backgroundColor: string = 'transparent';

  @HostListener('mouseenter') mouseover() {
    this.backgroundColor = 'blue'; // Changing this variable automatically updates the DOM
  }

  @HostListener('mouseleave') mouseleave() {
    this.backgroundColor = 'transparent';
  }
}


7. Binding to Directive Properties (Custom Input)

@Input() defaultColor: string = 'transparent';
@Input() highlightColor: string = 'blue';

@HostBinding('style.backgroundColor') backgroundColor: string;

ngOnInit() {
  this.backgroundColor = this.defaultColor;
}

@HostListener('mouseenter') mouseover() {
  this.backgroundColor = this.highlightColor;
}


<p appBetterHighlight [defaultColor]="'yellow'" [highlightColor]="'red'">
  Hover me!
</p>


8. Binding to the Directive Selector Name (Alias)

// Alias the input 'appBetterHighlight' to the internal property 'highlightColor'
@Input('appBetterHighlight') highlightColor: string = 'blue';


<p [appBetterHighlight]="'red'" [defaultColor]="'yellow'">
   Cleaner Syntax
</p>



9. What Happens Behind the Scenes with the Asterisk (*)

<div *ngIf="isVisible">I am visible</div>


<ng-template [ngIf]="isVisible">
  <div>I am visible</div>
</ng-template>


10. Building a Custom Structural Directive

Step A: Setup

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  // Inject the required references
  constructor(
    private templateRef: TemplateRef<any>,
    private vcRef: ViewContainerRef
  ) {}
}


Step B: Logic (The Input Setter)

We need to run logic whenever the input condition changes. We use a setter for the input property.

  @Input() set appUnless(condition: boolean) {
    if (!condition) {
      // Condition is FALSE => Show the content
      // createEmbeddedView puts the template into the DOM container
      this.vcRef.createEmbeddedView(this.templateRef);
    } else {
      // Condition is TRUE => Remove the content
      this.vcRef.clear();
    }
  }


Step C: Usage

<div *appUnless="onlyOdd">
  <h3>Value is Even!</h3>
</div>


11. ngSwitch

<div [ngSwitch]="value">
  <p *ngSwitchCase="5">Value is 5</p>
  <p *ngSwitchCase="10">Value is 10</p>
  <p *ngSwitchDefault>Value is Default</p>
</div>



Section Wrap-up

You have now mastered Directives.


Section 8: Transforming Values with Pipes - Deep Dive

1. Introduction & What Pipes are

2. Using Built-in Pipes

<p>Unformatted: </p>
<p>Formatted: </p>


3. Parameterizing Pipes

<p></p>

<p></p>

<p></p>


4. Chaining Pipes

<p></p>


Note: Order matters! If you swapped them (uppercase | date), it would fail because uppercase works on strings, but date might expect a Date object (though date pipe is robust, logic applies).

5. Creating a Custom Pipe

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'shorten' // This is the name used in HTML
})
export class ShortenPipe implements PipeTransform {
  // transform(value: any, ...args: any[])
  transform(value: any, limit: number = 10): any {
    if (value.length > limit) {
      return value.substr(0, limit) + ' ...';
    }
    return value;
  }
}


<p></p>


6. Filtering Lists with Pipes (Impure Pipes)

@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
  transform(value: any, filterString: string, propName: string): any {
    if (value.length === 0 || filterString === '') {
      return value;
    }
    const resultArray = [];
    for (const item of value) {
      if (item[propName] === filterString) {
        resultArray.push(item);
      }
    }
    return resultArray;
  }
}


<input type="text" [(ngModel)]="filteredStatus">
<div *ngFor="let server of servers | filter:filteredStatus:'status'">
  ...
</div>


7. Pure vs. Impure Pipes (Performance Issue)

@Pipe({
  name: 'filter',
  pure: false // WARNING: High Performance Cost
})
export class FilterPipe { ... }


8. The Async Pipe

// Component
appStatus = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('stable');
  }, 2000);
});


<h2>App Status: </h2>


(Initially empty, after 2 seconds updates to ‘stable’)


Section Wrap-up: You have learned how to format data for the user view.


Section 9: Services & Dependency Injection

1. Introduction & Why Services?

2. Creating a Basic Logging Service

export class LoggingService {
  logStatusChange(status: string) {
    console.log('A server status changed, new status: ' + status);
  }
}


3. Injecting the Service into a Component

import { Component } from '@angular/core';
import { LoggingService } from './logging.service';

@Component({
  selector: 'app-new-account',
  templateUrl: './new-account.component.html',
  // We tell Angular how to create the service here
  providers: [LoggingService] 
})
export class NewAccountComponent {
  // Angular instantiates the service and gives it to us
  constructor(private loggingService: LoggingService) {}

  onCreateAccount(accountName: string, accountStatus: string) {
    this.loggingService.logStatusChange(accountStatus);
  }
}


4. Creating a Data Service

export class AccountsService {
  accounts = [
    { name: 'Master Account', status: 'active' },
    { name: 'Testaccount', status: 'inactive' }
  ];

  addAccount(name: string, status: string) {
    this.accounts.push({ name, status });
  }

  updateStatus(id: number, newStatus: string) {
    this.accounts[id].status = newStatus;
  }
}


5. Understanding the Hierarchical Injector (Theory)

  1. AppModule (Root):
    • If provided here (or providedIn: 'root'), the service is available application-wide.
    • Singleton: The same instance is shared by everyone.
  2. AppComponent:
    • If provided here, the service is available to AppComponent and all its child components.
    • It overrides the Root instance (if any).
  3. Any Other Component:
    • If provided here, the service is available to this component and its children.
    • New Instance: Angular creates a new, separate instance for this component tree.

6. The Problem with Multiple Instances


7. Injecting Services into Services

import { Injectable } from '@angular/core';
import { LoggingService } from './logging.service';

@Injectable() // Mandatory because we inject LoggingService
export class AccountsService {
  constructor(private loggingService: LoggingService) {}

  addAccount(name: string, status: string) {
    this.accounts.push({ name, status });
    // Using the injected service
    this.loggingService.logStatusChange(status);
  }
}


8. Using Services for Cross-Component Communication

// accounts.service.ts
import { EventEmitter } from '@angular/core';

export class AccountsService {
  statusUpdated = new EventEmitter<string>();
  // ... rest of code
}


// new-account.component.ts
constructor(private accountsService: AccountsService) {}

onSetStatus(status: string) {
  this.accountsService.statusUpdated.emit(status);
}


// account.component.ts
constructor(private accountsService: AccountsService) {
  this.accountsService.statusUpdated.subscribe(
    (status: string) => alert('New Status: ' + status)
  );
}


9. Modern Provisioning: providedIn: 'root'

@Injectable({
  providedIn: 'root'
})
export class AccountsService { ... }


10. The inject() Function (Modern Alternative)

import { inject } from '@angular/core';

export class AccountComponent {
  private accountsService = inject(AccountsService);

  // Logic remains the same
}



Section Wrap-up

You have learned that the Dependency Injector is hierarchical.


Section 10: Change Detection

1. Introduction: How Change Detection Works

2. Zone.js: The Magic Behind the Scenes

3. The Default Change Detection Strategy

@Component({
  selector: 'app-default',
  template: '',
  // Default strategy is implied if not specified
  // changeDetection: ChangeDetectionStrategy.Default
})
export class DefaultComponent {
  @Input() data: { name: string };
}


4. Understanding Object Mutability vs. Immutability

To understand the next topic (OnPush), we must understand how JavaScript handles objects.

let user = { name: 'Max' };
user.name = 'Anna'; 
// The object is MUTATED, but 'user' variable still points to the same memory address.


let user = { name: 'Max' };
user = { name: 'Anna' }; 
// The reference CHANGED. 'user' points to a new memory address.


5. Optimizing with ChangeDetectionStrategy.OnPush

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

@Component({
  selector: 'app-on-push',
  template: '',
  // Switch to OnPush
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() data: { name: string };
}


6. How OnPush Actually Works (The Rules)

When a component uses OnPush, Angular will skip checking it unless:

  1. The Input Reference Changes: The @Input property receives a completely new object/value (new memory address).
  2. An Event Originated inside the Component: The user clicked a button inside this specific component.
  3. The Async Pipe emits: An Observable subscribed via | async emits a new value.
  4. Manual Trigger: You manually tell Angular to check (covered in Part 2).

7. The “Trap”: Mutating Objects with OnPush

Code Example of the Issue:

// Parent Component (Default Strategy)
@Component({
  template: `
    <button (click)="changeName()">Change Name</button>
    <app-on-push [data]="user"></app-on-push>
  `
})
export class ParentComponent {
  user = { name: 'Max' };

  changeName() {
    // MUTATION: We change the property, but 'this.user' is the SAME object
    this.user.name = 'Anna'; 
    // Result: Child component will NOT update because reference didn't change.
  }
}



8. Fixing the “OnPush” Issue with Immutability

changeName() {
  // BAD (Mutation - OnPush ignores this):
  // this.user.name = 'Anna';

  // GOOD (Immutability - OnPush detects this):
  // We create a new object, copying old properties (...) and overriding name
  this.user = { ...this.user, name: 'Anna' };
}


9. Triggering Change Detection Manually (ChangeDetectorRef)

import { Component, ChangeDetectorRef, OnInit, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-manual',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent implements OnInit {
  data = 'Initial';

  constructor(private cdRef: ChangeDetectorRef) {}

  ngOnInit() {
    setTimeout(() => {
      // We mutate data (or set it without Input change)
      this.data = 'Updated via Timeout';

      // With OnPush, the view might NOT update automatically here.
      // We manually mark it for the next check:
      this.cdRef.markForCheck(); 
    }, 2000);
  }
}


10. Detaching and Reattaching (Advanced Performance)

constructor(private cdRef: ChangeDetectorRef) {
  // 1. Stop checking this component automatically
  this.cdRef.detach();
}

onHeavyCalculationFinished() {
  // 2. We finished complex logic, now update the UI once
  this.cdRef.detectChanges();
}


11. Avoiding Zone Pollution with runOutsideAngular

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

@Component({ ... })
export class ScrollComponent implements OnInit {
  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    // Run this logic OUTSIDE Angular's awareness
    this.ngZone.runOutsideAngular(() => {

      window.addEventListener('scroll', () => {
        // This code runs on every scroll
        console.log('Scrolling...'); 
        // Angular does NOT run change detection here.

        // If we decide we DO need to update the UI eventually:
        if (window.scrollY > 500) {
          // Jump back INSIDE the zone to update the UI
          this.ngZone.run(() => {
            this.showButton = true;
          });
        }
      });

    });
  }

  showButton = false;
}



Section Wrap-up

You have learned how to take control of Angular’s update cycle.


Section 11: Working with RxJS

1. Introduction: What are Observables?

2. Angular’s Built-in Observables

ngOnInit() {
  // Angular "pushes" new data here whenever the URL parameter changes
  this.route.params.subscribe((params) => {
    console.log(params['id']);
  });
}


3. Creating a Custom Observable

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({ ... })
export class HomeComponent implements OnInit {

  ngOnInit() {
    // Create the Observable
    const customIntervalObservable = new Observable(observer => {
      let count = 0;
      setInterval(() => {
        // 1. Emit a value (The "Next" signal)
        observer.next(count);

        // 2. Example: Complete the stream after 2 seconds
        if (count === 2) {
          observer.complete(); 
        }

        // 3. Example: Throw an error if count > 3
        if (count > 3) {
          observer.error(new Error('Count is greater than 3!'));
        }

        count++;
      }, 1000);
    });

    // Subscribe to listen
    customIntervalObservable.subscribe({
      next: (data) => console.log(data),      // Handle Data
      error: (error) => alert(error.message), // Handle Error
      complete: () => console.log('Completed!') // Handle Completion
    });
  }
}


4. Handling Errors & Completion

5. Understanding Subscriptions & Memory Leaks

import { Subscription } from 'rxjs';

export class HomeComponent implements OnInit, OnDestroy {
  private firstObsSubscription: Subscription;

  ngOnInit() {
    const customObs = ...; // (as defined above)

    // Store the subscription
    this.firstObsSubscription = customObs.subscribe(data => {
      console.log(data);
    });
  }

  ngOnDestroy() {
    // Clean up whenever the component is removed
    this.firstObsSubscription.unsubscribe();
  }
}


6. Understanding Operators

7. The map Operator

import { map } from 'rxjs/operators';

// ... inside ngOnInit
this.firstObsSubscription = customIntervalObservable.pipe(
  map((data: number) => {
    return 'Round: ' + (data + 1);
  })
).subscribe(data => {
  console.log(data); // Output: "Round: 1", "Round: 2"...
});


8. The filter Operator

import { filter, map } from 'rxjs/operators';

this.firstObsSubscription = customIntervalObservable.pipe(
  filter((data: number) => {
    return data > 0; // Only emit if true
  }),
  map((data: number) => {
    return 'Round: ' + (data + 1);
  })
).subscribe(data => {
  console.log(data);
});


9. Subjects (A Special Type of Observable)

10. Refactoring: Replacing EventEmitter with Subject

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs'; // Import from rxjs, NOT @angular/core

@Injectable({providedIn: 'root'})
export class UserService {
  // activatedEmitter = new EventEmitter<boolean>(); // OLD WAY
  activatedSubject = new Subject<boolean>();         // NEW WAY
}


// In user.component.ts
onActivate() {
  // this.userService.activatedEmitter.emit(true); // OLD
  this.userService.activatedSubject.next(true);    // NEW
}


// In app.component.ts
ngOnInit() {
  this.sub = this.userService.activatedSubject.subscribe(didActivate => {
    if (didActivate) {
      this.userActivated = true;
    }
  });
}

ngOnDestroy() {
  // IMPORTANT: You MUST unsubscribe manually from Subjects!
  // (EventEmitters didn't require this, but Subjects do)
  this.sub.unsubscribe();
}


11. BehaviorSubjects (Holding a Value)

// Initial value is 'false'
activatedSubject = new BehaviorSubject<boolean>(false); 



Section Wrap-up


RxJS Operators

1. map

import { map } from 'rxjs/operators';

myObservable.pipe(
  map(data => {
    return 'Transformed Data: ' + data;
  })
).subscribe(result => console.log(result));


2. filter

import { filter } from 'rxjs/operators';

myObservable.pipe(
  filter(num => num > 0) // Only allow positive numbers
).subscribe(result => console.log(result));


3. tap (formerly do)

import { tap, map } from 'rxjs/operators';

myObservable.pipe(
  tap(data => console.log('Raw Data: ' + data)), // Spy here
  map(data => data * 2),
  tap(data => console.log('Modified Data: ' + data)) // Spy again
).subscribe();


4. take

import { take } from 'rxjs/operators';

// Even if this observable emits forever, we stop after 1
myIntervalObservable.pipe(
  take(1) 
).subscribe(data => {
  console.log(data); // Prints once, then closes connection
});


5. debounceTime

import { debounceTime } from 'rxjs/operators';

searchParam.valueChanges.pipe(
  debounceTime(500) // Wait 500ms of silence
).subscribe(term => {
  // Send HTTP request
});


6. distinctUntilChanged

import { distinctUntilChanged } from 'rxjs/operators';

myObservable.pipe(
  distinctUntilChanged()
).subscribe();


7. catchError

import { catchError } from 'rxjs/operators';
import { of, throwError } from 'rxjs';

http.get('...').pipe(
  catchError(errorRes => {
    // Return a new observable with a safe default value
    return of([]); 
    // OR re-throw it to handle it in the subscribe block
    // return throwError(() => new Error(errorRes));
  })
).subscribe();



The “Flattening” Operators (Crucial for HTTP)

Max emphasizes these three operators heavily in the HTTP and Authentication sections. They are used when one Observable (e.g., a click) triggers another Observable (e.g., an HTTP request).

8. switchMap

import { switchMap } from 'rxjs/operators';

route.params.pipe(
  // If user switches ID fast, cancel the old HTTP fetch
  switchMap(params => {
    return this.http.get('/user/' + params['id']);
  })
).subscribe(userData => { ... });


9. exhaustMap

import { exhaustMap, take } from 'rxjs/operators';

this.authService.user.pipe(
  take(1),
  exhaustMap(user => {
    // Wait for this HTTP call to finish before accepting new clicks
    return this.http.get('...'); 
  })
).subscribe();


10. concatMap

import { concatMap } from 'rxjs/operators';

clicks.pipe(
  concatMap(click => this.http.post('/save', click))
).subscribe();



Section 12: Sending HTTP Requests

1. Module Introduction & The “Backend”

2. Setup: Adding the HTTP Client

Before you can use HTTP, you must unlock the capability in your app.

import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient() 
    // ...other providers
  ]
};


import { HttpClientModule } from '@angular/common/http';
@NgModule({
  imports: [HttpClientModule, ...]
})
export class AppModule {}


3. Sending a POST Request

constructor(private http: HttpClient) {}


onCreatePost(postData: { title: string; content: string }) {
  this.http
    .post(
      'https://your-project-id.firebaseio.com/posts.json',
      postData
    )
    .subscribe(responseData => {
      console.log(responseData);
    });
}


4. Sending a GET Request

private fetchPosts() {
  this.http
    .get('https://your-project-id.firebaseio.com/posts.json')
    .subscribe(posts => {
      console.log(posts);
    });
}


5. Transforming Data with the map Operator

{
  "-N7a8s9d": { "title": "First", "content": "..." },
  "-N7b9s1a": { "title": "Second", "content": "..." }
}


import { map } from 'rxjs/operators';

this.http
  .get('https://.../posts.json')
  .pipe(
    map((responseData: {[key: string]: Post}) => {
      const postsArray: Post[] = [];
      for (const key in responseData) {
        if (responseData.hasOwnProperty(key)) {
          // Push a new object containing the ID (key) and the data (...)
          postsArray.push({ ...responseData[key], id: key });
        }
      }
      return postsArray;
    })
  )
  .subscribe(posts => {
    // Now 'posts' is a nice array we can loop over with *ngFor
    this.loadedPosts = posts;
  });


6. Using Types with HttpClient

// Define the interface
export interface Post {
  title: string;
  content: string;
  id?: string;
}

// Use the generic type
this.http.get<{ [key: string]: Post }>('...url')
  .pipe(...)
  .subscribe(...);


7. Moving Logic to a Service

Code (Service):

fetchPosts() {
  return this.http.get(...).pipe(map(...));
}


Code (Component):

ngOnInit() {
  this.postsService.fetchPosts().subscribe(posts => {
    this.loadedPosts = posts;
    this.isFetching = false;
  });
}



8. Showing a Loading Indicator

<p *ngIf="isFetching">Loading...</p>

<ul *ngIf="!isFetching && loadedPosts.length > 0">
  <li *ngFor="let post of loadedPosts">...</li>
</ul>


onFetchPosts() {
  this.isFetching = true; // Start loading
  this.postsService.fetchPosts().subscribe(posts => {
    this.isFetching = false; // Stop loading
    this.loadedPosts = posts;
  });
}


9. Handling Errors (The Basic Way)

onFetchPosts() {
  this.postsService.fetchPosts().subscribe({
    next: (posts) => {
      // Success
    },
    error: (error) => {
      // Failure
      this.error = error.message; // Display this string in the UI
      console.log(error);
    }
  });
}


10. Using a Subject for Error Handling

error = new Subject<string>();

createAndStorePost(title: string, content: string) {
  this.http.post(...)
    .subscribe({
      next: data => console.log(data),
      error: error => {
        // Push the error message to anyone listening
        this.error.next(error.message);
      }
    });
}


ngOnInit() {
  this.errorSub = this.postsService.error.subscribe(errorMessage => {
    this.error = errorMessage; // Update UI
  });
}


11. Using the catchError Operator

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

fetchPosts() {
  return this.http.get(...)
    .pipe(
      map(...),
      catchError(errorRes => {
        // Logic: Send to analytics server...
        return throwError(() => new Error(errorRes));
      })
    );
}


12. Setting Headers

import { HttpHeaders } from '@angular/common/http';

fetchPosts() {
  return this.http.get('url', {
    headers: new HttpHeaders({ 'Custom-Header': 'Hello' })
  });
}


13. Adding Query Params

import { HttpParams } from '@angular/common/http';

let searchParams = new HttpParams();
searchParams = searchParams.append('print', 'pretty');
searchParams = searchParams.append('custom', 'key');

return this.http.get('url', {
  headers: new HttpHeaders(...),
  params: searchParams // Result: url?print=pretty&custom=key
});


14. Observing Different Types of Responses

this.http.get('url', {
  observe: 'response' // Default is 'body'
}).subscribe(response => {
  // Now 'response' is the full HttpResponse object
  console.log(response.status); // 200
  console.log(response.body);   // The data
});


15. Changing the Response Body Type

this.http.get('url', {
  responseType: 'text'
}).subscribe(data => {
  console.log(data); // "data" is now a raw string
});



16. Introduction to Interceptors

17. Defining an Interceptor (The Class)

import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

export class AuthInterceptorService implements HttpInterceptor {
  // req: The outgoing request
  // next: The object that lets the request continue its journey
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Request is on its way');

    // IMPORTANT: You must call next.handle(req) or the request dies here.
    return next.handle(req); 
  }
}


18. Manipulating Request Objects

intercept(req: HttpRequest<any>, next: HttpHandler) {
  // Clone the request and overwrite/add headers
  const modifiedRequest = req.clone({
    headers: req.headers.append('Auth', 'xyz')
  });

  // Pass the MODIFIED request forward
  return next.handle(modifiedRequest);
}


19. Registering Interceptors (Legacy/Modules)

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptorService } from './auth-interceptor.service';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptorService,
      multi: true
    }
  ]
})
export class AppModule {}


20. Registering Functional Interceptors (Modern/Standalone)

// logging.interceptor.ts (The Function)
import { HttpInterceptorFn } from '@angular/common/http';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log('Outgoing Request:', req.url);
  return next(req);
};

// app.config.ts (The Registration)
import { provideHttpClient, withInterceptors } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([loggingInterceptor]) // Register function here
    )
  ]
};


21. Response Interceptors

import { tap } from 'rxjs/operators';
import { HttpEventType } from '@angular/common/http';

intercept(req: HttpRequest<any>, next: HttpHandler) {
  return next.handle(req).pipe(
    tap(event => {
      // Check if the event is the actual Response arrival
      if (event.type === HttpEventType.Response) {
        console.log('Incoming Response Body:', event.body);
      }
    })
  );
}


22. Multiple Interceptors & Order of Execution

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },    // Runs 1st
  { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }  // Runs 2nd
]



Section Wrap-up

You have now mastered backend communication.


Section 13: Handling Forms

1. Module Introduction & Two Approaches

2. Setup for Template-Driven Forms

import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [FormsModule, ...]
})
export class AppModule {}


3. Creating the Form & Registering Controls

Code:

<form>
  <label>Username</label>
  <input type="text" name="username" ngModel>
  
  <label>Email</label>
  <input type="email" name="email" ngModel>
  
  <button type="submit">Submit</button>
</form>


4. Submitting and Using the Form

Code:

<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  ... inputs ...
  <button type="submit">Submit</button>
</form>


TypeScript Logic:

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms'; // Import the type

@Component({ ... })
export class AppComponent {
  onSubmit(form: NgForm) {
    console.log(form);
    // You can access values here:
    console.log(form.value.username);
  }
}


5. Understanding the Form State

When you log the form object (of type NgForm) to the console, you see many properties automatically managed by Angular:

6. Adding Validation

Code:

<input 
  type="email" 
  name="email" 
  ngModel 
  required 
  email>


7. Accessing Control State & Showing Error Messages

Code:

<label>Email</label>
<input 
  type="email" 
  name="email" 
  ngModel 
  required 
  email
  #emailCtrl="ngModel">

<p class="help-block" *ngIf="!emailCtrl.valid && emailCtrl.touched">
  Please enter a valid email!
</p>


8. Styling Invalid Controls (CSS Classes)

CSS Code:

/* If input is invalid AND touched, make border red */
input.ng-invalid.ng-touched {
  border: 1px solid red;
}


9. Default Values & Two-Way Binding in Forms

Code:

<select name="secret" [ngModel]="'pet'">
  <option value="pet">Your first Pet?</option>
  <option value="teacher">Your first Teacher?</option>
</select>

<textarea name="questionAnswer" [(ngModel)]="answer"></textarea>
<p>Your reply: </p> 


10. Grouping Form Controls (ngModelGroup)

Code:

<div id="user-data" ngModelGroup="userData" #userData="ngModelGroup">
  <input name="username" ngModel required>
  <input name="email" ngModel required>
</div>

<p *ngIf="!userData.valid && userData.touched">User Data is invalid!</p>


11. Setting and Patching Form Values

Code (TypeScript):

@ViewChild('f') signupForm: NgForm; // Access form in TS

onSuggestUserName() {
  const suggestedName = 'Superuser';

  // Approach 1: Destructive (Resets email/secret)
  /*
  this.signupForm.setValue({
    userData: {
      username: suggestedName,
      email: ''
    },
    secret: 'pet',
    questionAnswer: ''
  });
  */

  // Approach 2: Safe (Only updates username)
  this.signupForm.form.patchValue({
    userData: {
      username: suggestedName
    }
  });
}


12. Resetting the Form

Code:

onSubmit() {
  // 1. Use the data
  console.log(this.signupForm.value);
  
  // 2. Clear form and validation state
  this.signupForm.reset();
}


13. Setup for Reactive Forms

Code:

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [ReactiveFormsModule, ...]
})
export class AppModule {}


14. Creating a Form in Code

Code (TypeScript):

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({ ... })
export class AppComponent implements OnInit {
  signupForm: FormGroup; // Declare the property

  ngOnInit() {
    // Initialize the form structure
    this.signupForm = new FormGroup({
      'username': new FormControl(null, Validators.required),
      'email': new FormControl(null, [Validators.required, Validators.email]),
      'gender': new FormControl('male') // Default value 'male'
    });
  }

  onSubmit() {
    // We access the form directly via 'this'
    console.log(this.signupForm.value);
  }
}


15. Syncing HTML with the TypeScript Form

Code (HTML):

<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
  
  <label>Username</label>
  <input type="text" formControlName="username">
  
  <label>Email</label>
  <input type="text" formControlName="email">
  
  <button type="submit">Submit</button>
</form>


16. Validation & Error Messages

Code:

<input type="text" formControlName="username">

<span 
  *ngIf="!signupForm.get('username').valid && signupForm.get('username').touched"
  class="help-block">
  Please enter a valid username!
</span>


17. Grouping Controls (Nested FormGroups)

Code (TS):

this.signupForm = new FormGroup({
  'userData': new FormGroup({ // Nested Group
    'username': new FormControl(null, Validators.required),
    'email': new FormControl(null, [Validators.required, Validators.email])
  }),
  'gender': new FormControl('male')
});


Code (HTML):

<form [formGroup]="signupForm" ...>
  <div formGroupName="userData"> <input formControlName="username">
    <input formControlName="email">
  </div>
  ...
</form>


18. Arrays of Controls (FormArray)

Step 1: Define Array in TS

import { FormArray } from '@angular/forms';

this.signupForm = new FormGroup({
  // Initialize empty array
  'hobbies': new FormArray([]) 
});


Step 2: Add Control Method

onAddHobby() {
  const control = new FormControl(null, Validators.required);
  // Cast to FormArray to treat it as an array
  (<FormArray>this.signupForm.get('hobbies')).push(control);
}


Step 3: Sync HTML (Looping)

<div formArrayName="hobbies">
  <h4>Your Hobbies</h4>
  <button type="button" (click)="onAddHobby()">Add Hobby</button>
  
  <div *ngFor="let hobbyControl of getControls(); let i = index">
    <input type="text" [formControlName]="i">
  </div>
</div>


Step 4: Helper Getter (Required for Modern Angular strict mode) Trying to loop directly over signupForm.get('hobbies').controls in the HTML often causes type errors. It’s best to create a getter.

getControls() {
  return (<FormArray>this.signupForm.get('hobbies')).controls;
}


19. Creating Custom Validators

Code (TypeScript):

// Define forbidden names
forbiddenUsernames = ['Chris', 'Anna'];

// The Validator Function
forbiddenNames(control: FormControl): {[s: string]: boolean} {
  // Check if value is in our forbidden array
  if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
    // Return error object (INVALID)
    return {'nameIsForbidden': true};
  }
  // Return null (VALID) - Do NOT return false
  return null;
}


Usage in OnInit:

this.signupForm = new FormGroup({
  'username': new FormControl(null, [
    Validators.required, 
    this.forbiddenNames.bind(this) // Bind 'this' context
  ]),
  // ...
});


20. Using Error Codes in HTML

Code (HTML):

<span *ngIf="signupForm.get('username').hasError('nameIsForbidden')">
  This name is forbidden!
</span>
<span *ngIf="signupForm.get('username').hasError('required')">
  Name is required!
</span>


21. Creating Async Validators

Code (TypeScript):

import { Observable } from 'rxjs';

forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
  const promise = new Promise<any>((resolve, reject) => {
    setTimeout(() => {
      if (control.value === 'test@test.com') {
        // Resolve with error object
        resolve({'emailIsForbidden': true});
      } else {
        // Resolve with null
        resolve(null);
      }
    }, 1500); // Simulate 1.5s server delay
  });
  return promise;
}


Usage in OnInit:

this.signupForm = new FormGroup({
  'email': new FormControl(
    null, 
    [Validators.required, Validators.email], // Arg 2: Sync Validators
    this.forbiddenEmails // Arg 3: Async Validators
  )
});


22. Reacting to Status & Value Changes

Code (ngOnInit):

// Log every keystroke in the form
this.signupForm.valueChanges.subscribe(
  (value) => console.log(value)
);

// React to status updates
this.signupForm.statusChanges.subscribe(
  (status) => console.log(status)
);


23. Setting & Patching Values (Reactive)

Code:

// Update specific field
this.signupForm.patchValue({
  'userData': {
    'username': 'Anna'
  }
});

// Reset Form
this.signupForm.reset();



Section Wrap-up

You have now mastered Forms in Angular.


Section 14: Routing & Navigation

1. Module Introduction & Setup

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';

export const routes: Routes = [
  { path: '', component: HomeComponent }, // localhost:4200/
  { path: 'users', component: UsersComponent } // localhost:4200/users
];


import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)]
});


2. The <router-outlet>

<app-header></app-header>
<router-outlet></router-outlet>
<app-footer></app-footer>


<a routerLink="/users">Users</a>

<a [routerLink]="['/users', userId]">User Profile</a>


<li routerLinkActive="active-link" [routerLinkActiveOptions]="{exact: true}">
  <a routerLink="/">Home</a>
</li>
<li routerLinkActive="active-link">
  <a routerLink="/users">Users</a>
</li>


5. Relative vs. Absolute Paths

constructor(private router: Router, private route: ActivatedRoute) {}

onReload() {
  // Goes to /current-path/servers
  this.router.navigate(['servers'], { relativeTo: this.route });
}


6. Passing Dynamic Parameters (Path Params)

{ path: 'users/:id', component: UserComponent }


// Only works if you land on the page once and don't change params while staying on same component
const id = this.route.snapshot.params['id'];


this.route.params.subscribe(params => {
  this.id = params['id']; // Updates whenever URL changes
});


7. Passing Query Parameters & Fragments

<a [routerLink]="['/servers']" 
   [queryParams]="{mode: 'edit'}" 
   fragment="loading">
   Edit Server
</a>


this.router.navigate(['/servers'], {
  queryParams: { mode: 'edit' },
  fragment: 'loading'
});


// Snapshot
this.mode = this.route.snapshot.queryParams['mode'];
this.fragment = this.route.snapshot.fragment;

// Observable (Reactive)
this.route.queryParams.subscribe(params => { ... });
this.route.fragment.subscribe(frag => { ... });


8. Nested Routes (Child Routes)

const routes: Routes = [
  { 
    path: 'servers', 
    component: ServersComponent, 
    children: [
      // Matches /servers/ (default text)
      { path: '', component: ServerStartComponent }, 
      // Matches /servers/:id (details)
      { path: ':id', component: ServerDetailComponent }, 
      // Matches /servers/:id/edit (edit mode)
      { path: ':id/edit', component: EditServerComponent } 
    ]
  }
];


<div class="row">
  <div class="col-xs-12 col-sm-4">
    <app-list-servers></app-list-servers>
  </div>
  <div class="col-xs-12 col-sm-4">
    <router-outlet></router-outlet>
  </div>
</div>


9. Redirecting and Wildcard Routes (404 Page)

Code:

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UsersComponent },
  
  // 1. Specific Page Not Found Component
  { path: 'not-found', component: PageNotFoundComponent },
  
  // 2. Redirect /something-weird to /not-found
  { path: '**', redirectTo: '/not-found' } 
];


10. Path Matching Strategies (full vs prefix)

{ path: '', redirectTo: '/home' } // ERROR (Infinite Loop potential)


Code:

{ path: '', redirectTo: '/home', pathMatch: 'full' }


11. Preserving Query Parameters

Code (TypeScript):

constructor(private router: Router, private route: ActivatedRoute) {}

onEdit() {
  // Navigate to 'edit' child route
  this.router.navigate(['edit'], { 
    relativeTo: this.route, 
    // KEEP the ?mode=edit param
    queryParamsHandling: 'preserve' 
  });
}


Code (HTML):

<a 
  routerLink="edit" 
  queryParamsHandling="preserve">
  Edit
</a>


12. Protecting Routes with Guards (CanActivate)

Step 1: Define the Guard Function (auth.guard.ts)

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true; // Allow navigation
  } else {
    // Optional: Redirect them to a login page
    return router.createUrlTree(['/']); 
  }
};


Step 2: Apply to Routes (app.routes.ts)

{ 
  path: 'servers', 
  component: ServersComponent,
  canActivate: [authGuard] // Attach the guard here
}


13. Protecting Child Routes (CanActivateChild)

Code (app.routes.ts):

{ 
  path: 'servers', 
  component: ServersComponent,
  // This guard runs for ANY child route, but not the parent itself
  canActivateChild: [authGuard], 
  children: [ ... ]
}


14. controlling Navigation Away (CanDeactivate)

Step 1: Define Interface (can-deactivate.guard.ts)

import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}


Step 2: Create the Guard

import { CanDeactivateFn } from '@angular/router';

export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = 
  (component, currentRoute, currentState, nextState) => {
    // Call the method ON the component
    return component.canDeactivate();
  };


Step 3: Implement in Component

export class EditServerComponent implements CanComponentDeactivate {
  changesSaved = false;

  canDeactivate(): boolean {
    if (!this.changesSaved) {
      return confirm('Do you want to discard your changes?');
    }
    return true;
  }
}


Step 4: Register in Routes

{ 
  path: ':id/edit', 
  component: EditServerComponent, 
  canDeactivate: [canDeactivateGuard] 
}


15. Passing Static Data to a Route

Config:

{ path: 'not-found', component: ErrorPageComponent, data: {message: 'Page not found!'} },
{ path: 'server-error', component: ErrorPageComponent, data: {message: 'Server is down!'} }


Component Access:

ngOnInit() {
  // Access the static data via snapshot or observable
  this.errorMessage = this.route.snapshot.data['message'];
  
  this.route.data.subscribe(data => {
    this.errorMessage = data['message'];
  });
}


16. Resolving Dynamic Data (Resolvers)

Step 1: Create Resolver Function (server-resolver.ts)

import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { ServersService } from './servers.service';

interface Server { id: number; name: string; status: string; }

export const serverResolver: ResolveFn<Server> = (route, state) => {
  const serversService = inject(ServersService);
  // Return the data (Observable, Promise, or value)
  // Angular waits for this to complete
  return serversService.getServer(+route.params['id']);
};


Step 2: Register in Route

{ 
  path: 'servers/:id', 
  component: ServerComponent, 
  // Map the result to a key named 'server'
  resolve: { server: serverResolver } 
}


Step 3: Access in Component

ngOnInit() {
  this.route.data.subscribe((data) => {
    // 'server' matches the key used in the 'resolve' object above
    this.server = data['server']; 
  });
}


17. Hash Location Strategy (Optional)

import { provideRouter, withHashLocation } from '@angular/router';

providers: [
  provideRouter(routes, withHashLocation())
]



Section Wrap-up

You have now mastered the Angular Router.


Section 15: Code Splitting & Deferrable Views

1. Module Introduction & Why Code Splitting?

2. Lazy Loading Routes (The Modern Way)

Code (app.routes.ts):

import { Routes } from '@angular/router';
// Note: We do NOT import UsersComponent here at the top!

export const routes: Routes = [
  {
    path: '',
    // Eagerly loaded (downloaded immediately)
    loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
  },
  {
    path: 'users',
    // LAZY Loaded: Downloaded only when user visits /users
    loadComponent: () => import('./users/users.component').then(m => m.UsersComponent)
  }
];


3. Introduction to Deferrable Views (@defer)

4. Basic Syntax & Triggers

Code:

@defer {
  <app-heavy-chart></app-heavy-chart>
}


5. Using Interaction Triggers (on interaction)

Code:

<button type="button" #loadBtn>Load Chart</button>

@defer (on interaction(loadBtn)) {
  <app-heavy-chart></app-heavy-chart>
}


6. Using Viewport Triggers (on viewport)

Code:

<div style="height: 2000px">Long content...</div>

@defer (on viewport) {
  <app-heavy-map></app-heavy-map>
} @placeholder {
  <p>Scroll down to see the map...</p>
}


7. Managing States: @placeholder, @loading, @error

Since the content is loaded asynchronously, you need to handle the different states of the UI.

  1. @placeholder:
    • Shown before loading starts.
    • Crucial: It reserves space in the DOM so the layout doesn’t “jump” (Cumulative Layout Shift - CLS) when the content finally loads.
    • Option: minimum 1s (Shows for at least 1s to prevent flickering).
  2. @loading:
    • Shown while the chunk is being downloaded from the network.
    • Option: after 100ms; minimum 1s (Only show if loading takes longer than 100ms, and keep showing for 1s).
  3. @error:
    • Shown if the network request fails.

Full Code Example:

@defer (on interaction) {
  <app-heavy-chart></app-heavy-chart>
} 
@loading (after 100ms; minimum 1s) {
  <div class="spinner">Downloading Chart...</div>
} 
@placeholder (minimum 500ms) {
  <button>Click to Load Chart</button>
} 
@error {
  <p>Failed to load the chart. Check internet connection.</p>
}


8. Other Triggers


Section Wrap-up: You now have the tools to make your app highly performant.


Section 16: Authentication

1. How SPA Authentication Works

2. Setting up the Auth Component & Form

Code (auth.component.html):

<form #authForm="ngForm" (ngSubmit)="onSubmit(authForm)">
  <div>
    <label>E-Mail</label>
    <input type="email" name="email" ngModel required email>
  </div>
  <div>
    <label>Password</label>
    <input type="password" name="password" ngModel required minlength="6">
  </div>
  
  <div>
    <button type="submit" [disabled]="!authForm.valid">
      
    </button>
    
    <button type="button" (click)="onSwitchMode()">
      Switch to 
    </button>
  </div>
</form>


Code (auth.component.ts):

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { AuthService } from './auth.service';

@Component({ ... })
export class AuthComponent {
  isLoginMode = true;

  constructor(private authService: AuthService) {}

  onSwitchMode() {
    this.isLoginMode = !this.isLoginMode;
  }

  onSubmit(form: NgForm) {
    if (!form.valid) return;
    const email = form.value.email;
    const password = form.value.password;

    if (this.isLoginMode) {
      // Login Logic
      this.authService.login(email, password).subscribe(...);
    } else {
      // Signup Logic
      this.authService.signup(email, password).subscribe(...);
    }
    
    form.reset();
  }
}


3. Creating the AuthService

Code (auth.service.ts):

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

// Define the shape of the API response
interface AuthResponseData {
  kind: string;
  idToken: string;
  email: string;
  refreshToken: string;
  expiresIn: string;
  localId: string;
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  // Use your actual Firebase API Key here
  private apiKey = 'YOUR_API_KEY'; 

  constructor(private http: HttpClient) {}

  signup(email: string, password: string) {
    return this.http.post<AuthResponseData>(
      `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${this.apiKey}`,
      {
        email: email,
        password: password,
        returnSecureToken: true
      }
    );
  }

  login(email: string, password: string) {
    return this.http.post<AuthResponseData>(
      `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${this.apiKey}`,
      {
        email: email,
        password: password,
        returnSecureToken: true
      }
    );
  }
}


4. Handling Loading States & Errors

Code (Handling Error in Component):

onSubmit(form: NgForm) {
  this.isLoading = true;
  
  let authObs: Observable<AuthResponseData>; // Store the generic observable

  if (this.isLoginMode) {
    authObs = this.authService.login(...);
  } else {
    authObs = this.authService.signup(...);
  }

  authObs.subscribe({
    next: resData => {
      console.log(resData);
      this.isLoading = false;
      this.router.navigate(['/recipes']); // Redirect on success
    },
    error: errorMessage => {
      console.log(errorMessage);
      this.error = errorMessage; // Show in UI
      this.isLoading = false;
    }
  });
}


5. Creating the User Model

export class User {
  constructor(
    public email: string,
    public id: string,
    private _token: string,
    private _tokenExpirationDate: Date
  ) {}

  // Getter allows us to access it like a property: user.token
  get token() {
    // Check if token exists and hasn't expired
    if (!this._tokenExpirationDate || new Date() > this._tokenExpirationDate) {
      return null;
    }
    return this._token;
  }
}


6. Managing State with BehaviorSubject

import { BehaviorSubject } from 'rxjs';
import { User } from './user.model';

export class AuthService {
  // Initially null (no user logged in)
  user = new BehaviorSubject<User>(null); 

  // ...rest of code
}


7. Saving the User (On Success)

import { tap } from 'rxjs/operators';

login(email: string, password: string) {
  return this.http.post<AuthResponseData>(...).pipe(
    // Use 'tap' to perform side-effects without altering the response
    tap(resData => {
      this.handleAuthentication(
        resData.email, 
        resData.localId, 
        resData.idToken, 
        +resData.expiresIn // Convert string to number
      );
    })
  );
}

private handleAuthentication(email: string, userId: string, token: string, expiresIn: number) {
  // Calculate exact expiration date (e.g., Current Time + 3600 seconds)
  const expirationDate = new Date(new Date().getTime() + expiresIn * 1000);

  // Create User instance
  const user = new User(email, userId, token, expirationDate);

  // Emit the user to all subscribers
  this.user.next(user);
}


8. Persisting Login (localStorage)

localStorage.setItem('userData', JSON.stringify(user));


autoLogin() {
  const userData: {
    email: string;
    id: string;
    _token: string;
    _tokenExpirationDate: string;
  } = JSON.parse(localStorage.getItem('userData'));

  if (!userData) {
    return;
  }

  const loadedUser = new User(
    userData.email,
    userData.id,
    userData._token,
    new Date(userData._tokenExpirationDate) // Convert string back to Date object
  );

  // Check if valid using the getter
  if (loadedUser.token) {
    this.user.next(loadedUser); // Restore state

    // Optional: Setup auto-logout timer based on remaining time
    const expirationDuration = 
      new Date(userData._tokenExpirationDate).getTime() - new Date().getTime();
    this.autoLogout(expirationDuration); 
  }
}


9. Adding the Logout Feature

logout() {
  this.user.next(null);
  this.router.navigate(['/auth']);
  localStorage.removeItem('userData');

  if (this.tokenExpirationTimer) {
    clearTimeout(this.tokenExpirationTimer);
  }
  this.tokenExpirationTimer = null;
}


10. Auto-Logout (Timer)

private tokenExpirationTimer: any;

autoLogout(expirationDuration: number) {
  this.tokenExpirationTimer = setTimeout(() => {
    this.logout();
  }, expirationDuration);
}


11. Attaching the Token via Interceptor

Code (auth-interceptor.service.ts):

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpParams } from '@angular/common/http';
import { take, exhaustMap } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    return this.authService.user.pipe(
      take(1), // Get user once and unsubscribe automatically
      exhaustMap(user => {
        // If no user (e.g., during login), send original request
        if (!user) {
          return next.handle(req);
        }
        
        // If user exists, clone request and add token param
        const modifiedReq = req.clone({
          params: new HttpParams().set('auth', user.token)
        });
        return next.handle(modifiedReq);
      })
    );
  }
}


12. Protecting Routes (AuthGuard)

Code (auth.guard.ts):

import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(): Observable<boolean | UrlTree> {
    return this.authService.user.pipe(
      take(1), // Important: prevent ongoing subscription bugs
      map(user => {
        const isAuth = !!user; // Convert Object to Boolean (hack)
        if (isAuth) {
          return true;
        }
        // Redirect to Auth page if not logged in
        return this.router.createUrlTree(['/auth']);
      })
    );
  }
}


Usage (app-routing.module.ts):

{ 
  path: 'recipes', 
  component: RecipesComponent, 
  canActivate: [AuthGuard] // Attach guard here
}


13. Reflecting Auth State in the UI

Code (header.component.ts):

isAuthenticated = false;
private userSub: Subscription;

constructor(private authService: AuthService) {}

ngOnInit() {
  this.userSub = this.authService.user.subscribe(user => {
    // !!user is a JS trick: null -> false, object -> true
    this.isAuthenticated = !!user;
  });
}

onLogout() {
  this.authService.logout();
}

ngOnDestroy() {
  this.userSub.unsubscribe();
}


Template (header.component.html):

<ul class="nav navbar-nav navbar-right">
  <li *ngIf="!isAuthenticated">
    <a routerLink="/auth">Authenticate</a>
  </li>
  
  <li *ngIf="isAuthenticated">
    <a style="cursor: pointer;" (click)="onLogout()">Logout</a>
  </li>
</ul>


14. Section Wrap-up

You have built a complete Authentication system.