When it comes to the services or data storage in UI applications we always think and write reusable code. But when it comes to certain views like tables and charts, which are not only extensively used across the application but also highly dependent on the theme and third-party library, We never give a thought about making it reusable, instead end up writing redundant code which drags development speed and productivity. The article provides a step-by-step guide on designing stateless components that focus solely on rendering UI elements based on input properties, without managing data fetching or state changes.
So, the answer to the above questions is to create a common component and use it everywhere—seems easy, right?
Wait… let’s take Tables as an example and think about different table views with the following cases:
Assume you need to support this on each page with tables representing different data—imagine the redundant code you would need to write! So, what’s the solution?
Below are the building blocks to reach up to the solution and remove our pain 😉
Here comes the actual savior 😉 The presentation component will work as a dumb component and it will.
import {
Component,
Input,
OnChanges,
OnInit,
Output,
EventEmitter,
AfterViewInit,
ViewChild
} from '@angular/core';
import { filter, isEmpty } from 'lodash';
import { Table } from 'primeng/table';
import { ITableColumns, ITableConfig } from 'presentation-component/table/models';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss']
})
export class TableComponent implements OnInit, OnChanges, AfterViewInit {
@ViewChild('dt') dt: Table;
@Input() columns: ITableColumns[];
@Input() data: Record<string, any>[] = [];
@Input() tableConfig?: ITableConfig;
@Input() searchData?: { searchValue: string; searchType: string };
@Output() selectedData? = new EventEmitter();
@Output() rowAction? = new EventEmitter();
@Output() tableInstance? = new EventEmitter();
selectedRowData: Record<string, any>[] = [];
globalFilterFields: string[] = [];
showHeader = true;
showGlobalFilter = false;
emptyMessage = '';
scrollHeight = '80px';
checkBoxColWidth = '3rem';
constructor() {}
ngOnInit(): void {}
ngAfterViewInit(): void {
this.tableInstance.emit(this.dt);
}
ngOnChanges(): void {
if (this.tableConfig) {
const {
emptyMessage,
showHeader,
selection,
selectAll,
defaultSelectedData,
selectionFieldName,
globalFilterFields,
scrollHeight
} = this.tableConfig;
this.scrollHeight =
scrollHeight && !isEmpty(this.data)
? scrollHeight
: isEmpty(this.data)
? '80px'
: this.scrollHeight;
this.emptyMessage =
emptyMessage !== '' ? emptyMessage : this.emptyMessage;
this.showHeader = showHeader !== undefined ? showHeader : true;
if (selection && selectAll) {
this.selectedRowData = this.data;
this.selectRow();
} else if (defaultSelectedData) {
if (selectionFieldName) {
const selectedData = defaultSelectedData.map(
(data) => data[selectionFieldName]
);
this.selectedRowData = !isEmpty(this.data)
? filter(this.data, (obj) =>
selectedData.includes(obj[selectionFieldName])
)
: [];
} else {
this.selectedRowData = defaultSelectedData;
}
} else {
this.selectedRowData = [];
}
if (globalFilterFields) {
this.globalFilterFields = globalFilterFields;
this.showGlobalFilter = true;
}
}
if (this.searchData) {
const { searchType, searchValue } = this.searchData;
this.dt.filterGlobal(searchValue, searchType);
}
}
selectRow(): void {
this.selectedData.emit(this.selectedRowData);
}
rowActions(event: unknown, rowData: unknown, action: string): void {
const data = { event, data: rowData, action };
this.rowAction.emit(data);
}
disableRowSelection(
tableConfig: ITableConfig,
rowData: Record<string, any>
): boolean {
if (tableConfig && tableConfig.disableSelection) {
return tableConfig.disableSelection;
} else if (rowData && rowData.rowCheckboxDisable) {
return rowData.rowCheckboxDisable;
} else {
return false;
}
}
includesRowIndex(index: number): boolean {
if (this.tableConfig && this.tableConfig.colIndexesForRowSpan) {
return this.tableConfig.colIndexesForRowSpan.includes(index);
}
return false;
}
asCols(cols: unknown): ITableColumns[] {
return cols as ITableColumns[];
}
}
So if any component has a very specific requirement then it can be passed on with the help of the table level configuration. This config is optional so in case there is no specific config then the presentation component will render table with the default configuration. Will create the type ITableConfig.ts which specifies the required and optional configs.
export interface ITableConfig {
readonly selection: boolean;
readonly selectAll: boolean;
readonly defaultSelectedData?: Record<string, any>[];
readonly disableSelection?: boolean;
readonly showCheckbox: boolean;
readonly showShort: boolean;
readonly showHeader?: boolean;
readonly globalFilterFields?: string[];
readonly emptyMessage?: string;
readonly selectionFieldName?: string;
readonly scrollHeight?: string;
readonly colIndexesForRowSpan?: number[];
}
Will take the all columns specification with column level config.
Will create the type ITableColumns.ts which specifies the required and optional configs at table level.
export interface ITableColumns {
readonly field: string;
readonly header: string;
readonly order?: number;
readonly class?: string;
readonly width?: string;
readonly pipe?: string;
readonly render?: Record<string, any>;
readonly showShort?: boolean;
readonly isHtml?: boolean;
readonly showComponent?: boolean;
readonly componentName?: string;
readonly componentData?:
| ITableLinks
| ITableCheckboxData
| ITableRowActionData
| ITableInputBoxData;
readonly action?: string;
readonly rowSpan?: number;
}
field: Use to specify the property name to take from the data, its required config.
header: Use to specify the header to show on the table, its required config.
order: Use to specify the order of the column on the table, in order to provide the drag feature for columns.
class: Use to specify the style class in order to provide specific style at column level.
width: Use to specify the width of a specific column render.
pipe: Use to specify the pipe name to transform the values on columns.
render: Use to render the specific value with current field value or to combine the value of 2 different field.
showShort: Use to specify the sort option to the specific column when no need to provide the short option to all the columns.
isHtml: Use to specify whether to directly display the value as is on columns or need to render the HTML element and then display the value.
showComponent: Use when the isHtml config is true and need to show specific HTML elements like links, input field, or checkbox in the column.
componentName: Use to specify the component name when showComponent config is true, component name will be predefined and provide on the table component.
componentData: Use to specify the properties of the specific component will going to use if its links then will specify the name, class, and action name which will use by the invoker component to identify which link was clicked on the table’s row componentData can have the various type of HTML element and each HTML element have the different attributes which will be specified with the element level required attribute and will use the specific type as mentioned like ITableLinks, ITableCheckboxData, ITableInputBoxData, ITableRowActionData.
export interface ITableLinks {
readonly name: string;
readonly label: string;
readonly class?: string;
}
export interface ITableCheckboxData {
readonly name: string;
readonly disable?: boolean;
readonly checked?: boolean;
readonly value?: unknown;
readonly propertyName?: string;
readonly tooltip?: string;
}
export interface ITableInputBoxData {
readonly name: string;
readonly type: string;
readonly value: string | number;
readonly text?: string;
readonly class?: string;
readonly min?: number;
readonly max?: number;
readonly step?: number;
readonly disableKeyDown?: boolean;
}
export interface ITableRowActionData {
readonly action: string;
readonly data: unknown;
readonly event: any;
}
Reusable presentational components are a powerful approach to structuring an Angular application. They promote maintainability, scalability, and efficiency by encapsulating UI logic and keeping it separate from business logic. By adopting this pattern, developers can easily adapt to design changes, switch UI libraries with minimal effort, and streamline development workflows. Ultimately, leveraging presentational components leads to cleaner, more modular, and testable code, enhancing both the developer experience and the overall application quality.