After moving from the PrimeNG component library to the Material library, having to manually re-implement each component instance, we had the exact same challenge. Moving forward, we wanted to de-couple the library components from the app as much as possible so that if ever we decided to change the library once again, it would be easier to replace.
The general idea is to create an app component that will act as a pass through component to a library component and all exposed functionality will be defined in an interface which the component implements.
A (rather) generic and simple example follows:
|-- Components
|-- button
|-- button.component.css
|-- button.component.html
|-- button.component.ts
|-- button.model.ts
|-- button.module.ts
The component files are the standard ones generated using the CLI. The .model.ts file contains the interface for the component and the .module.ts file contains an angular module also generated using the CLI.
button.component.html
<button (click)="clicked()"><i [ngClass]="icon"></i>{{text}}</button>
button.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { IButton } from './button.model';
@Component({
selector: 'app-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.css']
})
export class ButtonComponent implements OnInit, IButton {
@Input() text: string;
@Input() icon: string;
@Output() clickEvent: EventEmitter<void> = new EventEmitter<void>();
constructor() { }
ngOnInit() {
}
clicked(): void {
this.clickEvent.emit();
}
}
Define all of the inputs and outputs (as generically as possible) as an interface which the component (and any future implementation) will need to implement. This will make sure that the functionality of the component always remains the same.
button.model.ts
import { EventEmitter } from '@angular/core';
export interface IButton {
text: string;
icon: string;
clickEvent: EventEmitter<void>;
}
Declare and Export the component in the module.
button.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from './button.component';
@NgModule({
declarations: [
ButtonComponent
],
imports: [
CommonModule,
// component module imports here
],
exports: [
ButtonComponent
]
})
export class ButtonModule { }
Following the above method means that you only have to ever import the module to be able to reference the component in your app - you never directly reference the library component.
<app-button [text]="'export'" [icon]="'fas fa-file-export'" (clickEvent)="doSomething()"></app-button>
If ever you need to replace the component or extend it, make the necessary changes to the module (e.g. importing the library component module), the template and make sure that the component still implements the interface.