Hone logo
Hone
Problems

Angular Component Refactoring Challenge

This challenge focuses on applying common refactoring techniques to an existing Angular component. The goal is to improve its readability, maintainability, and testability by extracting logic into separate methods, services, or components. This exercise is crucial for building robust and scalable Angular applications.

Problem Description

You are provided with an Angular component that handles displaying a list of products and their availability. Currently, the component's logic is tightly coupled within the template and the component class, making it difficult to read and reuse. Your task is to refactor this component by:

  1. Extracting complex template logic: Move conditional rendering or complex data transformations out of the template into component methods.
  2. Introducing a service: Isolate data fetching and manipulation logic into a dedicated Angular service.
  3. Improving component responsibilities: Ensure the component primarily focuses on presentation and delegation to services.

Key Requirements:

  • The refactored component should maintain the exact same UI and behavior as the original.
  • All product data fetching should be handled by a new Angular service.
  • Any logic that makes the template "heavy" (e.g., complex conditional formatting, filtering) should be moved to methods within the component class.
  • The component should be more testable after refactoring.

Expected Behavior:

  • The component should fetch a list of products upon initialization.
  • Each product should display its name, price, and availability status (e.g., "In Stock", "Out of Stock").
  • Products that are out of stock should be visually distinct (e.g., grayed out or have a different background color).
  • A button should exist to toggle the display of out-of-stock products.

Edge Cases to Consider:

  • What happens if the product list is empty?
  • How will the component handle potential errors during data fetching? (For this challenge, assume successful data fetching, but be mindful of this for future development).

Examples

Example 1: Original Component (Conceptual)

product-list.component.ts (Before Refactoring)

import { Component, OnInit } from '@angular/core';

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css'],
})
export class ProductListComponent implements OnInit {
  products: Product[] = [];
  showOutOfStock = true;

  ngOnInit() {
    // Simulate fetching product data
    this.products = [
      { id: 1, name: 'Laptop', price: 1200, inStock: true },
      { id: 2, name: 'Mouse', price: 25, inStock: true },
      { id: 3, name: 'Keyboard', price: 75, inStock: false },
      { id: 4, name: 'Monitor', price: 300, inStock: true },
      { id: 5, name: 'Webcam', price: 50, inStock: false },
    ];
  }

  toggleOutOfStock() {
    this.showOutOfStock = !this.showOutOfStock;
  }

  // Direct logic in template to check availability
}

product-list.component.html (Before Refactoring)

<div>
  <h2>Product List</h2>
  <button (click)="toggleOutOfStock()">
    {{ showOutOfStock ? 'Hide' : 'Show' }} Out of Stock Products
  </button>

  <ul>
    <li *ngFor="let product of products">
      <ng-container *ngIf="showOutOfStock || product.inStock">
        <span [class.out-of-stock]="!product.inStock">{{ product.name }}</span>
        - ${{ product.price }}
      </ng-container>
    </li>
  </ul>
</div>

product-list.component.css (Before Refactoring)

.out-of-stock {
  color: gray;
  text-decoration: line-through;
}

Output (Conceptual): The UI displays the product list. Products like "Keyboard" and "Webcam" are visually distinct. The button correctly toggles their visibility.

Example 2: Refactored Component (Conceptual Goal)

product-list.component.ts (After Refactoring)

import { Component, OnInit } from '@angular/core';
import { ProductService } from '../services/product.service'; // New service
import { Product } from '../models/product.model'; // Model

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css'],
})
export class ProductListComponent implements OnInit {
  allProducts: Product[] = []; // Holds all fetched products
  displayProducts: Product[] = []; // Products to be displayed
  showOutOfStock = true;

  constructor(private productService: ProductService) {}

  ngOnInit() {
    this.loadProducts();
  }

  loadProducts() {
    this.productService.getProducts().subscribe(products => {
      this.allProducts = products;
      this.updateDisplayProducts();
    });
  }

  toggleOutOfStock() {
    this.showOutOfStock = !this.showOutOfStock;
    this.updateDisplayProducts();
  }

  // Method to filter products based on showOutOfStock
  updateDisplayProducts() {
    if (this.showOutOfStock) {
      this.displayProducts = [...this.allProducts]; // Create a new array
    } else {
      this.displayProducts = this.allProducts.filter(product => product.inStock);
    }
  }

  // Method to determine CSS class for availability
  getAvailabilityClass(product: Product): { [key: string]: boolean } {
    return {
      'out-of-stock': !product.inStock,
    };
  }
}

product.service.ts (New Service)

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Product } from '../models/product.model';

@Injectable({
  providedIn: 'root',
})
export class ProductService {
  getProducts(): Observable<Product[]> {
    // Simulate fetching product data
    const products: Product[] = [
      { id: 1, name: 'Laptop', price: 1200, inStock: true },
      { id: 2, name: 'Mouse', price: 25, inStock: true },
      { id: 3, name: 'Keyboard', price: 75, inStock: false },
      { id: 4, name: 'Monitor', price: 300, inStock: true },
      { id: 5, name: 'Webcam', price: 50, inStock: false },
    ];
    return of(products);
  }
}

product.model.ts (New Model)

export interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

product-list.component.html (After Refactoring)

<div>
  <h2>Product List</h2>
  <button (click)="toggleOutOfStock()">
    {{ showOutOfStock ? 'Hide' : 'Show' }} Out of Stock Products
  </button>

  <ul>
    <li *ngFor="let product of displayProducts">
      <span [ngClass]="getAvailabilityClass(product)">{{ product.name }}</span>
      - ${{ product.price }}
    </li>
  </ul>
</div>

product-list.component.css (Same as before)

.out-of-stock {
  color: gray;
  text-decoration: line-through;
}

Explanation: The logic for fetching products is now in ProductService. The ProductListComponent delegates this responsibility. The updateDisplayProducts method filters the products based on the showOutOfStock flag, and getAvailabilityClass handles the conditional styling, making the template cleaner and more declarative.

Constraints

  • The solution must be implemented in TypeScript.
  • The refactored component must use Angular's dependency injection for the new service.
  • The use of RxJS operators for data handling within the service is encouraged.
  • No third-party libraries beyond Angular core and RxJS are allowed for the refactoring logic itself.
  • The time complexity of filtering products should be efficient, ideally O(n) where n is the number of products.

Notes

  • Consider how you will handle the initial loading of products in ngOnInit.
  • Think about the best place to perform the filtering logic – should it happen in the service, the component, or a combination?
  • The goal is to create reusable and testable code. How does your refactoring contribute to this?
  • This exercise is about applying refactoring patterns. While error handling isn't the primary focus, consider how you might incorporate it in a real-world scenario.
  • You will need to create a new service file (.service.ts) and potentially a model file (.model.ts) to organize your code effectively.
Loading editor...
typescript