civil-and-structural-engineering
Using the Prototype Pattern to Clone Ui Components in Angular Applications
Table of Contents
Introduction to the Prototype Pattern in Angular
Modern Angular applications often require dynamic generation of UI components—think dashboards with hundreds of cards, lists of interactive widgets, or repeatable form sections. Creating each component from scratch using ngFor and @Input bindings works well for simple cases, but can become expensive when components are complex, involve heavy initialization logic, or need to carry default configurations. The Prototype Pattern, a creational design pattern from the Gang of Four, offers a better way: instead of constructing new objects from scratch, you clone a pre-configured prototype instance. This article explores how to apply this pattern to clone Angular UI components efficiently, with practical examples and performance insights.
What is the Prototype Pattern?
The Prototype Pattern solves the problem of object creation when instantiation is costly or when you need many objects that differ only slightly. Rather than using constructors or factories, you define a prototype object and then create new objects by copying it. The copy can then be modified to meet specific requirements. This is particularly useful in UI development where components often share a common structure (e.g., a card template) but vary in data or styling.
JavaScript’s dynamic nature makes the Prototype Pattern straightforward to implement—you can use Object.create(), the spread operator, or dedicated libraries for deep cloning. In Angular, the pattern aligns well with component-based architecture because each component can serve as a prototype, and cloning can be done at the data or template level.
Why Clone UI Components in Angular?
Angular already provides excellent tools for rendering lists of components via structural directives like *ngFor. Why would you need cloning? Consider these scenarios:
- Pre-configured Widgets: You have a complex widget component with default configurations (e.g., chart options, event handlers, custom styles). Cloning a prototype widget saves the overhead of re-running constructor logic or setting up default data for each instance.
- Dynamic Form Sections: In a form builder, users can add repeated sections (e.g., address blocks, product entries). Instead of re-creating the form control structure from scratch, clone a prototype of the entire section.
- High-Performance Rendering: When you need to spawn many identical components (e.g., a grid of tiles), cloning a pre-initialized component can reduce change detection cycles and memory allocations.
- State Preservation: The prototype pattern is ideal when you want to preserve a "default" state of a component and then clone it for editing, leaving the original untouched.
Deep Dive: Implementations of Cloning in Angular
Angular does not have a built-in API for cloning components or their data models. Instead, you implement cloning at the object level—either on component input data or on the entire component class instance using ViewContainerRef and ComponentFactoryResolver. Below we cover both approaches.
1. Cloning Data Objects (Shallow and Deep)
The most common use case is cloning the data that drives a component’s template. A component like `CardComponent` receives inputs for title, content, image URL, etc. You can create a prototype data object and clone it with variations.
Shallow copy using spread operator:
const prototypeCard = {
title: 'Default',
content: 'Default content',
config: { color: 'blue' } // nested object
};
const card1 = { ...prototypeCard, title: 'Card 1' }; // shallow copy
const card2 = { ...prototypeCard, title: 'Card 2' }; // shallow copy
⚠️ Warning: Shallow copy shares references to nested objects (e.g., config). If you mutate card1.config.color, prototypeCard.config.color and card2.config.color also change because they point to the same object. For UI components where configuration objects are often immutable, this may be fine. But if you need true independence, use a deep clone.
Deep copy using Lodash:
import cloneDeep from 'lodash/cloneDeep';
const prototype = {
title: 'Default',
config: { color: 'blue', size: 'medium' }
};
const card1 = cloneDeep(prototype);
card1.title = 'Card 1';
card1.config.color = 'red'; // safe: independent from prototype
Alternatively, you can use JSON.parse(JSON.stringify(prototype)) for simple objects, but this will lose functions, undefined, Symbols, and dates. Lodash cloneDeep is more reliable for UI-related data containing nested objects and arrays.
External resource: Lodash cloneDeep documentation.
2. Cloning Component Instances in the Template
Sometimes you need to clone the actual component instance rather than just its data. For example, you want to duplicate a dynamically created component that has its own lifecycle, subscriptions, and internal state. Angular’s ViewContainerRef and ComponentFactoryResolver allow you to create a component programmatically. You can treat an existing component instance as a prototype by storing its factory and input configurations.
Example: Dynamic Widget Factory
Suppose you have a WidgetComponent that is created dynamically with default settings. Instead of re-creating the widget’s internal state from scratch, you can store a "clone template" – a function that returns a new component with the same initial state.
import { ComponentFactoryResolver, ViewContainerRef, ComponentRef } from '@angular/core';
import { WidgetComponent } from './widget.component';
export class WidgetService {
private widgetFactory: ComponentFactory<WidgetComponent>;
constructor(private resolver: ComponentFactoryResolver) {
this.widgetFactory = this.resolver.resolveComponentFactory(WidgetComponent);
}
createWidget(container: ViewContainerRef, prototypeData: any): ComponentRef<WidgetComponent> {
const widgetRef = container.createComponent(this.widgetFactory);
// Apply prototype data (deep cloned for safety)
Object.assign(widgetRef.instance, cloneDeep(prototypeData));
widgetRef.instance.initialize(); // method to set up internal state
return widgetRef;
}
}
Now, whenever you need a new widget, call createWidget with a cloned version of the prototype data. This pattern gives you a fresh component instance with the same initial behavior as the prototype, but with independent internal state.
3. Using Structural Directives for Cloning via ngTemplateOutlet
Angular offers a powerful feature for cloning templates: ngTemplateOutlet. If you want to reuse a template structure (not a full component with logic), you can define it inside an <ng-template> and then render it multiple times, each time with different context data. This is essentially the Prototype Pattern applied to templates.
Example: Card template clone with context
<!-- Define prototype template -->
<ng-template #cardTemplate let-title="title" let-content="content">
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</ng-template>
<!-- Clone and customize -->
<ng-container *ngTemplateOutlet="cardTemplate; context: { title: 'Card 1', content: 'Content 1' }"></ng-container>
<ng-container *ngTemplateOutlet="cardTemplate; context: { title: 'Card 2', content: 'Content 2' }"></ng-container>
This approach doesn’t clone components but rather template fragments. It is extremely lightweight and avoids component lifecycle overhead when all you need is repeated structure with variable data. For complex UI components with event handling and change detection, you would still want full component cloning.
When to Use (and Not Use) the Prototype Pattern
The prototype pattern is not a silver bullet. Use it when:
- Object creation is expensive (e.g., components with heavy constructor logic, dependency injection, or asynchronous initialization).
- You need to generate many similar objects that differ only by a few properties.
- You want to keep a canonical "default" object and derive variants without mutating the original.
Avoid overuse when:
- Simple
*ngForwith input bindings suffices—the pattern adds unnecessary complexity. - You are working with deeply nested, mutable objects that require careful deep cloning – performance overhead may outweigh benefits.
- Your components depend heavily on Angular's change detection and need to be created within the component tree rather than manually.
Performance Implications
Cloning objects, especially deep cloning, has a cost. However, in many UI scenarios, the cost of cloning a data object (even a complex one) is negligible compared to the cost of creating a full Angular component with dependency injection and view creation. When you combine cloning with dynamic component creation, you save time by reusing the component factory and skipping redundant constructor logic.
Benchmark tip: If you are rendering hundreds of cards in a grid, measure the difference between creating a component class with new (not possible in Angular outside of DI) vs. providing data via *ngFor and cloning the data objects. The prototype pattern on data is effectively free. For actual component instances, the main performance gain comes from reducing setup logic per instance—for example, by caching computed styles or pre-registered event handlers on the prototype data.
Advanced: Cloning with Class Prototyping
You can also apply the Prototype Pattern at the class level using ES6 classes. Define a prototype class that holds default properties and methods, and then clone instances using Object.create() or the constructor approach.
class CardPrototype {
title = 'Default';
content = 'Default';
constructor() {}
display() { console.log(this.title); }
}
const proto = new CardPrototype();
// Clone and customize
const card1 = Object.create(proto);
card1.title = 'Custom Title';
This style is less common in Angular because components are managed via the framework's DI and lifecycle, but it can be useful for data models that represent component state.
Real-World Use Case: Building a Dashboard with Cloneable Widgets
Let's walk through a complete mini-application: a dashboard where users can add "widgets" (analytics cards) that all share a common layout but different data sources and sizes. Using the Prototype Pattern, we store a prototype widget configuration and clone it each time the user clicks "Add Widget".
Step 1: Define Widget Configuration Interface
export interface WidgetConfig {
title: string;
chartType: string;
dataUrl: string;
width: number;
height: number;
}
Step 2: Create a Prototype Configuration
const widgetPrototype: WidgetConfig = {
title: 'New Widget',
chartType: 'bar',
dataUrl: '/api/default',
width: 300,
height: 200
};
Step 3: Clone and Customize in Service
import { Injectable } from '@angular/core';
import cloneDeep from 'lodash/cloneDeep';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DashboardService {
private widgets = new BehaviorSubject<WidgetConfig[]>([]);
widgets$ = this.widgets.asObservable();
addWidget(overrides?: Partial<WidgetConfig>) {
const newConfig = { ...cloneDeep(widgetPrototype), ...overrides };
const current = this.widgets.getValue();
this.widgets.next([...current, newConfig]);
}
}
Step 4: Render Widgets in Dashboard Component
<div *ngFor="let widget of dashboardService.widgets$ | async" class="widget-container">
<app-widget [config]="widget"></app-widget>
</div>
Each widget component receives a fresh clone of the prototype, customized per user choices. The prototype pattern ensures that the default config is never mutated, and new widgets are cheap to create.
Summary of Benefits
- Efficiency: Avoids repeated costly initialization of prototypes.
- Consistency: All cloned components start with the same default state, reducing bugs.
- Immutability: The original prototype remains unchanged, serving as a safe template.
- Flexibility: Override only the properties that differ – the rest inherit from the prototype.
Considerations for Angular’s Change Detection
When using the Prototype Pattern with Angular components, be aware of how change detection treats cloned objects. If you clone an object and pass it to a component with @Input() that uses OnPush change detection strategy, the clone may be a new reference, triggering an update. This is usually desirable. However, if you mutate properties on a cloned object that was previously set, Angular (especially with OnPush) won’t detect the change. Always create new cloned instances to signal changes.
External Resources for Further Reading
- Prototype Pattern on Refactoring.Guru – a clear explanation with examples in multiple languages.
- Angular Documentation: Dynamic Component Loader – official guide on creating components programmatically.
- MDN: Object.create() – JavaScript’s native way to create objects with a prototype.
Conclusion
The Prototype Pattern is a practical tool for Angular developers who need to generate multiple UI components efficiently, especially when those components have complex default configurations or heavy initialization logic. By cloning prototype data objects or using dynamic component factories, you can achieve better performance, maintainability, and consistency. Whether you are building dashboards, form builders, or customizable widgets, applying the Prototype Pattern can streamline your Angular development workflow.