Angular 2+: Base Component Alternative

Jan 26, 2020 19:56 · 824 words · 4 minute read angular typescript

Hello again, base functionality is an inevitable requirement. I’m going to talk about a different base component approach in the Angular. Most of the examples create a base component and inject services to it. Later, you create components from it. However, internet has many examples that could lead you to write bad code.

DISCLAIMER: This story does not prove a pattern or anti-pattern. It just provides a different point of view.

The blog picture

Let’s see a common example;

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

@Component({
  selector: 'app-base',
  template: `no ui`
})
export class BaseComponent implements OnInit {

  constructor(private pageService: PageService) { }

  async ngOnInit() {
    await this.pageService.load(this);
  }
}

Assume that, every route have inherited from the same base component and the page content loads asynchronously.

export class HomePageComponent extends BaseComponent implements OnInit {
  constructor(public readonly pageService: PageService) {
    super(pageService);
  }
  
  async ngOnInit() {
    await super.ngOnInit();
  }
}

So the page’s content must be loaded at ngOnInit asynchronously. We are calling the base’s function to work things out. So far so good. The code seems clean and legit. Most of the examples on the internet are the same.

Problem

Angular projects could be bigger than you might think. As things could get more complex as the project evolves, so base component would require more services and data. For example, BaseComponent could require a filter service, widget service or more different services. Therefore, you must inject these services into all inherited components as well.

export class BaseComponent implements OnInit {
  ...

  constructor(public readonly pageService: PageService,
      public readonly filterService: FilterService,
      public readonly widgetService: WidgetService,
      public readonly service2: Service2,
      ...) { 
    this.name = this.constructor.name;
  }

  ...
}
export class AboutBasePageComponent extends BaseComponent implements OnInit {

   constructor(public readonly pageService: PageService,
      public readonly filterService: FilterService,
      public readonly widgetService: WidgetService,
      public readonly service2: Service2,
      ...) { 
    super(pageService, filterService, service1, service2, ...);
  }

  ngOnInit() {
    super.ngOnInit();
  }

}

As a result you have code repetition, and static analysis tools will report such kind of code as a duplication.

Solution

The complex part is too much services and injections. Despite that there exists a much simpler way. You could create a base class with all common properties rather than an angular component.

Angular’s router engine has the router-outlet component. In the course of interaction with the pages, the events would provide the activated/deactivated component cycle. Rather than requiring a lot of services in the constructor, you can manage them in the events. Let’s see the code

export class BasePageComponent {
    name: string;
    title: string;
    content: string;

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

export class HomePageComponent extends BasePageComponent implements OnInit {

  constructor() { super() }

  ngOnInit() {
  }
}

AppComponent.ts

export class AppComponent {
  title: string = '';
  content: string = '';

  constructor(private readonly pageService: PageService) {
  }

  onActivate(e) {
    if (e instanceof BasePageComponent) {
      this.pageService.getContent(e);
      this.content = e.content;
    }
  }

  onDeactivate(e) {
    // clear resources
  }
}

AppComponent.html

<div>
  <div class="content">
    {{ content }}
  </div>
  <router-outlet (activate)='onActivate($event)'
    (deactivate)='onDeactivate($event)'></router-outlet>
</div>

When a route clicked, the activate event will be fired. And, the page service will load the page’s content. By this way, we don’t need to inject the page service into each component. This one is very simple example. I would like to take things further.

However, this could change due to requirements. Assume that, we want to share a common functionality e.g. change page function, or loading indicator. We could use EventEmitter to support such functionalities.

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

export class BasePageComponent {
    name: string;
    title: string;
    content: string;

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

    changePageEmitter: EventEmitter<string> = new EventEmitter<string>();
    changePage(name: string) {
        this.changePageEmitter.emit(name);
    }
}

You might want to use a wrapper function to emit the EventEmitter.

import { Component } from '@angular/core';
import { PageService } from './services/page.service';
import { BasePageComponent } from './components/BasePageComponent';
import { Router } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title: string = '';
  content: string = '';

  constructor(private readonly pageService: PageService,
    private readonly router: Router) {
  }

  onActivate(e) {
    if (e instanceof BasePageComponent) {
      this.pageService.getContent(e);
      this.content = e.content;
      e.changePageEmitter.subscribe((name) => {
        this.router.navigateByUrl(`/${name}`);
      });
    }
  }

  onDeactivate(e) {
    // clear resources
    if (e instanceof BasePageComponent) {
      e.changePageEmitter.unsubscribe();
    }
  }
}

An example usage case

export class HomePageComponent extends BasePageComponent implements OnInit {
{
  ...
  goAbout() {
    this.changePage('about');
    return false;
  }
  ...
}
<a href="#" (click)="goAbout()">Go to about page</a>

As I mentioned before, you might use this.changePageEmitter.emit(‘about’) instead of this.changePage(‘about’). It is just a shortcut.

To sum up, the main point of this story is decrease the code duplication and centralize base functionalities. In my opinion, injecting such common services for page base component requirement is not mandatory. You can manage it within AppComponent. However, the methodology could change with your requirement. I hope you enjoyed it.

Angular Component Inheritance
_Encapsulate your common code in a base component and extend to your heart’s content! If you’ve spent any time in…_alligator.io