Component Inheritance in Angular

Respect the DRY rule! Learn how to write code efficiently using component inheritance

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

In my previous job at Reimnet, I was assigned as the head of a team that was in charge of the creation of the admin side of an Angular project called Exchange App. During the building process, I encountered this Repeating-Yourself kinda thing. I was constantly copying and pasting code from one component to another. It is said that if you find yourself copying and pasting code snippets, you have violated the sacred DRY principle. The components were similar and required the same logic and display markup.

  • Tip: To keep your code DRY, Make your components reusable with Bit, to easily share and sync them across your applications. Modularity and DRY go hand-in-hand, and Bit is a useful power tool for the job. Try it :)

For a glimpse into Bit and some other Angular dev tools, check out this article:

Bit 5 min Demo: DRY code-sharing with components

Back to business. I had the following components:

  • Users component
  • Clients component
  • Delivery Result component
  • Admin component

These above components do the same thing:

  • Display a list of each item (users, clients, delivery results, admins) in a table.
  • Perform CRUD operations on them.
  • Paginate and Filter the lists.

Let’s look at the simplified version of the User component:

@Component({
selector: 'app-user',
tempate: `
<h1>Manage Users</h1>
<search (search)="searchCallback()"></search>
<table>
<th>
<tr>#</tr>
<tr>Name</tr>
<tr>Email</tr>
<tr>Action</tr>
</th>
<tb *ngFor="let user of users">
<tr>{{ user.id }}</tr>
<tr>{{ user.name }}</tr>
<tr>{{ user.email }}</tr>
<tr>
<button (click)="editUser(user)">Edit</button>
<button (click)="deleteUser(user)">Delete</button>
</tr>
</tb>
</table>
<paginate (next)="calcPage('next')" (prev)="calcPage('prev')"></paginate>
`
})
export class User implements OnInit {
users: Array<User> = []
ngOnInit() {
//...
}
editUser(user: User) {
// ...
}
deleteUser(user: User) {
//...
}
searchCallback(text: any) {
//...
}
calcPage(type: any) {
//...
}
}

It renders a list of users it gets from an endpoint. It has buttons for editing and deleting a client. We have a paginate component and we pass it a paginate callback calcPage used to know how to calculate the pagination.

On top of the table is a search component; we pass in a callback searchCallback. This callback determines how text is searched in the users' array.

Now, let’s look at Clients component:

@Component({
selector: 'app-client',
tempate: `
<h1>Manage Clients</h1>
<search (search)="searchCallback()"></search>
<table>
<th>
<tr>#</tr>
<tr>Name</tr>
<tr>Email</tr>
<tr>Action</tr>
</th>
<tb *ngFor="let client of clients">
<tr>{{ client.id }}</tr>
<tr>{{ client.name }}</tr>
<tr>{{ client.email }}</tr>
<tr>
<button (click)="editClient(client)">Edit</button>
<button (click)="deleteClient(client)">Delete</button>
</tr>
</tb>
</table>
<paginate (next)="calcPage('next')" (prev)="calcPage('prev')"></paginate>
`
})
export class Client implements OnInit {
clients: Array<Client> = []
ngOnInit() {
//...
}
editUser(client: Client) {
// ...
}
deleteUser(client: Client) {
// ...
}
searchCallback(text: any) {
// ...
}
calcPage(type: any) {
// ...
}
}

See, it’s almost the same thing as User component — very similar features. So you’ll find yourself writing similar codes over and over in Admin and Delivery Requests components. Using inheritance in TypeScript we can use the Parent-Child design to delegate the common code to a base class and let other components inherit the base class. In doing so, the components only have to define and implement logic that is specific to the component.

Setting up the Base component

Run the following command to create the base component:

ng g c base --inline-template=true --skipTests

The baseComponent will look like this:

@Component({
selector: 'app-base',
template: `
<div>
base works!!
</div>
`
})
export class BaseComponent {}

The UI will not be shown so there is no need of the <div> base works!! </div> or to add any other thing. I'll prefer it is left blank.

We write the base logic here. We need to note some things. We will create an array of base items that the extending components will iterate to populate their tables. This base item will hold an array of either the admins, clients, delivery requests or drivers.

// ...
export class BaseComponnet {
baseItems: Array<any> = []
}

What the inheriting components need only to do is just reference the base items and render the items.

@Component({
selecor: 'app-user',
tempate: `
<h1>Manage Users</h1>
<search (search)="searchCallback()"></search>
<table>
<th>
<tr>#</tr>
<tr>Name</tr>
<tr>Email</tr>
<tr>Action</tr>
</th>
<tb *ngFor="let user of baseItems">
<tr>{{ user.id }}</tr>
<tr>{{ user.name }}</tr>
<tr>{{ user.email }}</tr>
<tr>
<button (click)="editUser(user)">Edit</button>
<button (click)="deleteUser(user)">Delete</button>
</tr>
</tb>
</table>
<paginate (next)="calcPage('next')" (prev)="calcPage('prev')"></paginate>
`
})
export Users extends BaseComponent {
//...
}

Now, each component has different APIs from which they fetch their data. Like User get its array of users from <API_DOMAIN>/users, Admins from <API_DOMAIN>/admin

We will create a property that will hold the API URL of each component, this will enable the base component to know from which API to fetch data.

// ...
export class BaseComponnet {
baseItems: Array<any> = []
apiName: string;
}

We implement the OnInit interface, so we can fetch and load the data to the baseItems:

// ...
export class BaseComponent implements OnInit {
baseItems: Array<any> = []
apiName: string;
ngOnInit() {
this.apiService[apiName].subscribe(res=>{
this.baseItems = res.data
})
}
}

With this, all components will get their data regardless of the type.

export Users extends BaseComponent {
//...
apiName = 'getUsers'
}
export Admin extends BaseComponent {
//...
apiName = 'getAdim'
}
// ...

We are done with loading data. Let’s implement the editing and loading of data. To do that we first remove the data from the baseItems array and run an API to delete the item from the server database.

// ...
export class BaseComponent implements OnInit {
baseItems: Array<any> = []
apiName: string;
ngOnInit() {
this.apiService[apiName].subscribe(res=>{
this.baseItems = res.data
})
}
editClient(item: any) {
// remve item from arrray
this.apiService[editApiName].subscribe(res => {
// ...
})
}
}

So all components will provide its own editApiName name. The above implementation will go also for deleting and adding an item.

export Users extends BaseComponent {
//...
apiName = 'getUsers'
editApiName - 'editUser'
}
export Admin extends BaseComponent {
//...
apiName = 'getAdim'
editApiName = 'editAdmin'
}
// ...

You see we have successfully moved the common code to a base class leaving us to implement code specific to the child components only.

There is more…

There are some key features that will need to understand when using inheritance in our Angular app.

Lifecycle methods are not inherited

Lifecycle methods (OnInit, OnChanges, …) are not inherited by the child components.

@Component({})
class ComponentA implements OnInit{
ngOnInit() {
//...
}
}
@Component({...})
class ComponentB extends ComponentA {}

When rendering ComponentB, ngOnInit won't be fired for ComponentB because the method is not passed down to the child components. If we need to fire ngOnInit on ComponentA in ComponentB we need to define the OnInit method on ComponentB and call the parent method using super.* call.

@Component({})
class ComponentA implements OnInit{
ngOnInit() {
console.log('ComponentA ngOnInit')
//...
}
}
@Component({...})
class ComponentB extends ComponentA implements OnInit {
ngOnInit() {
super.ngOnInit() // ComponentA's method is fired
console.log('ComponentB ngOnInit')
// ...
}
}

Inherited methods and properties are based on accessibility level

For sure, private methods and properties are not passed over to child components. only public methods and properties are inherited by child components.

@Component({...})
class RedCompo {
private privateProperty
public publicProperty
private privateMethod() {
// ...
}
public publicMethod() {
// ...
}
}
@Component({...})
class BlueCompo extends RedCompo {}

Only the property publicProperty and method publicMethod in the RedCompo will be visible to the BlueCompo class. privateProperty and privateMethod will only be visible to the RedCompo class.

Metadata and Decorators are not inherited

Angular made heavy of decorators and meta-data (@Component, @Directive, @NgModule, ...). This meta-data and decorators are not inherited by the child components. For example:

@Component({
selector: 'app-a',
template: `
<div>
ComponentA
</div>
`
})
class ComponentA {}
@Component({
selector: 'app-b',
template: `
<div>
ComponentB
</div>
`
})
class ComponentB extends ComponentA {}

The @Component({...}) in ComponentA will not be inherited by ComponentB. So when ComponentB is rendered app-b, the template in the ComponentA is not rendered.

But there is an exception, @Input and @Output decorators are passed down to the child components.

@Component({
selector: 'app-a',
template: `
<div>
ComponentA
</div>
`
})
class ComponentA {
@Input() input;
@Output() output: EventEmitter<any> = new EventEmitter();
}
@Component({
selector: 'app-b',
template: `
<div>
ComponentB
<buton (click)="runClick()">Run Click</buton>
</div>
`
})
class ComponentB extends ComponentA {
runClick() {
this.output.emit()
}
}

The two properties (@Input() input, @Output() output) in ComponentA will be visible to ComponentB.

@Component({
selector: 'app-b',
template: `
<div>
<app-b input="'nnamdi'" (output)="runOutput()"></app-b>
</div>
`
})
class App{
runOutput() {
console.log('runOutput')
}
}

When we click Run Click button, we will see runOutput logged in our console.

Dependency Injection

Angular DI might be tricky when using inheritance in Angular. We might get errors that we might not know what is the cause.

If we have our base component like this:

@Component({...})
class App {
constructor(private router: Router) {
// ...
}
}

This component takes a Router in its constructor. Angular provides the instance at runtime. Note that the Router is on a private level. The extending component will not see the router. We need to make it public:

@Component({...})
class App {
constructor(public router: Router) {
// ...
}
}

Coming to the extending component. The component must inject the parameters of its base component via the super call.

Let’s say BComponent extends App. BComponent must inject the Router and pass it to super() call.

@Component({...})
class BComponent extends App {
constructor(public router: Router) {
// ...
super(router)
}
}

Normally, the BComponent need not implement the constructor. JS engine will pass down the constructor params. But because our constructor params are injected by the Angular DI system we have to explicitly state to Angular what to pass and that we are a child component.

Conclusion

You see how easy it is to extend common functionality in Angular. We were able to do this because Angular is written in TypeScript so we have the power of TypeScript to extend and compose common code together.

If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email or DM me. Thanks !!!

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕