Encapsulate your common code in a base component and extend to your heart’s content!
If you’ve spent any time in Angular, you no doubt have come across a time when you’ve wanted to share data or functionality and you’ve 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 gives you a pretty easy way to handle this encapsulation; welcome to the world of inherited components.
Using class inheritance in TypeScript, you can declare a base component which 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.
Initializing a Simple App
Let’s start by using the Angular CLI to create a new app. If you haven’t installed the Angular CLI yet, go ahead and install it globally using npm:
$ npm install -g @angular/cli
Next, create the new app using the CLI:
$ ng new AngularComponentInheritance --skip-tests --routing true
We're passing some flag to the ng new
command to add routing to our app and not to add any testing files.
Once you open the folder, run the following command to create a base component (g = generate, c = component):
$ ng g c base --spec false --inline-template true --m app
The --m
flag here specifies the module to which the component should belong to.
Base Component
Open up the base/base.component.ts
file and use a simple generic template:
template: 'NO UI TO BE FOUND HERE!',
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 { Router } from '@angular/router'; constructor(public router: Router) { }
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):
openPage(routename: string) { this.router.navigateByUrl(`/${routename}`); }
This gives us the base functionality we need, so let’s use it on a few components.
Inherited Components
We’ll run three Angular CLI commands to generate some more components:
$ ng g c pageone --spec false -m app $ ng g c pagetwo --spec false -m app $ ng g c pagethree --spec false -m 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:
// ...component imports const routes: Routes = [ { path: '', component: PageoneComponent }, { path: 'pageone', component: PageoneComponent }, { path: 'pagetwo', component: PagetwoComponent }, { path: 'pagethree', component: PagethreeComponent } ];
Open each of the page components and extend it using the BaseComponent:
pageone.component.ts
export class PageoneComponent extends BaseComponent implements OnInit { // ... }
As well as adding the router and injecting it into the BaseComponent constructor using super
:
pageone.component.ts
constructor(public router: Router) { super(router); }
This will take the injected router module and pass it into the extended component.
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.
Completing the App
With page one in place, let’s run the app by using the CLI and check the functionality out:
Click on one of the buttons and you’re taken to the correct 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:
pagetwo.component.ts
export class PagetwoComponent extends BaseComponent implements OnInit { constructor(public router: Router) { super(router); } }
pagetwo.component.ts
<button type="button" (click)="openPage('pageone')"> Page One </button> <button type="button" (click)="openPage('pagethree')"> Page Three </button>
…and do the same for Pagethree, you get the picture!
Now you can navigate all around the app without repeating any of the logic.
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.
You can check out this example at the following repository: https://github.com/alligatorio/AngularComponentInheritance