๐—™๐—ฟ๐—ผ๐—บ ๐—œ๐—ณ-๐—˜๐—น๐˜€๐—ฒ ๐˜๐—ผ ๐——๐—ฒ๐˜€๐—ถ๐—ด๐—ป ๐—ฃ๐—ฎ๐˜๐˜๐—ฒ๐—ฟ๐—ป๐˜€: ๐˜”๐˜บ ๐˜‘๐˜ฐ๐˜ถ๐˜ณ๐˜ฏ๐˜ฆ๐˜บ ๐˜ช๐˜ฏ ๐˜ž๐˜ณ๐˜ช๐˜ต๐˜ช๐˜ฏ๐˜จ ๐˜‰๐˜ฆ๐˜ต๐˜ต๐˜ฆ๐˜ณ ๐˜Š๐˜ฐ๐˜ฅ๐˜ฆ

Shanit PaulShanit Paul
3 min read

A few days ago, I was working on dynamically rendering different chart components in an Angular app.

These charts were being loaded from Micro Frontends as web components, and I needed to create them dynamically based on a config array.

๐Ÿ”…Initial Approach: The If-Else Trap ๐Ÿคฆโ€โ™‚๏ธ

At first, I went with a basic approach, using multiple if-else or switch-case statements to check the โ€œwidgetTypeโ€ and render the respective component.

(I have shown only 3 charts but there were at least 8โ€“9 different kinds of custom components including charts, maps and cards etc.)

<ng-container class="chart-container">
  @if(widgetType === 'bar'){
    <chs-bar-chart [data]="chartData"></chs-bar-chart>
  }
  @else if(widgetType === 'pie'){
    <chs-pie-chart [data]="chartData"></chs-pie-chart>
  }
  @else if(widgetType === 'line'){
    <chs-line-chart [data]="chartData"></chs-line-chart>
  }
  @else{
    <div> No chart types found</div>
  }
</ng-container>

It worked, but it wasnโ€™t scalable. Every time a new chart type was added, I had to modify my code, making it harder to maintain. And my seniors kept saying this code is โ€œTRASHโ€ and โ€œSTOP USING IF-ELSE ๐Ÿ˜  โ€œ

๐Ÿ”…Turning Point: Discovering Design Patterns through one of my mentors. Thatโ€™s when I explored Design Patterns, and it completely changed how I approached the problem.

So I used the :

โœ… ๐—™๐—ฎ๐—ฐ๐˜๐—ผ๐—ฟ๐˜† ๐—ฃ๐—ฎ๐˜๐˜๐—ฒ๐—ฟ๐—ป to dynamically generate components based on the โ€œwidgetTypeโ€ property.
โœ… ๐—ฆ๐˜๐—ฟ๐—ฎ๐˜๐—ฒ๐—ด๐˜† ๐—ฃ๐—ฎ๐˜๐˜๐—ฒ๐—ฟ๐—ป to handle different API configurations for each chart type.

The Factory Pattern allows us to create components dynamically without modifying the main logic.

๐Ÿ”น Step 1: Create a Factory Service

@Injectable({
  providedIn: 'root'
})
export class ChartFactoryService {
  constructor(private resolver: ComponentFactoryResolver) {}

  createChart(widgetType: string, container: ViewContainerRef) {
    container.clear();

    const componentMap: { [key: string]: Type<any> } = {
      bar: ChsBarChartComponent,
      pie: ChsPieChartComponent,
      line: ChsLineChartComponent
    };

    const component = componentMap[widgetType];

    if (!component) {
      console.error('Unknown chart type');
      return;
    }

    const factory = this.resolver.resolveComponentFactory(component);
    container.createComponent(factory);
  }
}
  • Eliminates the if-else/switch case.

  • Easily extendableโ€Šโ€”โ€Šjust update componentMap.

๐Ÿ”น Step 2: Use the Factory in the Component

@Component({
  selector: 'app-dashboard',
  template: `<ng-container #chartContainer></ng-container>`
})
export class DashboardComponent {
  @ViewChild('chartContainer', { read: ViewContainerRef }) chartContainer!: ViewContainerRef;
  constructor(private chartFactory: ChartFactoryService) {}

  renderChart(widgetType: string) {
    this.chartFactory.createChart(widgetType, this.chartContainer);
  }
}

Now, adding a new chart type only requires adding it to the componentMap, without modifying logic in multiple places.

โœ… Enhancing with Strategy Pattern

If different chart types require different API handling, we use the Strategy Pattern.

Step 1: Define an Interface for Chart Strategies

export interface ChartStrategy {
  fetchData(apiUrl: string): Observable<any>;
}

Step 2: Implement Different Strategies for Each Chart

@Injectable({ providedIn: 'root' })
export class BarChartStrategy implements ChartStrategy {
  constructor(private http: HttpClient) {}

  fetchData(apiUrl: string): Observable<any> {
    return this.http.get(`${apiUrl}/bar-chart-data`);
  }
}
@Injectable({ providedIn: 'root' })
export class PieChartStrategy implements ChartStrategy {
  constructor(private http: HttpClient) {}

  fetchData(apiUrl: string): Observable<any> {
    return this.http.get(`${apiUrl}/pie-chart-data`);
  }
}

Step 3: Implement a Service to Pick the Right Strategy

@Injectable({
  providedIn: 'root'
})
export class ChartDataService {
  private strategies: { [key: string]: ChartStrategy } = {};
  constructor(
    barChartStrategy: BarChartStrategy,
    pieChartStrategy: PieChartStrategy
  ) {
    this.strategies['bar'] = barChartStrategy;
    this.strategies['pie'] = pieChartStrategy;
  }
  getData(widgetType: string, apiUrl: string): Observable<any> {
    const strategy = this.strategies[widgetType];
    if (!strategy) {
      console.error('No strategy found for', widgetType);
      return of(null);
    }
    return strategy.fetchData(apiUrl);
  }
}
  • Each chart type has its own API logic.

  • Decouples data fetching logic from the main component.

Step 4: Use the Strategy Service in Your Component

this.chartDataService.getData(widgetType, apiUrl).subscribe(data => {
  console.log('Chart Data:', data);
});

Refactoring this felt like a game-changer. Now my code is:

โบ๏ธ More flexibleโ€Šโ€”โ€ŠI can add new charts without touching existing logic.
โบ๏ธ Easier to maintainโ€Šโ€”โ€ŠNo more messy if-else chains or Switch cases.
โบ๏ธ Optimized for Micro Frontendsโ€Šโ€”โ€ŠCharts load seamlessly as Web Components.

๐Ÿ”…Learnings: Good code isnโ€™t just about making it workโŒ
Itโ€™s about making it scalable and future-proof โœ”๏ธ

Even in frontend there are ways to ensure the code is not just basic โ€œlogic and executeโ€.

0
Subscribe to my newsletter

Read articles from Shanit Paul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shanit Paul
Shanit Paul