What is the Basics of Angular?

Introduction

Welcome to the exciting realm of Angular development! This ultimate guide takes you from Angular basics to expert levels, offering insights into state-of-the-art features like authentication, deployment, and more. Whether you're just starting or leveling up, join us on this adventure to craft amazing web applications with Angular.

Introduction to Web Development


HTML (Hypertext Markup Language)

HTML is the standard markup language used to create the structure of web pages. It consists of elements represented by tags that define the content and layout of a webpage.

<!DOCTYPE html>
<html>
<head>
    <title>Hello, World!</title>
</head>
<body>
    <h1>Welcome to My Webpage</h1>
    <p>This is a sample paragraph.</p>
</body>
</html>

CSS (Cascading Style Sheets)

CSS is used to control the presentation and layout of HTML documents. It allows you to style elements by applying rules to them.

/* styles.css */
body {
  font-family: Arial, sans-serif;
  background-color: #f0f0f0;
}

h1 {
  color: blue;
}

p {
  font-size: 16px;
  color: #333;
}

JavaScript

JavaScript is a powerful scripting language used to add interactivity to web pages. It allows you to manipulate HTML and CSS, handle events, and interact with the user.

// script.js
function greetUser(name) {
  alert(`Hello, ${name}!`);
}

const button = document.querySelector('button');
button.addEventListener('click', () => {
  const name = prompt('Enter your name:');
  greetUser(name);
});

TypeScript Fundamentals

TypeScript is a superset of JavaScript, and it adds static typing to the language. It's used extensively in Angular development. Understanding TypeScript is crucial to working effectively with Angular.

Basic Types

TypeScript introduces various basic data types such as

  1. Number
  2. String
  3. Boolean
  4. Array
  5. Tuple
  6. Enum
let age: number = 30;
let name: string = "John";
let isStudent: boolean = true;
let hobbies: string[] = ["reading", "swimming", "coding"];
let coordinates: [number, number] = [10, 20];

enum Color {
  Red,
  Green,
  Blue
}
let selectedColor: Color = Color.Green;

Functions and Interfaces

TypeScript allows you to define function signatures and interfaces to create custom data types.

interface Person {
  name: string;
  age: number;
}

function greet(person: Person): string {
  return `Hello, ${person.name}! You are ${person.age} years old.`;
}

let user: Person = { name: "Alice", age: 25 };
console.log(greet(user));

Classes and Access Modifiers

TypeScript supports class-based object-oriented programming with access modifiers like 'public', 'private', and 'protected'.

class Animal {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  public makeSound(): void {
    console.log("Animal makes a sound");
  }
}

class Dog extends Animal {
  public makeSound(): void {
    console.log("Dog barks");
  }
}

const dog = new Dog("Buddy");
dog.makeSound(); // Output: "Dog barks"

Type Inference

TypeScript can often infer the types without explicit annotations, making your code concise.

let x = 10; // TypeScript infers x as a number
let y = "hello"; // TypeScript infers y as a string

Angular


Directory Structure

The directory structure of an Angular application follows a modular and organized approach. Angular CLI sets up a default directory structure when you create a new Angular project. Here is a typical directory structure:

|-- app
|   |-- components
|   |   |-- component-1
|   |   |   |-- component-1.component.ts
|   |   |   |-- component-1.component.html
|   |   |   |-- component-1.component.scss
|   |   |   |-- component-1.component.spec.ts
|   |   |-- component-2
|   |   |-- ...
|   |-- services
|   |   |-- service-1
|   |   |   |-- service-1.service.ts
|   |   |-- service-2
|   |   |-- ...
|   |-- modules
|   |   |-- feature-module-1
|   |   |   |-- feature-module-1.module.ts
|   |   |   |-- feature-module-1.component.ts
|   |   |   |-- ...
|   |   |-- feature-module-2
|   |   |-- ...
|   |-- shared
|   |   |-- shared.module.ts
|   |   |-- shared-component-1
|   |   |-- shared-component-2
|   |-- app-routing.module.ts
|   |-- app.component.ts
|   |-- app.component.html
|   |-- app.component.scss
|   |-- app.component.spec.ts
|   |-- app.module.ts
|   |-- ...
|-- assets
|   |-- images
|   |-- ...
|-- environments
|   |-- environment.ts
|   |-- environment.prod.ts
|-- index.html
|-- styles.scss
|-- ...
|-- main.ts
|-- polyfills.ts
|-- ...
  1. app: This directory contains the main code of your Angular application.
    • components: Contains individual components of your application. Each component has its own directory with a TypeScript file, HTML template, CSS/SCSS styles, and a spec file for testing.
    • services: Contains services used to provide business logic, data handling, and other shared functionality.
    • modules: Contains feature modules that group related components, directives, and services together. Each module has its own directory with a module file and related components.
    • shared: Contains shared components, directives, and pipes that can be used across multiple modules.
    • app-routing.module.ts: Defines the routing configuration for your application.
    • app.component.ts/html/scss: The root component of the application, which acts as the entry point of your application.
    • app.module.ts: The root module of your application where you declare and import components, services, and other modules.
  2. assets: This directory contains static assets like images, fonts, and other files that should be included in the final build.
  3. environments: Contains environment-specific configuration files, such as environment.ts for development and environment.prod.ts for production.
  4. index.html: The main HTML file of your application where the Angular app is bootstrapped.
  5. styles.scss: The global styles file that contains CSS rules applied to the entire application.
  6. main.ts: The entry point of the application where the Angular app is bootstrapped.
  7. polyfills.ts: Contains polyfills needed to support older browsers.

Lifecycle Hooks

Angular components have a lifecycle managed by Angular itself. These lifecycle hooks are methods that allow you to tap into key moments in the lifecycle of a component and take actions accordingly. Some of the commonly used lifecycle hooks are

  1. ngOnChanges: Called when one or more input properties of the component change.
  2. ngOnInit: Called once when the component is initialized after the first ngOnChanges and before rendering the view.
  3. ngDoCheck: Called during every change detection cycle. It allows you to implement custom change detection logic.
  4. ngAfterContentInit: Called after content (e.g., child components) has been projected into the component.
  5. ngAfterContentChecked: Called after every check of the projected content.
  6. ngAfterViewInit: Called after the component's view (and child views) has been initialized.
  7. ngAfterViewChecked: Called after every check of the component's view.
  8. ngOnDestroy: Called just before the component is destroyed. Used for cleaning up resources.

Angular Basics


Components

Components are the basic building blocks of Angular applications. They represent UI elements and are responsible for rendering data and responding to user interactions.

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>Hello, {{ name }}!</h1>
  `,
})
export class AppComponent {
  name = 'Angular Developer';
}

Modules

Modules in Angular help to organize the application by grouping related components, services, and other artifacts together.

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

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

Templates and Data Binding

Angular uses templates to define the UI. Data binding allows you to connect the component's data with the template to display dynamic content.

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>Hello, {{ name }}!</h1>
    <input [(ngModel)]="name" placeholder="Enter your name" />
  `,
})
export class AppComponent {
  name = 'Angular Developer';
}

Services

Services are used to share data, logic, and functionality between components. They are typically used for data retrieval and business logic.

// message.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  messages: string[] = [];

  addMessage(message: string): void {
    this.messages.push(message);
  }

  clearMessages(): void {
    this.messages = [];
  }
}

Templates and Directives


Templates

Angular templates are HTML-based and are used to define the user interface of components. They can contain expressions, bindings, and other Angular-specific syntax.

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>{{ title }}</h1>
    <p *ngIf="showMessage">Hello, {{ name }}!</p>
    <button (click)="toggleMessage()">Toggle Message</button>
  `,
})
export class AppComponent {
  title = 'Angular App';
  name = 'Angular Developer';
  showMessage = false;

  toggleMessage(): void {
    this.showMessage = !this.showMessage;
  }
}

Structural Directives

Structural directives manipulate the DOM's structure by adding, removing, or updating elements. They are prefixed with an asterisk (*ngIf, *ngFor, etc.).

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h2 *ngIf="isLoggedIn">Welcome, {{ userName }}!</h2>
    <ul>
      <li *ngFor="let item of items">{{ item }}</li>
    </ul>
  `,
})
export class AppComponent {
  isLoggedIn = true;
  userName = 'John Doe';
  items = ['Item 1', 'Item 2', 'Item 3'];
}

Attribute Directives

Attribute directives modify the appearance or behavior of elements by applying custom logic. They are used as attributes in HTML elements.

// highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor: string = 'yellow';

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter(): void {
    this.highlight(this.highlightColor);
  }

  @HostListener('mouseleave') onMouseLeave(): void {
    this.highlight(null);
  }

  private highlight(color: string | null): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p appHighlight="orange">Highlight me on hover</p>
  `,
})
export class AppComponent { }

Component Interaction

In Angular, components often need to communicate with each other. We'll explore different ways to achieve component interaction, such as using '@Input',' @Output', and 'services'.

@Input and @Output

'@Input' allows a parent component to pass data to its child component, while '@Output' allows a child component to emit events to its parent component.

Parent Component

// parent.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Component</h2>
    <app-child [childMessage]="message" (onChildEvent)="handleChildEvent($event)"></app-child>
    <p>Message from child: {{ childMessage }}</p>
  `,
})
export class ParentComponent {
  message = 'Hello from parent';

  handleChildEvent(message: string): void {
    this.childMessage = message;
  }
}

Child Component

// child.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <h3>Child Component</h3>
    <p>Message from parent: {{ parentMessage }}</p>
    <button (click)="sendMessageToParent()">Send Message to Parent</button>
  `,
})
export class ChildComponent {
  @Input() parentMessage: string;
  @Output() onChildEvent: EventEmitter<string> = new EventEmitter<string>();

  sendMessageToParent(): void {
    this.onChildEvent.emit('Hello from child');
  }
}

Services for Component Interaction

Services can be used to share data and communication logic between unrelated components.

Service

// message.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  private messageSource = new Subject<string>();
  message$ = this.messageSource.asObservable();

  sendMessage(message: string): void {
    this.messageSource.next(message);
  }
}

Sender Component

// sender.component.ts
import { Component } from '@angular/core';
import { MessageService } from './message.service';

@Component({
  selector: 'app-sender',
  template: `
    <h3>Sender Component</h3>
    <button (click)="sendMessageToReceiver()">Send Message to Receiver</button>
  `,
})
export class SenderComponent {
  constructor(private messageService: MessageService) { }

  sendMessageToReceiver(): void {
    this.messageService.sendMessage('Hello from sender');
  }
}

Receiver Component

// receiver.component.ts
import { Component, OnDestroy } from '@angular/core';
import { MessageService } from './message.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-receiver',
  template: `
    <h3>Receiver Component</h3>
    <p>Received message: {{ receivedMessage }}</p>
  `,
})
export class ReceiverComponent implements OnDestroy {
  receivedMessage: string = '';
  private subscription: Subscription;

  constructor(private messageService: MessageService) {
    this.subscription = this.messageService.message$.subscribe(message => {
      this.receivedMessage = message;
    });
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

Dependency Injection

In Angular, dependency injection is a fundamental concept that allows you to manage the creation and distribution of objects and their dependencies.

Service and Dependency Injection

Services in Angular are a way to share data and logic across different parts of your application. Dependency Injection (DI) is a design pattern where the class's dependencies (services) are injected into the class rather than the class creating them itself.

Service

// message.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  getMessage(): string {
    return 'Hello from MessageService';
  }
}

Component

// app.component.ts
import { Component } from '@angular/core';
import { MessageService } from './message.service';

@Component({
  selector: 'app-root',
  template: `
    <h2>{{ message }}</h2>
  `,
})
export class AppComponent {
  message: string;

  constructor(private messageService: MessageService) {
    this.message = this.messageService.getMessage();
  }
}

Injecting Services into Services

Services can also depend on other services. Angular's DI system takes care of providing the correct instances.

Service A

// service-a.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class ServiceA {
  getMessage(): string {
    return 'Hello from Service A';
  }
}

Service B

// service-b.service.ts
import { Injectable } from '@angular/core';
import { ServiceA } from './service-a.service';

@Injectable({
  providedIn: 'root',
})
export class ServiceB {
  constructor(private serviceA: ServiceA) {}

  getMessage(): string {
    const messageFromA = this.serviceA.getMessage();
    return `Message from Service B: ${messageFromA}`;
  }
}

Routing and Navigation

In Angular, routing allows you to build single-page applications (SPAs) with multiple views and navigate between them without a full-page reload

Setting Up Routes

To set up routes in an Angular application, you need to define a set of components and associate them with specific paths.

Create Components

ng generate component home
ng generate component about
ng generate component contact

Configure Routes

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: '**', redirectTo: '', pathMatch: 'full' } // Redirect all other routes to the home component
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule { }

Add Router Outlet

// app.component.html
<router-outlet></router-outlet>

Navigating Between Routes

To navigate between routes, you can use the 'Router' service provided by Angular.

In a component

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

@Component({
  selector: 'app-home',
  template: `
    <h2>Home Component</h2>
    <button (click)="goToAbout()">Go to About</button>
  `,
})
export class HomeComponent {
  constructor(private router: Router) {}

  goToAbout(): void {
    this.router.navigate(['/about']);
  }
}

Route Parameters

You can also pass parameters in the URL to capture dynamic values using route parameters.

Define the Route

// app-routing.module.ts
const routes: Routes = [
  { path: 'user/:id', component: UserProfileComponent },
];

Access Route Parameters

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

@Component({
  selector: 'app-user-profile',
  template: `
    <h2>User Profile</h2>
    <p>User ID: {{ userId }}</p>
  `,
})
export class UserProfileComponent {
  userId: string;

  constructor(private route: ActivatedRoute) {
    this.userId = this.route.snapshot.params['id'];
  }
}

Forms

Forms are an essential part of web applications, and Angular provides powerful tools to handle both template-driven forms and reactive forms.

Template-driven Forms

Template-driven forms are easy to use and suitable for simple forms with basic validation. They are defined within the template using Angular's form directives.

Create a Form

<!-- app.component.html -->
<form #myForm="ngForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="name">Name:</label>
    <input type="text" id="name" name="name" [(ngModel)]="formData.name" required>
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" [(ngModel)]="formData.email" required>
  </div>

  <button type="submit" [disabled]="myForm.invalid">Submit</button>
</form>

Handling Form Submission

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {
  formData = {
    name: '',
    email: ''
  };

  onSubmit(): void {
    // Handle form submission logic here
    console.log(this.formData);
  }
}

Reactive Forms

Reactive forms are more flexible and provide more control over form validation and data manipulation. They are built programmatically using form controls and form groups.

Create a Form

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
  myForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit(): void {
    this.myForm = this.formBuilder.group({
      name: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
    });
  }

  onSubmit(): void {
    // Handle form submission logic here
    console.log(this.myForm.value);
  }
}

Use Reactive Form in Template

<!-- app.component.html -->
<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="name">Name:</label>
    <input type="text" id="name" formControlName="name">
    <div *ngIf="myForm.get('name').invalid && myForm.get('name').touched">
      Name is required.
    </div>
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" formControlName="email">
    <div *ngIf="myForm.get('email').invalid && myForm.get('email').touched">
      Please enter a valid email.
    </div>
  </div>

  <button type="submit" [disabled]="myForm.invalid">Submit</button>
</form>

HTTP Client

In modern web applications, it's common to interact with backend APIs to fetch data or send data to the server. Angular provides an HTTP client module to make HTTP requests to APIs.

Setting Up HTTP Client

To use the Angular HTTP client, you need to import the HttpClientModule in your application module.

Import HttpClientModule

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  bootstrap: [AppComponent],
})
export class AppModule { }

Making GET Request

You can use the 'HttpClient' service to make GET requests to fetch data from an API.

Create a Service to Fetch Data

// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private apiUrl = 'https://api.example.com/data';

  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    return this.http.get<any>(this.apiUrl);
  }
}

Use the Service in a Component

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-root',
  template: `
    <h2>Data from API:</h2>
    <ul>
      <li *ngFor="let item of data">{{ item.name }}</li>
    </ul>
  `,
})
export class AppComponent implements OnInit {
  data: any[];

  constructor(private dataService: DataService) {}

  ngOnInit(): void {
    this.dataService.getData().subscribe((response) => {
      this.data = response;
    });
  }
}

Making POST Request

You can use the 'HttpClient' service to make POST requests to send data to an API.

Create a Service to Send Data

// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private apiUrl = 'https://api.example.com/data';

  constructor(private http: HttpClient) {}

  sendData(data: any): Observable<any> {
    return this.http.post<any>(this.apiUrl, data);
  }
}

Use the Service in a Component

// app.component.ts
import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <input [(ngModel)]="name" placeholder="Enter name">
      <button (click)="sendDataToApi()">Send Data</button>
    </div>
  `,
})
export class AppComponent {
  name: string;

  constructor(private dataService: DataService) {}

  sendDataToApi(): void {
    this.dataService.sendData({ name: this.name }).subscribe((response) => {
      // Handle response from the server if needed
    });
  }
}

Observables and RxJS

In Angular, RxJS is used extensively for handling asynchronous operations and working with streams of data. Understanding Observables and RxJS is crucial for building reactive applications.

Introduction to Observables

Observables are a powerful way to handle asynchronous data streams. They can represent a single value or a sequence of values over time.

import { Observable } from 'rxjs';

const observable = new Observable<number>((observer) => {
  observer.next(1);
  observer.next(2);
  observer.next(3);

  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});

const subscription = observable.subscribe({
  next: (value) => console.log(value),
  complete: () => console.log('Observable completed'),
});

// Output: 1, 2, 3, (after 1 second) 4, "Observable completed"

Operators in RxJS

RxJS provides a wide range of operators that allow you to transform, filter, combine, and handle observables in various ways.

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

const source$ = of(1, 2, 3, 4, 5);

source$.pipe(
  filter((value) => value % 2 === 0),
  map((value) => value * 2),
  tap((value) => console.log(`Processing value: ${value}`))
)
.subscribe((result) => console.log(`Result: ${result}`));

// Output: 
// Processing value: 4
// Result: 4
// Processing value: 8
// Result: 8
// Processing value: 10
// Result: 10

Common RxJS Operators

  • map: Transforms each emitted value.
  • filter: Filters values based on a predicate function.
  • tap: Perform side effects without modifying the values.
  • mergeMap: Projects each source value to an Observable and merges the results.
  • combineLatest: Combines multiple observables into one by emitting an array of the latest values.
  • switchMap: Projects each source value to an Observable, cancels the previous inner observable, and emits values only from the most recent inner observable.

Handling Errors

You can use the 'catchError' operator to gracefully handle errors in observables.

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

const source$ = of(1, 2, 3, 4, 5);

source$.pipe(
  map((value) => {
    if (value === 3) {
      throw new Error('Value cannot be 3');
    }
    return value;
  }),
  catchError((error) => {
    console.error('Error:', error.message);
    return throwError('Something went wrong');
  })
)
.subscribe(
  (result) => console.log(`Result: ${result}`),
  (error) => console.error(`Caught Error: ${error}`)
);

// Output:
// Result: 1
// Result: 2
// Error: Value cannot be 3
// Caught Error: Something went wrong
// Result: 4
// Result: 5

State Management with NgRx

As your application grows, managing the state becomes more complex. NgRx is a powerful state management library based on Redux, and it helps manage the state of your Angular application in a predictable and scalable way.

Introduction to NgRx

NgRx is a set of libraries that implement the Redux pattern in Angular. It consists of:

  1. Store: A centralized store that holds the state of the application.
  2. Actions: Describes the state changes as plain objects.
  3. Reducers: Pure functions that handle state changes based on actions.

Setting Up NgRx

To use NgRx in your application, you need to install the required packages and set up the store.

Install NgRx Packages

npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools --save

Create a Root Reducer

// app.reducer.ts
import { Action, createReducer, on } from '@ngrx/store';
import { EntityState, createEntityAdapter, EntityAdapter } from '@ngrx/entity';
import { Product } from './product.model';
import * as ProductActions from './product.actions';

export interface State extends EntityState<Product> {
  loading: boolean;
  error: string | null;
}

export const adapter: EntityAdapter<Product> = createEntityAdapter<Product>({
  selectId: (product) => product.id,
});

export const initialState: State = adapter.getInitialState({
  loading: false,
  error: null,
});

const productReducer = createReducer(
  initialState,
  on(ProductActions.loadProducts, (state) => ({ ...state, loading: true })),
  on(ProductActions.loadProductsSuccess, (state, { products }) =>
    adapter.setAll(products, { ...state, loading: false })
  ),
  on(ProductActions.loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  })),
  // Add more action handlers as needed...
);

export function reducer(state: State | undefined, action: Action) {
  return productReducer(state, action);
}

Create Actions and Effects

// product.actions.ts
import { createAction, props } from '@ngrx/store';
import { Product } from './product.model';

export const loadProducts = createAction('[Product] Load Products');
export const loadProductsSuccess = createAction(
  '[Product] Load Products Success',
  props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
  '[Product] Load Products Failure',
  props<{ error: string }>()
);
// Add more actions as needed...
// product.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { ProductService } from './product.service';
import { mergeMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import * as ProductActions from './product.actions';

@Injectable()
export class ProductEffects {
  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ProductActions.loadProducts),
      mergeMap(() =>
        this.productService.getProducts().pipe(
          map((products) =>
            ProductActions.loadProductsSuccess({ products })
          ),
          catchError((error) =>
            of(ProductActions.loadProductsFailure({ error: error.message }))
          )
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private productService: ProductService
  ) {}
}

Register NgRx Store in AppModule

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { AppComponent } from './app.component';
import { reducer } from './app.reducer';
import { ProductEffects } from './product.effects';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule,
    StoreModule.forRoot({ products: reducer }),
    EffectsModule.forRoot([ProductEffects]),
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Using NgRx in Components

In your components, you can access the store state using selectors and dispatch actions to modify the state.

// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Product } from './product.model';
import { loadProducts } from './product.actions';
import { selectAllProducts, selectLoading, selectError } from './app.reducer';

@Component({
  selector: 'app-product-list',
  template: `
    <div *ngIf="loading">Loading...</div>
    <div *ngIf="error">{{ error }}</div>
    <ul>
      <li *ngFor="let product of products">{{ product.name }}</li>
    </ul>
    <button (click)="onLoadProducts()">Load Products</button>
  `,
})
export class ProductListComponent implements OnInit {
  products$: Observable<Product[]>;
  loading$: Observable<boolean>;
  error$: Observable<string>;

  constructor(private store: Store) {}

  ngOnInit(): void {
    this.products$ = this.store.pipe(select(selectAllProducts));
    this.loading$ = this.store.pipe(select(selectLoading));
    this.error$ = this.store.pipe(select(selectError));
  }

  onLoadProducts(): void {
    this.store.dispatch(loadProducts());
  }
}

Authentication and Authorization

In modern web applications, implementing user authentication and authorization is crucial to control access to certain parts of the application and protect sensitive data.

Authentication: Authentication is the process of identifying users and confirming their identity. In Angular, you can use various methods for authentication, such as JSON Web Tokens (JWT), OAuth, or session-based authentication.

Authorization: Authorization is the process of determining what actions a user is allowed to perform within the application. It is typically based on the user's role or permissions.

Implementing Authentication and Authorization: The implementation of authentication and authorization can vary based on your specific requirements and the backend server you are using. Below is a general outline of how it can be done:

  1. User Registration and Login: Implement API endpoints for user registration and login. When a user registers or logs in, the server should issue an access token (JWT) that will be used for subsequent requests.
  2. JWT Authentication: Store the access token in the browser's local storage or cookies. Use Angular's HTTP Interceptor to attach the access token to outgoing requests as an Authorization header.
  3. Protected Routes: Use Angular's Route Guards to protect certain routes from unauthorized access. Create a route guard that checks if the user is authenticated and has the necessary roles or permissions to access the route.
  4. Role-based Authorization: Implement server-side logic to check the user's roles or permissions when accessing certain resources or performing actions. The server should respond with an appropriate status code if the user is not authorized.
// auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { AuthService } from './auth.service';

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

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean | UrlTree {
    if (this.authService.isAuthenticated()) {
      // Check if the user has necessary roles or permissions to access the route
      // Implement your own logic based on your server-side roles/permissions system
      return true;
    } else {
      // Redirect to login page if not authenticated
      return this.router.createUrlTree(['/login']);
    }
  }
}

AuthService

// auth.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private isAuthenticated = false;

  login(username: string, password: string): boolean {
    // Implement login logic here (e.g., send login request to the server)
    // If successful, set isAuthenticated to true and return true
    // Otherwise, return false
    this.isAuthenticated = true;
    return true;
  }

  logout(): void {
    // Implement logout logic here
    this.isAuthenticated = false;
  }

  isAuthenticated(): boolean {
    return this.isAuthenticated;
  }
}

Internationalization (i18n) and Localization

Internationalization is the process of designing an application to be adaptable to different languages and regions, while Localization is the process of translating the application to specific languages and regions.

Setting Up i18n

Angular provides built-in support for i18n through the '@angular/localize' package. It allows you to mark strings in the templates for translation and manage multiple language translations.

Enable i18n in 'angular.json'

{
  "projects": {
    "your-project-name": {
      "i18n": {
        "sourceLocale": "en-US",
        "locales": {
          "fr": "src/locale/messages.fr.xlf",
          "es": "src/locale/messages.es.xlf"
        }
      }
    }
  }
}

Mark Strings for Translation

<!-- app.component.html -->
<p i18n="@@welcomeMessage">Welcome to my app!</p>

Extract Translatable Strings

ng extract-i18n

Translate Strings

<!-- src/locale/messages.fr.xlf -->
<trans-unit id="welcomeMessage" datatype="html">
  <source>Welcome to my app!</source>
  <target>Bienvenue dans mon application !</target>
</trans-unit>

Switching Languages at Runtime

You can implement a language switcher to allow users to change the language at runtime.

Create a Language Service

// language.service.ts
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';

@Injectable({
  providedIn: 'root',
})
export class LanguageService {
  constructor(private translateService: TranslateService) {}

  setLanguage(language: string): void {
    this.translateService.use(language);
  }
}

Implement Language Switcher

<!-- app.component.html -->
<button (click)="switchLanguage('en')">English</button>
<button (click)="switchLanguage('fr')">Français</button>
// app.component.ts
import { Component } from '@angular/core';
import { LanguageService } from './language.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {
  constructor(private languageService: LanguageService) {}

  switchLanguage(language: string): void {
    this.languageService.setLanguage(language);
  }
}

Pluralization and Date/Number Formatting

i18n also supports pluralization and formatting dates and numbers for different locales.

Pluralization

<!-- app.component.html -->
<p i18n="{minutes, plural, =0 {No minutes} =1 {One minute} other {{{minutes}} minutes}}">{{ minutes }}</p>

Date/Number Formatting

<!-- app.component.html -->
<p>{{ today | date }}</p>
<p>{{ amount | currency:'EUR' }}</p>

Unit Testing in Angular

Testing is a crucial part of software development, and Angular provides built-in support for writing unit tests for components, services, and other parts of your application.

Setting Up Testing Environment

Angular CLI sets up testing environments for you. You can use 'ng test' to run tests using Karma test runner.

Component Testing

Angular provides 'TestBed' and 'ComponentFixture' to create and test components.

Basic Component Test

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [AppComponent],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create the app', () => {
    expect(component).toBeTruthy();
  });

  it('should have a title', () => {
    const title = fixture.nativeElement.querySelector('h1').textContent;
    expect(title).toContain('My App Title');
  });
});

Service Testing

You can test Angular services as regular TypeScript classes.

Basic Service Test

import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

describe('DataService', () => {
  let service: DataService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [DataService],
    });
    service = TestBed.inject(DataService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should fetch data from API', () => {
    const testData = [{ id: 1, name: 'Item 1' }];

    service.getData().subscribe((data) => {
      expect(data).toEqual(testData);
    });

    const req = httpMock.expectOne('https://api.example.com/data');
    expect(req.request.method).toBe('GET');
    req.flush(testData);
  });
});

Mocking Dependencies

You can use test doubles (spies or mocks) to mock dependencies and isolate the unit under test.

Code Coverage

Angular CLI provides code coverage reports to measure how much of your code is covered by tests.

End-to-End Testing with Protractor

End-to-end (E2E) testing is used to simulate user interactions and test the flow of your application from start to finish. Protractor is the recommended E2E testing framework for Angular applications.

Setting Up Protractor

Protractor requires a test configuration file and uses Selenium WebDriver to interact with the browser.

Install Protractor and WebDriver Manager:

npm install -g protractor
webdriver-manager update

Create a Protractor Configuration

// protractor.conf.js
exports.config = {
  directConnect: true,
  capabilities: {
    browserName: 'chrome',
  },
  specs: ['./src/app.e2e-spec.ts'],
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function () {},
  },
};

Writing Protractor Tests

Protractor tests are written in Jasmine and use WebDriver to simulate user interactions

Basic Test Example

// app.e2e-spec.ts
import { browser, by, element } from 'protractor';

describe('App', () => {
  beforeAll(() => {
    browser.get('/');
  });

  it('should display welcome message', () => {
    const welcomeMessage = element(by.css('h1')).getText();
    expect(welcomeMessage).toEqual('Welcome to my app!');
  });

  it('should navigate to about page', () => {
    const aboutLink = element(by.css('a[href="/about"]'));
    aboutLink.click();

    const aboutPageTitle = element(by.css('h2')).getText();
    expect(aboutPageTitle).toEqual('About Us');
  });
});

Running Protractor Tests

To run your Protractor tests, use the following command:

protractor protractor.conf.js

Deployment

Deployment is the process of making your Angular application available to users. There are various ways to deploy an Angular app, depending on your hosting platform and requirements.

1. Build your Angular Application

Before deployment, you need to build your Angular application for production. This step bundles and optimizes your code for better performance.

ng build --prod

The '--prod' flag enables production mode and applies various optimizations to the build.

2. Hosting Options

There are several hosting options available for deploying Angular applications

  • Hosting Services: You can use hosting services like Firebase Hosting, Netlify, GitHub Pages, or AWS S3 to deploy your app easily.
  • Traditional Web Servers: Deploying on traditional web servers like Apache, Nginx, or Microsoft IIS is also a common option.
  • Cloud Platforms: Cloud platforms like AWS, Google Cloud, or Microsoft Azure offer various options for hosting and deploying Angular applications.

3. Server Configuration

If your application uses server-side rendering (SSR) or requires server configuration (e.g., handling URL rewrites for Angular's routing), you'll need to set up the appropriate server configuration.

4. Domain and SSL

If you're deploying on your own domain, you'll need to configure your DNS settings to point to the hosting server. Additionally, consider enabling SSL (HTTPS) for improved security.

5. Continuous Integration and Deployment (CI/CD)

For continuous deployment, you can integrate your code repository with CI/CD tools like Jenkins, CircleCI, GitHub Actions, or Azure DevOps to automate the deployment process.

Deployment is a crucial step in the software development lifecycle, as it makes your application accessible to users worldwide.