Reusability
Reusable Presentational Components in Angular 6+
Riddhi Gajera
Clock Icon
10 min read

Abstract

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.

Background and Problem Statement
In large-scale Angular applications, managing UI consistency and adopting changes efficiently can be challenging. Without a structured approach, components often become tightly coupled with business logic, making reusability difficult and leading to maintenance overhead. This blog addresses how presentational components can solve these challenges by promoting modularity and clean architecture.
1
How to create a reusable view?
Use stateless presentational components that receive data via @Input() and emit events via @Output() in Angular.
2
How to make enhancements and change adoption faster with more consistency?
Maintain a clear separation between presentational and business logic by using smart and dumb components, ensuring modularity and reusability.


Understanding the Problem in Depth

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:


  • Each table shows data fetched from various APIs.
  • Each table has a different number of columns, where each column can have different types of data, such as strings, numbers, currency, links, or HTML elements.
  • Each table has different styles for various data types, such as:
    • If data is a string → Align it to the center of the cell.
    • If data is currency → Align it to the right of the cell and show it with the “$” sign.
    • If data is a simple number → Align it to the left.
    • If data is a link → Display it in a specific style with different behavior.
  • Additional table requirements:
    • Combine the values of two fields and display them in one cell.
    • Provide row selection for a few tables.
    • Show a specific message when no data is available.
    • Add sorting capability for specific columns or all columns.
    • Pre-select rows in the table while loading the page.
    • Hide headers for some tables.
    • Provide a global filter for specific or all columns.
    • Add an input box in certain columns and handle value changes.
    • Add an input box in certain columns with an event that triggers on value change.

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?



Dividing Problem into solvable chunks

Below are the building blocks to reach up to the solution and remove our pain 😉

  • Presentation component.
  • Table level Configs.
  • Column specification and Column level configs.
  • Data.
  • Events to emit the actions on table or columns.

Presentation component

Here comes the actual savior 😉 The presentation component will work as a dumb component and it will.

  • Use the @Input to get the all required configs & data.
  • Use the @Ouput to emit all the events happening on the table.
  • Render the view of the table based on specific theme or UI frameworks like PrimeNg, Angular Material, or Bootstrap used within the application with the help of the configs and data.
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[];
  }
}

Table level Configs

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 {
  selection: boolean;
  selectAll: boolean;
  defaultSelectedData?: Record<string, any>[];
  disableSelection?: boolean;
  showCheckbox: boolean;
  showShort: boolean;
  showHeader?: boolean;
  globalFilterFields?: string[];
  emptyMessage?: string;
  selectionFieldName?: string;
  scrollHeight?: string;
  colIndexesForRowSpan?: number[];
}
  • selection: Specify weather to provide the row selection to table or not.
  • selectAll: Specify weather to provide default selection of all rows of table on load or not.
  • defaultSelectedData: Provide data that needs to be selected instead of all rows.
  • disableSelection: To disable the row level selection on the table.
  • showCheckbox: To Specify weather to show the checkbox for all rows or not.
  • showShort: Specify weather to provide sorting for each column or not.
  • showHeader: To show or hide the header on the table as its optional config default value will be true.
  • globalFilterFields: To specify on which field table level filter needs to provide as its optional config default value will be an empty array.
  • emptyMessage: To specify the message which needs to show when there is no data, as its optional config default value can be a generic message like “No Data Available”.
  • scrollHeight: To specify the height of the table after which needs to show the scroll in order to make sure table takes only specified space while there are huge data, its optional config when there is no value provided it will take the whole page in case of a huge amount of data rather than showing scroll on the table.

Column specification and configs

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 {
  field: string;
  header: string;
  order?: number;
  class?: string;
  width?: string;
  pipe?: string;
  render?: Record<string, any>;
  showShort?: boolean;
  isHtml?: boolean;
  showComponent?: boolean;
  componentName?: string;
  componentData?:
    | ITableLinks
    | ITableCheckboxData
    | ITableRowActionData
    | ITableInputBoxData;
  action?: string;
  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 {
      name: string;
      label: string;
      class?: string;
    }
    
    • name: Specify the name for links.
    • label: Specify the display name for the links.
    • class: Specify the style class for links.

    ITableCheckboxData:

    export interface ITableCheckboxData {
      name: string;
      disable?: boolean;
      checked?: boolean;
      value?: unknown;
      propertyName?: string;
      tooltip?: string;
    }
    
    • name: Specify the name property of the checkbox.
    • disable: Use to specify the weather need to disable the checkbox based on some data or condition.
    • checked: Use to check/ uncheck the checkbox based on the conditions.
    • value: Use to set or get the value of the checkbox.
    • propertyName: Use for data binding on the checkbox.

    ITableInputBoxData:

    export interface ITableInputBoxData {
      name: string;
      type: string;
      value: string | number;
      text?: string;
      class?: string;
      min?: number;
      max?: number;
      step?: number;
      disableKeyDown?: boolean;
    }
    
    • name: Specifies the name of the input element.
    • type: Specifies the type of the input field like text, number, password.
    • value: Specifies value for input field.
    • text: Specifies the text in case want to display some text after the input field.
      Example: Added the number input element to get the percentage value from the user so after input field, you can show the sign of the “%” using a text field.
    • class: Specifies the style classes for the input field.
    • min: Specifies the minimum value, if the input field type is number.
    • max: Specifies the maximum value, if the input field type is number.
    • step: Specifies the legal number intervals for an input field.
    • disableKeyDown: This flag is used to disable the key down event on the input field based on the data.

    ITableRowActionData:

    • This is used to specify the action name in order to emit the event from the table.
    export interface ITableRowActionData {
      action: string;
      data: unknown;
      event: any;
    }
    
Technology Used
Angular


Results and Benefits
Well-Defined Interface for Flexibility
Well-Defined Interface for Flexibility
A presentational component acts as a well-defined interface, making it easy to accommodate changes related to themes or different UI libraries with minimal effort and time. By isolating UI logic, developers can make modifications without affecting other parts of the application.
Simplified Maintenance and Upgrades
Simplified Maintenance and Upgrades
For example, if a table component is built using the PrimeNG library and a new version introduces breaking changes or renames properties, only a single update is needed within the presentational component. This avoids scattered changes across the entire codebase.
Focus on New Development
Focus on New Development
By centralizing UI elements, developers can focus on new features instead of repeatedly dealing with the same UI-related issues. This enhances productivity and reduces redundant work.
Faster Development and Scalability
Faster Development and Scalability
A well-structured presentational component accelerates development by allowing enhancements and refinements to be made as new requirements arise. This improves efficiency and ensures adaptability.
Reduced Testing Efforts
Reduced Testing Efforts
Since UI-related logic is isolated within the presentational component, testing becomes more manageable. Changes can be tested in one place without requiring extensive regression testing across the application.
Improved Code Readability
Improved Code Readability
Encapsulating UI logic in a dedicated presentational component enhances code readability, making it easier for developers to understand, maintain, and extend the codebase.

Conclusion

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.

Blogs You Might Like
Tech Prescient
We unleash growth by helping our customers become data driven and secured with our Data and Identity solutions.
Social Media IconSocial Media Icon
Social Media IconSocial Media Icon
Glassdoor
OUR PARTNERS
AWS Partner
Azure Partner
Okta Partner
Databricks Partner

© 2017 - 2025 | Tech Prescient | All rights reserved.

Tech Prescient
Social Media IconSocial Media Icon
Social Media IconSocial Media Icon
We unleash growth by helping our customers become data driven and secured with our Data and Identity solutions.
OUR PARTNERS
AWS Partner
Azure Partner
Databricks Partner
Okta Partner
Glassdoor

© 2017 - 2025 | Tech Prescient | All rights reserved.

Tech Prescient
We unleash growth by helping our customers become data driven and secured with our Data and Identity solutions.
Social Media IconSocial Media Icon
Social Media IconSocial Media Icon
OUR PARTNERS
AWS Partner
Okta Partner
Azure Partner
Databricks Partner
Glassdoor

© 2017 - 2025 | Tech Prescient | All rights reserved.