Sitemap
Liel Shani

Liel Shani

Building a Reusable Angular Grid: Wrapping a Third-Party Library the Right Way

7 min readMar 2, 2025

--

When working with third-party UI components in Angular, such as grids, modals, and charts, developers often integrate them directly into multiple parts of an application. While this approach may seem straightforward initially, it often leads to inconsistent implementations, repeated logic, and long-term maintenance challenges.

A scalable and maintainable solution is to wrap third-party components inside custom Angular components and delegate state management to a headless service. This method ensures encapsulation, reusability, and centralized control over configuration and behavior.

This approach is applicable to any third-party UI component, but in this post, I’ll focus on data grids, demonstrating how wrapping a third-party grid library results in a flexible, maintainable, and consistent grid component in Angular.

Challenges of Directly Using a Third-Party Grid Component

👎🏽 Configuration Duplication — Each grid instance requires its own configuration, leading to inconsistent implementations when different approaches are used for the same feature (e.g., filtering).

👎🏽 Code Duplication & Bugs — Repeating logic across multiple instances increases the risk of inconsistent UX, unexpected behaviors, and maintenance overhead.

👎🏽 Inconsistent Behavior Across Screens — Pagination, sorting, and filtering may be implemented differently, creating user friction instead of a seamless experience.

👎🏽 Harder Maintenance & Upgrades — If the grid library changes, all instances must be updated, making refactoring time-consuming.

👎🏽 Increased Testing Complexity — UI tests must be written separately for each instance, leading to inefficient regression testing.

👎🏽 Inflexibility for Future Enhancements — Global changes (e.g., server-side filtering, caching) require refactoring every instance, instead of modifying one shared implementation.

👎🏽 UI/UX Inconsistencies Over Time — Without a unified grid component, teams introduce minor variations, making the application feel disjointed.

These issues compound over time, making the application harder to maintain, scale, and evolve. A better approach is to wrap the third-party grid into a reusable Angular component with a shared state management service, ensuring consistency, reusability, and long-term maintainability.

The Solution: A Custom Grid Component with Headless State Management

Instead of using the third-party grid component directly, I wrapped it inside a custom Angular component and delegated state management to a separate service.

Key Architectural Decisions

1. A Configurable Grid Wrapper Component

  • The grid component acts as a wrapper around the third-party UI library.
  • It accepts a configuration object, which determines the available features and provides the mandatory configurations required for the grid to function.
  • One key mandatory configuration is the data fetch method, which handles loading, caching, canceling requests, and error handling inside the grid. Managing data loading centrally ensures that all grid instances follow a consistent pattern across the application.
  • The list of features in the configuration (such as pagination, sorting, filtering, column chooser, search bar) are just examples. The actual configuration should be customized based on the application’s needs.
  • For example, some applications may require a view mode for charts, so they can add chart configuration as part of the global grid settings.
@Component({
selector: 'app-custom-grid',
templateUrl: `
@if(gridService.hasHeader()) {
<app-grid-header></app-grid-header>
}
@if(gridService.hasSearchBar()) {
<app-grid-search-bar></app-grid-search-bar>
}
<app-grid-views></app-grid-views>
@if(gridService.hasPager()) {
<app-grid-pager></app-grid-pager>
}
`,
providers: [CustomGridService]
})
export class CustomGridComponent {
private readonly gridService = inject(CustomGridService);

readonly config: InputSignal<CustomGridConfig> = input.required();

readonly load = output<CustomGridApi>();

ngOnInit() {
this.gridService.initialize(this.config());
this.load.emit(this.getGridFunctionality());
}

private getGridFunctionality(): CustomGridApi {
return {
reload: () => this.gridService.reload(),
setFilters: (filters) => this.gridService.setFilters(filters),
// The functionality you want to externalize
};
}
}

2. A Headless Grid State Service

  • The service manages state only and does not handle UI rendering.
  • It maintains internal private Subjects (RxJS) to manage its state and exposes signals and observables to allow UI components to consume the state reactively.
  • The service also provides methods to update the state, which dumb subcomponents use to modify grid behavior.
  • The list of methods and exposed state properties are just examples, and the actual implementation depends on the needs of the application.

Partial example of the service that focus on the Paging feature:

@Injectable()
export class CustomGridService<TRowData, TFilters> implements OnDestroy, ICustomGridService<TRowData, TFilters> {
// ...

private readonly _paging$ = new BehaviorSubject<CustomGridPaging>({ pageSize: 10, pageNumber: 1 });

// ...

readonly pageSize: Signal<number> = toSignal(this._paging$.pipe(map(p => p.pageSize)));

readonly pageNumber: Signal<number> = toSignal(this._paging$.pipe(map(p => p.pageNumber)));

// ...

readonly pagingConfig: Signal<CustomGridPagingConfig> = signal(null);

// ...

readonly hasPager: Signal<boolean> = computed(() => !!this.pagingConfig() && this.pagingConfig().enabled);

// ...

initialize(config: CustomGridConfig<TRowData, TFilters>): void {
// ...
this.initPagingConfig(config.pagingConfig);
// ...
}

private initPagingConfig(pagingConfig?: CustomGridPagingConfig) {
// resolve the config and set the state
this.pagingConfig.set({
...this.defaultPagingConfig,
...pagingConfig
});
this._paging$.next({
pageSize: this.pagingConfig().defaultPageSize,
pageNumber: this.pagingConfig().defaultPageNumber
});
}

setPageNumber(currentPageNumber: number): void {
if (currentPageNumber > 0) {
this._paging$.next({ ...this._paging$.value, pageNumber: currentPageNumber });
}
}

setPageSize(currentPageSize: number): void {
if (currentPageSize > 0) {
this._paging$.next({ pageNumber: 1, pageSize: currentPageSize });
}
}

// ...
}

3. Configurable “Dumb” Subcomponents

  • The grid consists of multiple independent subcomponents, each responsible for a specific UI functionality.
  • These components are purely presentational (“dumb components”), meaning:
    - They consume state from the headless grid service.
    - They emit user interactions (e.g., row selection, sorting, filtering).
    - They do not manage their own state — they only reflect the current grid state.
  • For example, the pager component can use setPageNumber() or setPageSize() to update the grid state, while also using pageNumber and pageSize signals to display the current pagination state in the UI.

Examples of Dumb Subcomponents

  • GridHeaderComponent — Displays column headers and sorting options.
  • GridPagerComponent — Manages pagination controls (next/previous page, page size selection).
  • GridTableViewComponent — Displays data in a table format.
  • GridCardsViewComponent — Displays data in a card-based layout.
  • GridViewSwitcherComponent — Allows users to toggle between table and card views.
  • GridNoDataComponent — Displays a message when there is no data.
  • GridSearchBarComponent — Provides a search input for filtering grid data.
  • GridColumnChooserComponent — Allows users to show/hide specific columns dynamically.

These components consume state from the grid service and emit events when a user action occurs (e.g., sorting a column, navigating pages, changing views).

Get Liel Shani’s stories in your inbox

Join Medium for free to get updates from this writer.

Example of the pager sub component:

@Component({
selector: 'app-custom-grid-pager',
template: `
<div class="pager">
@if (totalPages() > 1) {
<button (click)="onPrev()" [disabled]="pageNumber() <= 1">‹ Prev</button>

<span>Page {{ pageNumber() }} of {{ totalPages() }}</span>

<button (click)="onNext()" [disabled]="pageNumber() >= totalPages()">Next ›</button>
}

<app-custom-select-dropdown
[options]="pageSizeOptions()"
[selected]="pageSize()"
(onChange)="setPageSize($event)">
</app-custom-select-dropdown>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomGridPagerComponent {
private readonly gridService = inject(CustomGridService);

readonly pageNumber = computed(() => this.gridService.pageNumber());
readonly pageSize = computed(() => this.gridService.pageSize());
readonly totalPages = computed(() => this.gridService.totalPages());
readonly pageSizeOptions = computed(() => this.gridService.pageSizeOptions());

onPrev(page: number) {
this.gridService.setPageNumber(this.pageNumber() - 1);
}

onNext(page: number) {
this.gridService.setPageNumber(this.pageNumber() + 1);
}

setPageSize(size: number) {
this.gridService.setPageSize(+size);
}
}

Choosing the Right Scope for Your Grid Component

The level of complexity in the grid component should match your application’s needs:

1️⃣ Full-Feature Grid — If sorting, filtering, and pagination are widely used across multiple screens, handling them centrally inside the grid service prevents redundant logic.

2️⃣ Lightweight Grid — If grid usage varies from page to page, keeping configurations minimal and flexible makes customization easier.

Both approaches are legitimate, and the decision should be based on the application’s complexity and requirements.

Where to Integrate the Third-Party Grid?

To keep the architecture clean and maintainable, the third-party grid should be isolated inside a dedicated subcomponent responsible for rendering data. The main CustomGridComponent delegates all state management to CustomGridService, ensuring the grid remains flexible and easy to upgrade.

Best Practices for Integration

✔️ Use the third-party grid inside a view subcomponent (GridTableViewComponent, GridCardsViewComponent).
✔️ Keep state management in the service – The grid should only consume data and react to state changes.
✔️ Subcomponents should use signals and emit events – No direct modification of the grid state.
✔️ Minimize direct dependencies on the third-party library – If the library changes, only the view component needs updates.

By following this approach, the grid wrapper remains independent of any specific library, making future upgrades or replacements much easier. 🚀

Advantages of Using This Approach

✔️ Single Configuration for All Instances — Eliminates redundant setups and ensures uniform behavior.

✔️ Consistent User Experience — Sorting, filtering, and pagination work identically across screens.

✔️ Better Maintainability — Updates and fixes apply once instead of modifying multiple files.

✔️ Easier Feature Expansion — New capabilities (e.g., column chooser, export) can be added in one place instead of multiple instances.

✔️ Future-Proofing & Easier Library Upgrades — Changes in the third-party grid only affect the wrapper and view subcomponent.

✔️ Improved Testability — A single test suite ensures all grid instances function correctly.

✔️ Scalability & Flexibility — Can be configured dynamically to match different application needs.

✔️ Simpler Developer Onboarding — New developers learn one implementation, making it easier to use.

By wrapping the third-party grid in a custom Angular component with a shared state service, we reduce complexity, improve maintainability, and create a more consistent and scalable solution. 🚀

Disadvantages of Wrapping the Third-Party Grid in a Custom Angular Component

While wrapping a third-party grid in a custom Angular component offers many benefits, it’s not always the right choice. It’s ideal for applications with multiple screens requiring a consistent UX but may not suit every use case.

👎🏽 Higher Initial Development Effort — Building a wrapper, state service, and subcomponents requires more upfront work, which may not be justified for a few grid instances.

👎🏽 Additional Abstraction to Maintain — If the third-party library rarely changes and is manageable, a wrapper may add unnecessary complexity.

👎🏽 Overhead for Simple Use Cases — For minimal grid instances with little customization, managing state and configuration may be excessive.

👎🏽 Less Flexibility for Unique Grids — Centralized configurations can limit highly specialized grid implementations that require distinct behaviors.

👎🏽 Learning Curve for Developers — Teams must learn the wrapper’s structure before using the grid, which can slow onboarding.

👎🏽 Requires Consistent Grid Usage — If grids vary significantly across screens, a per-instance approach might be more practical than enforcing uniformity.

When Should You Use This Approach?

This solution fits best for applications that:
✔️ Manage multiple screens with grid-based views.
✔️ Require a consistent UX for sorting, filtering, pagination, and interactions.
✔️ Want to reduce code duplication and centralize configuration.
✔️ Need better state management to avoid redundant API calls and pagination resets.
✔️ Are scalable, meaning new features and grid capabilities will be added over time.

This solution may not be necessary for applications that:
❌ Only use a few grid instances with minimal customization.
❌ Require highly specialized behavior per grid instance instead of a shared implementation.
❌ Do not prioritize consistent UX across different screens.
❌ Prefer direct per-instance control over sorting, filtering, and pagination logic.

By understanding these trade-offs, teams can decide whether wrapping a third-party grid is the right choice for their application’s architecture and long-term maintainability. 🚀

--

--