Exploring Angular Wrapper Component Patterns with PrimeNG
In Angular applications, we often come across scenarios where we need to create wrapper components around UI libraries to enhance their functionality, simplify usage, or adapt them to our app’s needs. In this article, we’ll explore a few different patterns for creating wrapper components around PrimeNG components, highlighting their use cases, advantages, and disadvantages.
Pattern 1: Directly Wrapping a Component
One of the most straightforward approaches is to create a wrapper component that directly wraps another component and exposes its inputs and outputs. This approach works best for simple components but can become cumbersome when the wrapped component has many inputs and outputs.
Example: Wrapping PrimeNG’s p-inputTextarea
Let’s wrap PrimeNG’s p-inputTextarea
component in a custom wrapper component:
@Component({
selector: 'app-custom-input-textarea',
template: `
<p-inputTextarea
[rows]="rows"
[cols]="cols"
[autoResize]="autoResize"
(onChange)="handleChange($event)"
></p-inputTextarea>
`,
})
export class CustomInputTextareaComponent {
@Input() rows = 5;
@Input() cols = 30;
@Input() autoResize = false;
@Output() onChange = new EventEmitter<Event>();
handleChange(event: Event) {
this.onChange.emit(event);
}
}
Disadvantages:
- Limited Flexibility: For every input/output in
p-inputTextarea
, we need to bind it manually. If PrimeNG updates the component or adds more inputs/outputs, the wrapper component needs to be updated as well. - Feature Restriction: By manually binding all inputs/outputs, the wrapper can restrict the component’s full functionality unless you manually add every feature
Pattern 2: Wrapping with Templates
A more flexible approach is to pass templates as child content within the wrapper. This allows customizable layouts, where the parent can inject content such as headers, footers, or other sections. Here, we’ll show how to wrap PrimeNG’s p-card
component, allowing users to pass in templates for the card's header, content, and footer.
In this example, we use directives for the card header and content but not for the footer. This demonstrates that template projection can be done both with and without directives.
Example: Wrapping PrimeNG Card with Custom Templates
We wrap p-card
and allow users to pass custom templates for the card’s header, content, and footer.
@Directive({ selector: '[appCardHeaderTemplate]', standalone: true })
export class CardHeaderTemplateDirective {
constructor(public template: TemplateRef<any>) {}
}
@Directive({ selector: '[appCardContentTemplate]', standalone: true })
export class CardContentTemplateDirective {
constructor(public template: TemplateRef<any>) {}
}
@Component({
selector: 'app-custom-card',
template: `
<p-card>
<!-- Card Header Template -->
<ng-template *ngIf="cardHeaderTemplate" pTemplate="header">
<ng-container [ngTemplateOutlet]="cardHeaderTemplate"></ng-container>
</ng-template>
<!-- Card Content Template -->
<ng-template *ngIf="cardContentTemplate">
<ng-container [ngTemplateOutlet]="cardContentTemplate"></ng-container>
</ng-template>
<!-- Card Footer Template (without directive) -->
<ng-template *ngIf="cardFooterTemplate" pTemplate="footer">
<ng-container [ngTemplateOutlet]="cardFooterTemplate"></ng-container>
</ng-template>
</p-card>
`,
})
export class CustomCardComponent {
@ContentChild(CardHeaderTemplateDirective, { read: TemplateRef })
cardHeaderTemplate!: TemplateRef<any>;
@ContentChild(CardContentTemplateDirective, { read: TemplateRef })
cardContentTemplate!: TemplateRef<any>;
@ContentChild('appCardFooterTemplate', { read: TemplateRef })
cardFooterTemplate!: TemplateRef<any>;
}
Usage:
In the consumer’s template, you can specify the card’s header, content, and footer. Notice that for the header and content, we use the custom directive selectors, while for the footer, we directly use ng-template
without a directive.
<app-custom-card>
<!-- Card Header -->
<ng-template appCardHeaderTemplate>
<h3>Custom Card Header</h3>
</ng-template>
<!-- Card Content -->
<ng-template appCardContentTemplate>
<p>This is the custom card content.</p>
</ng-template>
<!-- Card Footer (no directive) -->
<ng-template #appCardFooterTemplate>
<button>Action</button>
</ng-template>
</app-custom-card>
Advantages:
- Explicit Content Placement: By using custom directives like
appCardHeaderTemplate
andappCardContentTemplate
, it's clear which parts of the card are customizable. - Flexibility: The footer template is passed without using a directive, showing that you can handle templates both with and without structural directives.
Pattern 3: Passing the Entire Component with ng-content
In some cases, you might not need to explicitly bind or manage inputs/outputs. Instead, you can simply pass the entire component as ng-content
. This approach avoids the need to expose specific inputs/outputs, allowing users to directly interact with the component inside the wrapper.
Example: Passing PrimeNG InputTextarea with ng-content
In this example, we wrap PrimeNG’s p-inputTextarea
inside a custom wrapper component and allow users to customize it via ng-content
.
@Component({
selector: 'app-custom-wrapper',
template: `
<div class="wrapper-container">
<ng-content></ng-content>
</div>
`,
})
export class CustomWrapperComponent {}
Usage:
<app-custom-wrapper>
<p-inputTextarea [rows]="6" [cols]="50" autoResize="true"></p-inputTextarea>
</app-custom-wrapper>
Advantages:
- No Need to Expose Inputs/Outputs: Since the component is passed directly as content, there’s no need to bind inputs/outputs manually. The parent component can control the child component’s properties directly.
- Simplicity: This approach requires minimal setup, making it a good option for straightforward use cases.
Conclusion
In this article, we explored three different patterns for creating wrapper components around PrimeNG components:
- Direct Wrapping: Simple but can restrict functionality and require manual input/output binding.
- Template Wrapping with Directives: Flexible and allows explicit content placement but adds complexity with custom directives.
- Passing Components via
ng-content
: The simplest and most flexible approach, but not suitable for complex or highly controlled scenarios.
Each pattern serves different use cases, and the best choice depends on the complexity of the component and the level of customization required. By combining these approaches, you can create reusable, maintainable, and flexible components in your Angular applications.