In this article, I want to introduce you to the techniques available with Angular to build components by sharing as much code as possible:
My least favorite, but also the most used way to share code among Angular components, is ES6 class inheritance using the extends keyword.
ES6 class inheritance is seen as hugely controversial in the Javascript community for various reasons, but it is still incredibly used in the Angular world; when used in the right way and is not abused, the technique is a decent solution for sharing code between components.
Let’s see an example of extending a component with inheritance by creating a component ListComponent
, extended by two more abstract classes that extend the functionality of the base class, and then we implement these with the actual Angular component.
A common way of using this technique is to create an abstract class and define there the methods shared by all the subclasses. A subclass may have to implement the abstract methods or override the existing ones.
The Base class is very simple: we simply define the Input items.
export abstract class ListComponent {
@Input() items: Item[];
}
Next, we want to extend the functionality of a simple list with pagination and selection. Therefore, we proceed and extend BaseList
with two more abstract classes.
The component PageableListComponent
extends ListComponent
and adds pagination functionality.
export abstract class PageableListComponent extends ListComponent {
page = 0;
itemsPerPage = 2;
get start() {
return this.page * this.itemsPerPage;
}
get end() {
return this.page * this.itemsPerPage + this.itemsPerPage;
}
get pages() {
return new Array(this.items.length / this.itemsPerPage);
}
changePage(page: number) {
this.page = page;
}
}
The component SelectableListComponent
extends PageableListComponent
and adds selection/unselection functionality.
export abstract class SelectableListComponent extends PageableListComponent {
@Output() selected = new EventEmitter<Item>();
@Output() unselected = new EventEmitter<Item>();
selectedItems: Item[] = [];
select(item: Item) {
this.selected.emit(item);
this.selectedItems = [...this.selectedItems, item];
}
unselect(item: Item) {
this.unselected.emit(item);
this.selectedItems = this.selectedItems.filter(({value}) => value !== item.value);
}
isItemSelected(item: Item) {
return this.selectedItems.some(({value}) => item.value === value);
}
}
Finally, we create an implementation of the class CustomersListComponent
and extend it SelectableListComponent
. The template and the component will have access to all the outputs and inputs we specified in the other classes.
@Component({
selector: 'customers-list',
template: `
<div *ngFor="let item of items | slice: start : end">
<label>
<input
type="checkbox"
[checked]="isItemSelected(item)"
(change)="
$event.target.checked ? select(item) : unselect(item)
"
/>
{{ item.display }}
</label>
</div>
<div class='pages'>
<div *ngFor="let p of pages; let i = index;"
class='page'
[class.selected]="i === page"
(click)="changePage(i)"
>
{{ i }}
</div>
</div>
`
})
export class CustomersListComponent extends SelectableListComponent {}
// USAGE
<customers-list [items]="customers"
(selected)="onSelected($event)"
(unselected)="onUnselected($event)"
></customers-list>
We can also create a subclass from CustomersListComponent
, although the decorator’s metadata will have to be redefined. That means we will need to assign a new selector, template, styles, etc. to the new component. If you want to reuse them, then you can point the URLs to the parent class’:
@Component({
selector: 'new-customers-list',
templateUrl: '../customers-list/customers-list.component.html'
})
export class NewCustomersListComponent extends CustomersListComponent {}
In order to share logic between Angular component classes, we can also leverage a less-known method known as Mixins. Mixins allow us to compose multiple small classes that extend the target class but without having to use multiple inheritance.
Let’s demonstrate what a mixin is with a simple example. First, we define a base class:
class BaseButton {
label: string;
disabled: boolean;
}
Next, we define a function that extends the base class with a new mini-class
function themeMixin(BaseClass) {
return class extends BaseClass {
theme: string;
}
}
Finally, we extend the BaseButton class with the mixin:
class PrimaryButton extends themeMixin(BaseButton) {}
Let’s rewrite the CustomersListComponent
example using mixins.
export function pageableListMixin(BaseClass) {
return class extends BaseClass {
page = 0;
itemsPerPage = 2;
get pages() {
return new Array(this.items.length / this.itemsPerPage);
}
changePage(page: number) {
this.page = page;
}
get start() {
return this.page * this.itemsPerPage;
}
get end() {
return this.page * this.itemsPerPage + this.itemsPerPage;
}
}
export function selectableListMixin(BaseClass) {
class SelectableListMixin extends BaseClass {
@Output() selected = new EventEmitter<Item>();
@Output() unselected = new EventEmitter<Item>();
selectedItems: Item[] = [];
select(item: Item) {
this.selected.emit(item);
this.selectedItems = [...this.selectedItems, item];
}
unselect(item: Item) {
this.unselected.emit(item);
this.selectedItems = this.selectedItems.filter(({value}) => {
return value !== item.value;
});
}
isItemSelected(item: Item) {
return this.selectedItems.some(({value}) => {
return item.value === value;
});
}
}
return SelectableListMixin;
}
Once we define all the mixins we need to compose the component, we import the mixins and pass the Base class as an argument.
Then, we simply extend CustomersListComponent
with the mixin CustomersListMixin
.
const CustomersListMixin =
selectableListMixin(
pageableListMixin(ListComponent)
);
@Component(...)
export class CustomersListComponent extends CustomersListMixin {}
While also Mixins have several pitfalls, this is, in my opinion, a more elegant and safer solution to multiple inheritance, at least in the long term.
The component composition is a technique that complements inheritance and mixins: instead of extending a component with more functionality, we can combine multiple smaller components to achieve the same result.
The first component we can create is a generic, reusable component ListComponent
: its responsibility is to simply render the items based on start and end indexes as provided by the parent component.
As you can see, the component does not dictate how to render each individual item: we let the parent define that by providing ngTemplateOutlet
and passing each item as context.
@Component({
selector: "list",
template: `
<div *ngFor="let item of items | slice : start : end">
<ng-container
*ngTemplateOutlet="template; context: { item: item }"
>
</ng-container>
</div>
`
})
export class ListComponent {
@Input() items: Item[] = [];
@Input() itemsPerPage = 2;
@Input() currentPage: number;
@ContentChild('item', { static: false })
template: TemplateRef<any>;
get start() {
return this.currentPage * this.itemsPerPage;
}
get end() {
return this.currentPage * this.itemsPerPage + this.itemsPerPage;
}
}
Then, we add a pagination component that takes care of listing the page numbers, and to notify the parent when the user clicks on a page:
@Component({
selector: "pagination",
template: `
<div class="pages">
<div
*ngFor="let p of pages; let i = index"
class="page"
[class.selected]="i === currentPage
(click)="pageChanged.emit(i)"
>{{ i }}
</div>
</div>
`
})
export class PaginationComponent {
@Input() currentPage: number;
@Input() itemsPerPage = 2;
@Input() itemsLength: number;
@Output() pageChanged = new EventEmitter<number>();
get pages() {
return new Array(this.itemsLength / this.itemsPerPage);
}
}
Next, we define a component to represent each item in the list: it takes care of defining how the item is displayed, and dispatch events when the item is selected or unselected:
@Component({
selector: "customer",
template: `
<label>
<input
type="checkbox"
[checked]="isSelected"
(change)="$event.target.checked ? selected.emit(item) : unselected.emit(item)"
/>
{{ item.display }}
</label>
`
})
export class CustomerComponent {
@Input() item: Item;
@Input() isSelected: boolean;
@Output() selected = new EventEmitter<Item>();
@Output() unselected = new EventEmitter<Item>();
}
It’s now time to put things together! We can reuse the previously defined components to compose a list of customers, that is selectable and pageable. These components are all reusable and can be composed with any other list.
@Component({
selector: "composition-customers-list",
template: `
<list
[items]="items"
[itemsPerPage]="2"
[currentPage]="currentPage"
>
<ng-template #item let-item="item">
<customer
(selected)="selected($event)"
(unselected)="unselected($event)"
[item]="item"
[isSelected]="isItemSelected(item)"
></customer>
</ng-template>
</list>
<pagination
[currentPage]="currentPage"
[itemsLength]="items.length"
[itemsPerPage]="2"
(pageChanged)="currentPage = $event"
></pagination>
`
})
export class CompositionCustomersListComponent {
@Input() items = [];
currentPage = 0;
selectedItems = [];
selected(item) {
this.selectedItems = [...this.selectedItems, item];
}
unselected(item) {
this.selectedItems = this.selectedItems.filter(({ value }) => value !== item.value);
}
isItemSelected(item) {
return this.selectedItems.some(({ value }) => item.value === value);
}
}
Component composition is the ultimate way to create highly-reusable, clean, and effective components, and is easily my favorite way of thinking about sharing code and reusability.
Instead of writing God components, we can reuse many smaller ones. Getting right the public API of each component is fundamental for them to work well with the rest of your application.
As you can see above, we still have some repeated logic due to some methods being rewritten for each list we create: that’s why using one technique is not exclusive: we can easily combine this with a mixin that takes care of selection, so we do not have to rewrite it for other lists.
You can find all the examples’ code at this
In this article, we went through three techniques for sharing code between components.
If it wasn’t clear by now, I am not a fan of inheritance and multiple inheritances, but I think it’s still very important to know and recognize when it’s a good idea to use and when it’s not.
Happy Coding !
#angular