Tutorial

How To Extend Classes with Angular Component Inheritance

Angular

Introduction

If you have spent any time in Angular, you may have come across a time when you wanted to share data or functionality and you used services/providers.

What if, instead of common data functionality, you want common UI functionality? For example, consider a simple scenario where you want to navigate from one component to another using buttons. A simple way to implement this is to create a button, call a method in the code which then uses the Angular router to navigate to the page. And what if you didn’t want to repeat the same code in each component? Typescript and Angular give you a way to handle this encapsulation. Inherited components!

Using class inheritance in TypeScript, you can declare a base component that contains common UI functionality and use it to extend any standard component you’d like. If you’re used to any language focused on object-oriented methodology such as C#, you’ll recognize this approach pretty easily as inheritance. With Typescript, it’s merely known as extending a class.

In this article, you will encapsulate your common code in a base component and extend it to create reusable page navigation buttons.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v16.4.2, npm v7.19.1, and @angular/core v12.1.1.

Step 1 – Setting Up the Project

Let’s start by using the Angular CLI to create a new app.

If you haven’t installed the Angular CLI before, install it globally using npm:

  • npm install -g @angular/cli

Next, create the new app using the CLI:

  • ng new AngularComponentInheritance --style=css --routing --skip-tests

Note: We are passing some flag to the ng new command to add routing to our app (--routing) and not to add any testing files (--skip-tests).

Navigate to the project directory:

  • cd AngularComponentInheritance

Then, run the following command to create a Base component:

  • ng generate component base --inline-template --inline-style --skip-tests --module app

Note: The --module flag here specifies the module to which the component should belong to.

This command will create a base.component.ts file and adds it as a declaration for the app module.

Step 2 – Building the Base Component

Open up the base/base.component.ts file with your code editor:

src/app/base/base.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-base',
  template: `
    <p>
      base works!
    </p>
  `,
  styles: [
  ]
})
export class BaseComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

This UI will never be shown so no need to add anything other than a simple UI.

Next, inject the Router into the component:

src/app/base/base.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-base',
  template: `
    <p>
      base works!
    </p>
  `,
  styles: [
  ]
})
export class BaseComponent implements OnInit {

  constructor(public router: Router) { }

  ngOnInit(): void {
  }

}

Take note of the accessibility level. It’s important to keep this declaration “public” due to the inheritance.

Next, add a method to the base component called openPage which takes a string and uses it to navigate to a route (note: use the tick below instead of the single quote for a template literal):

src/app/base/base.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-base',
  template: `
    <p>
      base works!
    </p>
  `,
  styles: [
  ]
})
export class BaseComponent implements OnInit {

  constructor(public router: Router) { }

  ngOnInit(): void {
  }

  openPage(routename: string) {
    this.router.navigateByUrl(`/${routename}`);
  }
}

This gives us the base functionality we need, so let’s use it on a few components.

Step 3 – Inheriting Components

We’ll run three Angular CLI commands to generate some more components:

  • ng generate component pageone --skip-tests --module app
  • ng generate component pagetwo --skip-tests --module app
  • ng generate component pagethree --skip-tests --module app

These commands will generate a PageoneComponent, PagetwoComponent, and PagethreeComponent and adds all three as delcarations for app.

Open app-routing.module.ts, which was created by the CLI when we first generated the app, and add a path for each page:

src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageoneComponent } from './pageone/pageone.component';
import { PagetwoComponent } from './pagetwo/pagetwo.component';
import { PagethreeComponent } from './pagethree/pagethree.component';

const routes: Routes = [
  { path: '', component: PageoneComponent },
  { path: 'pageone', component: PageoneComponent },
  { path: 'pagetwo', component: PagetwoComponent },
  { path: 'pagethree', component: PagethreeComponent }
];

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

Open the page components and have it extends the BaseComponent:

src/app/pageone/pageone.component.ts
import { Component, OnInit } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-pageone',
  templateUrl: './pageone.component.html',
  styleUrls: ['./pageone.component.css']
})
export class PageoneComponent extends BaseComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

As well as adding the router and injecting it into the BaseComponent constructor using super:

src/app/pageone/pageone.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-pageone',
  templateUrl: './pageone.component.html',
  styleUrls: ['./pageone.component.css']
})
export class PageoneComponent extends BaseComponent implements OnInit {

  constructor(public router: Router) {
    super(router);
  }

  ngOnInit(): void {
  }

}

This will take the injected router module and pass it into the extended component.

Next, repeat these changes for the remaining page components.

Due to the inheritance passed from the base component, anything defined on the base component is available to all components which extend it. So let’s use the base functionality.

Let’s add two buttons to the pageone.component.html template:

src/app/pageone/pageone.component.html
<button type="button" (click)="openPage('pagetwo')">
  Page Two
</button>

<button type="button" (click)="openPage('pagethree')">
  Page Three
</button>

Notice how there’s no extra qualification needed to use the openPage method? If you think about how you would do a similar inheritance in a language like C#, you would call something such as base.openPage(). The reason you don’t have to do this with TypeScript is because of the magic that happens during transpilation. TypeScript turns the code into JavaScript and the base component module is imported into the pageone component so it can be used directly by the component.

Looking at the transpiled JavaScript makes this a little clearer:

var PageoneComponent = /** @class */ (function (_super) {
    __extends(PageoneComponent, _super);
    function PageoneComponent(router) {
        var _this = _super.call(this, router) || this;
        _this.router = router;
        return _this;
    }
    PageoneComponent.prototype.ngOnInit = function () {
    };
    PageoneComponent = __decorate([
        Object(_angular_core__WEBPACK_IMPORTED_MODULE_0__["Component"])({
            selector: 'app-pageone',
            template:
             __webpack_require__("./src/app/pageone/pageone.component.html"),
            styles: [
              __webpack_require__("./src/app/pageone/pageone.component.css")
            ]
        }),
        __metadata("design:paramtypes",
            [_angular_router__WEBPACK_IMPORTED_MODULE_2__["Router"]])
    ], PageoneComponent);
    return PageoneComponent;
}(_base_base_component__WEBPACK_IMPORTED_MODULE_1__["BaseComponent"]));

This is also why we need to keep our injected modules public rather than private. In TypeScript, super() is how the constructor of the base component is called and requires all the injected modules to be passed. When modules are private, they become separate declarations. Keep them public and pass them using super, and they remain a single declaration.

Step 4 – Completing the App

Take a moment to use your code editor to remove the boilerplate code from app.component.html, leaving only <router-outlet>:

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

With Pageone finished, let’s run the app by using the CLI and investigate the functionality:

  • ng serve

Click on one of the buttons and observe that you are directed to the expected page.

That’s a lot of overhead to encapsulate functionality for a single component so let’s extend both the Pagetwo and Pagethree components as well as add buttons to help navigate to the other pages.

First, open pagetwo.component.ts and update it like pageone.component.ts:

src/app/pagetwo/pagetwo.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-pagetwo',
  templateUrl: './pagetwo.component.html',
  styleUrls: ['./pagetwo.component.css']
})
export class PagetwoComponent extends BaseComponent implements OnInit {

  constructor(public router: Router) {
    super(router);
  }

  ngOnInit(): void {
  }

}

Then, open pagetwo.component.html and add a button for pageone and pagethree:

src/app/pagetwo/pagetwo.component.html
<button type="button" (click)="openPage('pageone')">
  Page One
</button>

<button type="button" (click)="openPage('pagethree')">
  Page Three
</button>

Next, open pagethree.component.ts and update it like pageone.component.ts:

src/app/pagethree/pagethree.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-pagethree',
  templateUrl: './pagethree.component.html',
  styleUrls: ['./pagethree.component.css']
})
export class PagethreeComponent extends BaseComponent implements OnInit {

  constructor(public router: Router) {
    super(router);
  }

  ngOnInit(): void {
  }

}

Then, open pagethree.component.html and add a button for pageone and pagetwo:

src/app/pagethree/pagethree.component.html
<button type="button" (click)="openPage('pageone')">
  Page One
</button>

<button type="button" (click)="openPage('pagetwo')">
  Page Two
</button>

Now you can navigate all around the app without repeating any of the logic.

Conclusion

In this article, you encapsulated your common code in a base component and extend it to create reusable page navigation buttons.

From here, it’s easy to see how you could extend common functionality across many components. Whether you are handling navigation, common modal alert UI, or anything else, using the inheritance model granted via TypeScript to extend components is a powerful tool to keep in our toolbox.

The code for this tutorial is available on GitHub.

Creative Commons License