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.
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.
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.
Open up the base/base.component.ts
file with your code editor:
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:
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):
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.
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:
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
:
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
:
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:
<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.
Take a moment to use your code editor to remove the boilerplate code from app.component.html
, leaving only <router-outlet>
:
<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
:
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
:
<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
:
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
:
<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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!