Skip to content

A recap: Angular

A Recap: Angular

Sometimes I forget things. It is normal. Humans forget, the lives we lead are bustling and it is all too easy for memories to just – disappear. That is why I’ve decided to create a couple of articles in order to preserve and catalog various different technologies and lessons I learned from the world of software development. These small notes might be useful as a refresher, for example when preparing for an interview, or when I just want to remind myself on how a different mechanic works.

I’ll start with Angular.

First things first, one of the prerequisites for learning Angular is knowing JavaScript (JS) and by extension TypeScript (TS). I plan on writing a separate ‘recap’ article on these two topics, so I’ll skip going into detail on them. Let us talk Angular.

Angular

Angular is a framework for building client-side applications. It provides fupport for MVC and MVVM architectures. MVC stands for Model-View-Controller and it is a software design pattern that divides the program logic into these three components. MVVM stands for Model-View-ViewModel.

Directives

In my opinion the basis of Angular is the Angular Directive. It is a class that can modify the structure of the DOM, or modify the attributes in the DOM. The main kinds of directives in Angular are component directives, which have a template, structural directives that change DOM layout by adding and removing DOM elements, and attribute directives that change the appearance or behavior of an element, component or another directive. Angular supplies a number of built-in directives that begin with the ng prefix.

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

@Directive({ // the @Directive decorator marks this class as an attribute directive
  selector: '[appSimpleColorChange]'

export class SimpleColorChangeDirective {

  constructor(private el: ElementRef, private renderer: Renderer2) {
    this.changeColor('blue'); // Default color
  }

  private changeColor(color: string) {
    this.renderer.setStyle(this.el.nativeElement, 'color', color);
  }
}
<p appSimpleColorChange>This text will be blue.</p>

Modules

Angular applications are modular, and they use the concept of Angular modules (NgModules) to organize code into cohesive blocks. Each module groups related components, services, directives, pipes and other modules.

Change Detection

Change detection is one of Angular’s core features. It allows the framework to detect changes to the data and update the DOM accordingly, so that the UI can stay in sync with the data model. This process is triggered automatically when certain events occur such as DOM events, timer events, HTTP requests and such. Angular then performs a change detection cycle that starts from the root component and moves down the component tree to all the children, then for each component (each component has a change detector). The change detector uses a mechanism called ‘dirty checking’ which checks whether the value of template expression has changed. Then, if Angular detects a change in data values, it updates the DOM.

Change Detection Strategies

There are two change detection strategies that can be configured for each project

Default which uses change detection on every async event. This is safe for most applications but can lead to performance issues in large applications with complex UIs.

OnPush which instructs Angular only to run the change detection process when new references are passed to inputs. Which means that the component configured with onPush will only be checked if any of the properties change, an event originates from the component or its children or you explicitly trigger change detection via ChangeDetectorRef.

Zone.js

Zone.JS is an Angular library that provides an execution context that persists across asynchronous operations. Basically it patches all the standard async APIs to notify Angular when a task starts or finishes. This allows Angular to automatically trigger change detection whenever such a task completes, ensuring that the UI is updated with the latest data.

What are lifecycle hooks in Angular?

Lifecycle hooks are special methods that enable developers to perform specific actions at various stages in the life of the component, for example – when it is created, rendered, has data updated or when it is destroyed. Lifecycle hooks are called in sequence, giving the developer control on how and when they react to changes.

  • ngOnInit – triggered after the component/directive is initialized, it is used for initialization work and fetching data for the template
  • ngOnChanges – triggered before ngOnInit and whenever data-bounds property change. It is used as a way to react on input changes
  • ngDoCheck – triggered on every change detection run, it is run after ngOnInit and ngOnChanges. It is used to implement custom change detection when Angular’s is insufficient
  • ngAfterContentInit - triggered after Angular projects external content into component’s view or into the view that a directive is in, it is suitable for querying content children
  • ngAfterContentChecked - triggered after the default change detector has finished checking all contents of a component
  • ngAfterViewInit - triggered after Angular has fully initialized component’s views and child views, it is good to triggered DOM manipulation in this lifecycle hook
  • ngAfterViewChecked - triggered after the components view are checked
  • ngOnDestroy - triggered just before Angular destroys the component, inside the lifecycle we usually trigger cleanup methods and unsubscribing from observables, detaching event handlers, stopping timers etc.

Dependency Injection (DI) in Angular?

Dependency Injection (DI) is an Angular design pattern and a core mechanism for resolving dependencies in your application. With DI a class receives dependencies from an external source instead of creating them itself. DI allows a class to receive its dependencies from an external source rather than creating them itself. This pattern facilitates greater modularity, easier testing, and increased flexibility in Angular applications.

What are the key components of dependency injection in Angular?

Key components of dependency injection in Angular are injectors, providers, tokens and the injection process.

Injectors are containers for a collection of service instances. Injectors are responsible for creating instances of services and injecting them into classes like components and other services. When the Angular application is bootstrapped it creates a root injector. This injector is available application-wide. Components can also have their own injectors that form an injection-tree.

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

@Injectable({
    providedIn: 'root'
})
export class MyServiceService {
    getMessage() {
        return 'Hello from MyService!';
    }
}

Providers is an instruction to the DI framework on how to obtain the value for the dependency. Providers tell the injector how to create an instance of a service. For example using the metadata decorator @Injectable() and providing the providedIn property to declare the level of the injector. Providers can be configured in different ways.

Tokens are identifiers to the dependency. The most common token is the class type:

constructor(private myService: MyService) {}

What is the async pipe?

Async pipe handles asynchronous data streams directly in the template. It is commonly used with Observables and Promises, allowing you to bind the output of an asynchronous operation directly to the view. Some features of the async pipe include automatic subscription and unsubsription and automatic update of the view. In the snippet below data$ is an asynchronous property. The async pipe subscribes to data$, waits for the value and renders it to the template.

<div>{{ data$ | async }}</div>

What is lazy loading?

Lazy loading is a design pattern in Angular whose primary purpose is to optimize performance. Lazy loading defers the loading of feature modules until they are needed. This reduces the initial load time of the application by splitting the app into multiple bundles and loading them on demand. The snippet below tells Angular to load the FeatureModule only when /feature route is accessed.

const routes: Routes = [
  {
    path: 'feature',
    loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule)
  },
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', redirectTo: '/home' }
];

What is RXJS?

RXJS (Reactive Extensions for JavaScript) is a library which makes it easier to compose asynchronous or callback-based code using Observables. It is a powerful tool for handling reactive programming. RXJS is deeply integrated in Angular, and a lot of services that include data handling, HTTP requests, event handling and reactive forms use RXJS under the hood.

What is an observable?

An observable represents a collection of possible or future values or events. It is used when handling asynchronous data flows and handling events. Observables are comparable to Promises (another asynchronous concept) but observables can emit multiple values over time, they do not start emitting values until they are subscribed to, they can be cancelled and they can be transformed using operators, making it easier to handle complex data flows.

In the snippet below we are creating an observable and subscribing to it, the observable will emit two values and then it will complete.

import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {
  subscriber.next('Hello');
  subscriber.next('World');
  subscriber.complete();
});

observable.subscribe({
  next(x) { console.log(x); },
  error(err) { console.error('something went wrong: ' + err); },
  complete() { console.log('done'); }
});

Above we created the observable using the Observable constructor. Another way of creating an observable is using creation operators:

import { of } from 'rxjs';

const observable = of(1, 2, 3);

observable.subscribe({
  next(x) { console.log(x); },
  complete() { console.log('done'); }
});

What are RXJS operators?

Observables support operators – functions that enable manipulation of Observables. Some of the most common operators are mapping, filtering and combining streams. They can be piped – meaning that the result of one operator can be used as an input to another operator.

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

const observable = from([1, 2, 3, 4, 5]);

observable.pipe(
  filter(x => x % 2 === 0),
  map(x => x * 10)
).subscribe(value => console.log(value));

How to create your own (simplistic) implementation of an observable?

Let us implement a simple implementation of an Observable to help us understand the concept a bit more. This isn’t a fully fledged Observable, but it is a good starting point.

First let us implement the interfaces (because we are writing our implementations in TypeScript):

interface Observer<T> {
  next: (value: T) => void;
  error: (err: any) => void;
  complete: () => void;
}

interface Subscription {
  unsubscribe: () => void;
}

interface SafeObserver<T> extends Observer<T> {
  isUnsubscribed: boolean;
}

Observer is an object that receives notifications from the observable. Observer has three methods next, error and complete. In order to emit values from the observable, we would just need to call the next method, an close it up with complete. We would then provide the Observer object to the observable function.

Next, let us implement the Observable class:

class Observable<T> {
  private subscribeFunction: (observer: Observer<T>) => void;

  constructor(subscribeFunction: (observer: Observer<T>) => void) {
    this.subscribeFunction = subscribeFunction;
  }

  subscribe(observer: Observer<T>): Subscription {
    const safeObserver = this.createSafeObserver(observer);
    this.subscribeFunction(safeObserver);

    return {
      unsubscribe: () => {
        safeObserver.isUnsubscribed = true;
      }
    };
  }

  private createSafeObserver(observer: Observer<T>): Observer<T> {
    const safeObserver = {
      next: (value: T) => {
        if (!safeObserver.isUnsubscribed && observer.next) {
          observer.next(value);
        }
      },
      error: (err: any) => {
        if (!safeObserver.isUnsubscribed && observer.error) {
          observer.error(err);
        }
        safeObserver.isUnsubscribed = true;
      },
      complete: () => {
        if (!safeObserver.isUnsubscribed && observer.complete) {
          observer.complete();
        }
        safeObserver.isUnsubscribed = true;
      },
      isUnsubscribed: false
    };

    return safeObserver;
  }
}

And lastly let us end with the use case of the Observable:

const myObservable = new Observable<number>((observer) => {
  let count = 0;
  const intervalId = setInterval(() => {
    observer.next(count++);

    if (count > 5) {
      observer.complete();
      clearInterval(intervalId);
    }
  }, 1000);

  return () => {
    clearInterval(intervalId);
  };
});

const myObserver: Observer<number> = {
  next: (value) => console.log('Value:', value),
  error: (err) => console.error('Error:', err),
  complete: () => console.log('Completed'),
};

const subscription = myObservable.subscribe(myObserver);

setTimeout(() => {
  subscription.unsubscribe();
  console.log('Unsubscribed');
}, 7000);

A great resource that helped me grasping the concepts of observables is this one.

What is the router outlet?

Router outlet is a directive which acts as a placeholder where the Angular Router should insert the component that matches the current route. It is one part of Angular’s routing mechanism. The directive is placed inside the template of the component.

<!-- some HTML -->
<router-outlet></router-outlet>
<!-- some HTML -->

How are routes configured?

Angular Routes are usually defined in a configuration object, typically inside a routing module. Each individual route matches an URL path to a component.

// 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';

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

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

In order to define routes in Angular, we need to import the RouterModule and provide them the list of routes. RouterModule is used to configure and manage navigation and routing, and additionally it provides services and directives for managing application states and views.

What is the forRoot(routes) doing?

Router.forRoot(routes) is part of route configuration. The method forRoot is used specifically to define top-level routes, if we wanted to define child routes in feature modules, we would use RouterModule.forChild(routes).

What is a singleton service in Angular?

A singleton service in Angular is a service that is instantiated only once throughout the lifetime of an application. This means that all components, directives, and other services that depend on this service will share the same instance, ensuring a single source of truth and consistency in the application’s state and behavior.

In order to create a singleton service, you need to make sure that the service is provided at the root level.

How to provide a singleton service?

As mentioned in order to create a singleton service you need to ensure that the service is provided at the root level. One method is using the providedIn property, making sure that the service is provided in the root injector.

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

@Injectable({
  providedIn: 'root'
})
export class SingletonService {
  constructor() { }
}

Another way of creating a singleton service is by providing it in the AppModule.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { SingletonService } from './singleton.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [SingletonService], // This ensures the service is a singleton
  bootstrap: [AppComponent]
})
export class AppModule { }

Some useful links

Angular Official Documentation

Angular Interview Questions & Answers

Top 100+ Angular Interview Questions and Answers in 2024

Tags: