Sometimes you need to be able to inherit a chunk of functionality from a component into other components in Angular. In this story I will show you how easy it is.

Why

I have a practical example to explain why you might need to do this, in an app I am working on I have a dashboard with a ngFor list of tools. When you click on each of the tools you are redirected to a details component, different for each tool. Why the sh1t would you ever do that for you might ask? and you are right, in a normal world I would have a details component that binds values for all of them. However in this app each details page will have different HTML and styles, effectively changing the layout and content in it.

This sounds like it should be a CMS, and I also agree on that one, but honestly we didn’t have time to invest on a CMS solution and creating a bunch of angular components for a small number of details pages didn’t sound that bad. This also allows us to integrate healthchecks on the content and styles that the content teams will be adding.

Anyway, no need to sell the reasoning behind the decision, what matters here is that each details page also shares some logic in the component class, specifically loading up the data for the tool being visited, and enabling some events that should happen when NavigationEnd triggers in each of the details component.

In any case, I quickly realized that I didn’t want to copy all of that code on each and every single one of the details pages. Can you imagine the pain if I ever need to make a small change to that logic? I wanted those details to be empty if possible, not even a spec file, just html and scss so content editors can just open the file and change it to their satisfaction without breaking anything else in the project.

Ok so here is my base Details component:

import { Component, OnInit, HostBinding, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';

import { DataService } from '../../services/data.service';

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

@Component({
  selector: 'details-component',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.scss']
})
export class DetailsComponent implements OnInit, OnDestroy {
  public _routeSubscription: Subscription;

  constructor(protected router: Router, 
    protected activatedRoute: ActivatedRoute,
    protected dataService: DataService) {

    // Subscribe to the router events to listen to NavigationEnd
    // with this monstrocity
    this._routeSubscription = this.router
      .events
      .pipe(
        filter(e => e instanceof NavigationEnd),
        map(() => this.activatedRoute),
        map(route => {
          if (route.firstChild) {
              route = route.firstChild;
          }

          return route;
        }),
        filter(route => route.outlet === 'primary'),
        mergeMap(route => route.data)
      )
      .subscribe(data => {
        // Do what we need in here
      });
  }

  ngOnInit() {
      // Initialize the data based on the current route
    this.dataService.resolve(this.activatedRoute);
  }

  ngOnDestroy() {
    // Destroy the subscription to avoid leaks
    if (this._routeSubscription) { this._routeSubscription.unsubscribe(); }
  }
}

Ok cool, it is a basic component that gets a data service injected which loads the tool details from an API depending on the route currently active. Nothing special.

How do we share this code in the ‘child’ details components?

Well, we can just extend from it and point to separate html and scss urls like:

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

// This component extends the default details
import { DetailsComponent } from '../details/details.component';

@Component({
  selector: 'tool-extended-one',
  templateUrl: './tool-extended-one.component.html',
  styleUrls: ['./tool-extended-one.component.scss']
})
export class ToolExtendedOneComponent extends DetailsComponent {}

And that’s it, seriously. This works fine, in your routes you will probably point to this component like:

const routes: Routes = [
  {
      { path: 'tool-one.html', component: ToolExtendedOneComponent },
      { path: 'tool-two.html', component: ToolExtendedTwoComponent },
      // ...
      // more custom extended components
      // ...
      // and finally for all urls that dont macth use the base Component
      { path: '**', component: DetailsComponent }
 }

Handling dependencies

Now comes a different problem, how do we inject dependencies in the extended components constructors? Since we are using inheritance it automatically complains that the constructor of the super class needs to be invoked too. Let’s say for example in tool one component we need to access a separate service, but the moment we add a constructor to the child class, we need to invoke super(), which also has the data service and the route services injected into it.

This is how:

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

// This component extends the default details
import { DetailsComponent } from '../details/details.component';

import { SecondService } from '../services/second.service';

@Component({
  selector: 'tool-extended-one',
  templateUrl: './tool-extended-one.component.html',
  styleUrls: ['./tool-extended-one.component.scss']
})
export class ToolExtendedOneComponent extends DetailsComponent {

   constructor(private secondService: SecondService) {
           super(); 
        // This is broken, we need to pass the dependencies to the base class constructor
    }

}

At this point you might be tempted to just add the data service and the routing services here and then pass them into the super() call, but that is just spaghetti. Just don’t do it.

First of all, you will have the private/protected/public variables already exist issues, which will force you to start renaming the dependencies in the child class, to pass them to the base class. You just added carbonara sauce to your spaghetti.

The Solution

For the dependencies there is a much more elegant solution, using the StaticInjector. I assume you are using Angular5 or 6 at least, if you are not, check online for ReflectiveInjector instead.

  1. Add a ServiceLocator class that exposes the static injector:
import {Injector} from "@angular/core";

export class ServiceLocator {
  static injector: Injector;
}
  1. Set it up in your AppModule class:
@NgModule(...)
export class AppModule {
  constructor() {
    ServiceLocator.injector = Injector.create(
      'ActivatedRoute': { provide: ActivatedRoute, deps: [] },
      'DataService': { provide: DataService, deps: [] },
      'Router': { provide: Router, deps: [] }
      // Add more in here if needed
    );
  }
}
  1. Use this ServiceLocator class in your base DetailsComponent to not need to inject the dependencies through its constructor:
@Component({
  selector: 'details-component',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.scss']
})
export class DetailsComponent implements OnInit, OnDestroy {
  protected dataService: DataService;
  protected activatedRoute: ActivatedRoute;
  protected router: Router;

  constructor() {
     this.dataService = ServiceLocator.injector.get(DataService);
     this.activatedRoute = ServiceLocator.injector.get(ActivatedRoute);
     this.router = ServiceLocator.injector.get(Router);
  }

  // The rest of the component doesnt change

}
  1. Now you can add new dependencies to your child components without breaking the base class constructor:
import { SecondService } from '../services/second.service';

@Component({
  selector: 'tool-extended-one',
  templateUrl: './tool-extended-one.component.html',
  styleUrls: ['./tool-extended-one.component.scss']
})
export class ToolExtendedOneComponent extends DetailsComponent {

   constructor(private secondService: SecondService) {
           super(); 
        // Now this works!! the base class doesnt have any params in the constructor
    }

}

That’s it. Simple, you only keep your code in one place, you can test the crap out of your base component, and inheritance works without breaking angular dependency injection.