Friday, June 25, 2021

creating reusable components - List component with pagination and selection

Image by 95C from Pixabay

Techniques to share logic between components

RY (Don’t repeat yourself) is one of the fundamental concepts of software engineering; As software engineers, we often strive to build as much as possible using as little code as we can.

That’s indeed a really good thing because it allows us to ship less code, increase productivity, and keep a healthy codebase.

In this article, I want to introduce you to the techniques available with Angular to build components by sharing as much code as possible:

  • Class inheritance
  • Class mixins
  • Component composition

Tip: Use Bit (Github) to easily share and reuse Angular components across your projects, suggest updates, sync changes and build faster as a team.

Angular components with Bit: Easily share across projects as a team

Component Class Inheritance

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.

ListComponent Base class

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.

PageableListComponent

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;
}
}

SelectableListComponent

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);
}
}

Implementation component: CustomersListComponent

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 {}

Component Class Mixins

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.

An Example of Typescript Mixin

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) {}

Building CustomersListComponent using Mixins

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.

Component Composition

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.

ListComponent: Leveraging the Power of ngTemplateOutlet

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;
}
}

PaginationComponent

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);
}
}

CustomerComponent

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>();
}

CustomersListComponent

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.

Source code

You can find all the examples’ code at this Stackblitz link.

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.

In my next article, I will be focussing more on Typescript Mixins, which in my opinion is the least known and underrated way of building components. I will explore a scenario where inheritance results in brittle, hard to maintain code, and how Mixins can help, including cons and pros, and prior art from the Javascript community.

No comments:

Post a Comment

Technology’s generational moment with generative AI: A CIO and CTO guide

 Ref:  A CIO and CTO technology guide to generative AI | McKinsey 1. Determine the company’s posture for the adoption of generative AI As us...