
In this post, we’ll explore how to create a web component for cart items in an e-commerce application. Web components allow for encapsulation and reusability, making them ideal for such features.
This is the first blog post in a trilogy related to vanilla JavaScript Web Components:
The ecommerce app implementation is shown in the image below. We are going to focus on the CartItem Web Component, located to the right, which renders inside the Cart container.

Feel free to explore the entire ecommerce codebase by visiting the GitHub repository. To try the app in action, you can click here.
CartItem ClassFirst, we define the CartItem class extending from HTMLElement. This class will manage the cart item’s data and render its HTML structure:
export class CartItem extends HTMLElement {
  // Define the private state variable
  #state;
  // Constructor takes an object with properties like:
  // 'name', 'image', 'alt', 'quantity', 'price', and 'subTotal'
  constructor({ name, image, alt, quantity, price, subTotal }) {
    // Define properties for the element
    super();
    // Initialize properties with the values passed in the constructor
    this.name = name;
    this.image = image;
    this.alt = alt;
    this.quantity = quantity;
    this.price = price;
    this.isRemoveCTAHidden = true;
    // Set 'subTotal' to the provided value or default to the product price
    this.subTotal = subTotal ?? this.price;
    // Initialize the 'state' object with the initial values of the properties
    this.#state = {
      quantity: this.quantity,
      subTotal: this.subTotal
    }
  }
}Next, we implement getters and setters to access and update the internal state of the CartItem component. This allows controlled access to the component’s data:
// Getter method to get the value of a property in the state object
getState(path) {
  return this.#state[path];
}
// Setter method to set the value of a property in the state object
setState(path, value) {
  if (this.#state[path] !== value) {
    this.#state = { ...this.#state, [path]: value };
  }
}render functionThe render function manages the HTML structure to append to the selected DOM node. Here, we’re using template literals since it’s a basic component.
Keep in mind that the numberToPrice function could incorporate custom logic for price formatting.
Additionally, we’ve included a ternary operator to toggle the hidden class in the remove button. This allows us to show or hide the button based on the isRemoveCTAHidden prop, which is set by default to true:
For more details on the CSS styles, please check out the codebase by visiting the GitHub repository.
// Render method to create the initial HTML structure of the component
render() {
  this.innerHTML = `
    <li data-name="${this.name}">
      <div class="plate">
        <img src="images/${this.image}" alt="${this.alt}" class="plate" />
        <div class="quantity">${this.getState('quantity')}</div>
      </div>
      <div class="content">
        <p class="menu-item">${this.name}</p>
        <p class="price">${numberToPrice(this.price)}</p>
      </div>
      <div class="quantity__wrapper">
        <button class="decrease">
          <img src="images/chevron.svg" />
        </button>
        <div class="quantity">${this.getState('quantity')}</div>
        <button class="increase">
          <img src="images/chevron.svg" />
        </button>
      </div>
      <div class="subtotal">${numberToPrice(this.getState('subTotal'))}</div>
      <button class="remove ${this.isRemoveCTAHidden ? 'hidden' : ''}">Remove</button>
    </li>
  `;
}increaseQuantity and decreaseQuantity methodsIn this step, we focus on implementing the increaseQuantity and decreaseQuantity methods within the CartItem component. These methods handle the functionality of adjusting the quantity of an item in the cart and updating the subtotal accordingly:
// Private method to increase the quantity and update the subTotal property in the state object.
#increaseQuantity() {
  // Get the current quantity value from the state
  const currentValue = this.getState('quantity');
  // Calculate the new quantity by incrementing the current value
  const newValue = currentValue + 1;
  // Update the quantity and subTotal properties in the state
  this.setState('quantity', newValue);
  this.setState('subTotal', newValue * this.price);
  // Hide the remove button if the quantity is greater than 0
  this.isRemoveCTAHidden = true;
  // Re-render the cart item with the updated values
  this.render();
}
// Private method to decrease the quantity and update the subTotal property in the state object.
#decreaseQuantity() {
  // Get the current quantity value from the state
  const currentValue = this.getState('quantity');
  // Calculate the new quantity by decrementing the current value
  const newValue = currentValue - 1;
  // Check if the new quantity is greater than 0 before updating
  if (currentValue > 0) {
    this.setState('quantity', newValue);
    this.setState('subTotal', newValue * this.price);
  }
  // Update the visibility of the remove button based on the quantity
  if (this.getState('quantity') === 0) {
    this.isRemoveCTAHidden = false;
  }
  // Re-render the cart item with the updated values
  this.render();
}In this step, we will explore the removeFromCartEvent and quantityChangeEvent methods in the CartItem component, responsible for dispatching custom events related to cart actions:
By utilizing these methods to dispatch custom events, the CartItem component can communicate changes in item quantities and removal actions effectively within the application, enhancing the overall user experience and functionality.
// Private method to dispatch a custom event when 'Remove from cart' button is clicked
#removeFromCartEvent() {
  // Dispatch a custom event to remove the item from the cart
  document.dispatchEvent(new CustomEvent('remove-from-cart', {
    detail: { name: this.name }
  }));
}
// Private method to dispatch custom events for 'Decrease quantity'
// and 'Increase quantity'
#quantityChangeEvent(event) {
  // Prepare product details for the custom event
  const productDetails = {
    name: this.name,
    quantity: this.quantity,
    price: this.price
  };
  // Dispatch custom events based on the event type
  if (event === 'decrease-quantity') {
    // Dispatch the decreaseQuantity event with product details
    document.dispatchEvent(new CustomEvent('decrease-quantity', {
      detail: productDetails
    }));
  }
  if (event === 'increase-quantity') {
    // Dispatch the increaseQuantity event with product details
    document.dispatchEvent(new CustomEvent('increase-quantity', {
      detail: productDetails
    }));
  }
}click event handlerIn this step, we will delve into the handleButtonClick method of the CartItem component, which manages the actions triggered by clicking on buttons within the cart item:
// Private method to handle click events on the remove,
// decrease, and increase buttons
#handleButtonClick(event) {
  // Extract the target element from the event
  const { target } = event;
  // Find the closest button element and get its class list
  const targetClassList = target.closest('button').classList;
  // Check the class of the clicked button and perform corresponding actions
  if (targetClassList.contains('remove')) {
    // If the button is for removing, trigger the remove event
    this.#removeFromCartEvent();
  } else if (targetClassList.contains('decrease')) {
    // If the button is for decreasing quantity, decrease the quantity
    // and trigger the decrease quantity event
    this.#decreaseQuantity();
    this.#quantityChangeEvent('decrease-quantity');
  } else if (targetClassList.contains('increase')) {
    // If the button is for increasing quantity, increase the quantity
    // and trigger the increase quantity event
    this.#increaseQuantity();
    this.#quantityChangeEvent('increase-quantity');
  }
}connectedCallback and disconnectedCallbackThe connectedCallback method is where we render the cart item and attach event listeners. Here, we dynamically create the HTML structure and append it to the DOM.
The disconnectedCallback method is where we detach event listeners when the component is removed from the DOM to avoid memory leaks:
// ConnectedCallback is called when the element is inserted into the DOM
connectedCallback() {
  // Render the initial state
  this.render();
  // Add event listeners for remove, decrease, and increase buttons
  this.addEventListener('click', this.#handleButtonClick)
}
// DisconnectedCallback is called when the element is removed from the DOM
disconnectedCallback() {
  // Remove previously added event listeners
  this.removeEventListener('click', this.#handleButtonClick)
}Finally, we register the custom element using window.customElements.define:
window.customElements.define('cart-item', CartItem);That’s it!. We now have a fully functional CartItem Web Component to be integrated in the ecommerce app.
Click on the collapsible section to see the entire code:
export class CartItem extends HTMLElement {
  #state;
  constructor({ name, image, alt, quantity, price, subTotal }) {
    super();
    this.name = name;
    this.image = image;
    this.alt = alt;
    this.quantity = quantity;
    this.price = price;
    this.isRemoveCTAHidden = true;
    this.subTotal = subTotal ?? this.price;
    this.#state = {
      quantity: this.quantity,
      subTotal: this.subTotal
    }
  }
  getState(path) {
    return this.#state[path];
  }
  setState(path, value) {
    if (this.#state[path] !== value) {
      this.#state = { ...this.#state, [path]: value };
    }
  }
  connectedCallback() {
    this.render();
    this.addEventListener('click', this.#handleButtonClick)
  }
  disconnectedCallback() {
    this.removeEventListener('click', this.#handleButtonClick)
  }
  #handleButtonClick(event) {
    const { target } = event;
    const targetClassList = target.closest('button').classList;
    if (targetClassList.contains('remove')) {
      this.#removeFromCartEvent();
    } else if (targetClassList.contains('decrease')) {
      this.#decreaseQuantity();
      this.#quantityChangeEvent('decrease-quantity');
    } else if (targetClassList.contains('increase')) {
      this.#increaseQuantity();
      this.#quantityChangeEvent('increase-quantity');
    }
  }
  #increaseQuantity() {
    const currentValue = this.getState('quantity');
    const newValue = currentValue + 1;
    this.setState('quantity', newValue);
    this.setState('subTotal', newValue * this.price);
    this.isRemoveCTAHidden = true;
    this.render();
  }
  #decreaseQuantity() {
    const currentValue = this.getState('quantity');
    const newValue = currentValue - 1;
    if (currentValue > 0) {
      this.setState('quantity', newValue);
      this.setState('subTotal', newValue * this.price);
    }
    if (this.getState('quantity') === 0) {
      this.isRemoveCTAHidden = false;
    }
    this.render();
  }
  #removeFromCartEvent() {
    document.dispatchEvent(CartEvents.removeFromCart(this.name));
  }
  #quantityChangeEvent(event) {
    const productDetails = {
      name: this.name,
      quantity: this.quantity,
      price: this.price
    };
    if (event === 'decrease-quantity') {
      document.dispatchEvent(CartEvents.decreaseQuantity(productDetails));
    }
    if (event === 'increase-quantity') {
      document.dispatchEvent(CartEvents.increaseQuantity(productDetails));
    }
  }
  render() {
    this.innerHTML = `
      <li data-name="${this.name}">
        <div class="plate">
          <img src="images/${this.image}" alt="${this.alt}" class="plate" />
          <div class="quantity">${this.getState('quantity')}</div>
        </div>
        <div class="content">
          <p class="menu-item">${this.name}</p>
          <p class="price">${numberToPrice(this.price)}</p>
        </div>
        <div class="quantity__wrapper">
          <button class="decrease">
            <img src="images/chevron.svg" />
          </button>
          <div class="quantity">${this.getState('quantity')}</div>
          <button class="increase">
            <img src="images/chevron.svg" />
          </button>
        </div>
        <div class="subtotal">${numberToPrice(this.getState('subTotal'))}</div>
        <button class="remove ${this.isRemoveCTAHidden ? 'hidden' : ''}">Remove</button>
      </li>
    `;
  }
}
window.customElements.define('cart-item', CartItem);In order to use this component in other apps or directly in the HTML with the custom <cart-item></cart-item> tag, we need to make some changes:
super() method and the shadow DOM with mode: open to encapsulate the CSS stylesstatic get observedAttributes() method for observing attributesattributeChangedCallback method for responding to observed attributes changestemplate for storing the HTML structure<style></style> tag in a new getStyles() method and executing it inside the templatetemplate to the shadowRoot for renderingFor more details, refer to the Web Components official documentation.
This approach to creating the CartItem component encapsulates its functionality and presentation, making it a reusable and maintainable part of the application’s UI.