articles

Home / DeveloperSection / Articles / Product Drag and Drop with Knockout

Product Drag and Drop with Knockout

Product Drag and Drop with Knockout

Ravi Vishwakarma 62 31-May-2024

Drag and Drop with Knockout.js 

In modern web development, enhancing user interaction and providing intuitive interfaces is crucial. One of the popular methods to achieve this is through drag-and-drop functionality. Knockout.js, a powerful JavaScript library that follows the MVVM (Model-View-ViewModel) pattern, can be seamlessly integrated with drag-and-drop features to create dynamic and interactive web applications. This article will guide you through implementing drag-and-drop functionality in a Knockout.js-based application.

 

Why Use Knockout.js?

Knockout.js offers a clean way to manage complex data-driven interfaces. It simplifies the process of keeping the UI in sync with underlying data models using declarative bindings. When combined with drag-and-drop, it can significantly enhance the user experience by allowing intuitive interactions with the UI elements.

 

Setting Up the Environment

Before diving into the implementation, ensure you have the necessary libraries included in your HTML file:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Drag and Drop Demo</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"></script>
  <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- Your HTML content here -->
  <script src="script.js"></script>
</body>
</html>

 

HTML Structure

We will create a basic structure with two sections: one for selecting items and another for dropping the selected items.

<!-- Main container using Bootstrap's grid system -->
  <div class="container" data-bind="">
    <div>
      <!-- Header with main title -->
      <h1>
        Drag and Drop
      </h1>
      <hr />
    </div>

    <!-- Row for drag and drop sections -->
    <div class="row g-3">
      <!-- Column for selecting items to drag -->
      <div class="col-12 col-md-6 h-100" title="Select it">
        <p class="fw-semibold">Select It</p>
        <div class="droptarget border h-100 w-100 rounded-3 p-2" ondrop="drop(event)" ondragover="allowDrop(event)">
          <!-- List of products to drag -->
          <ul class="list-group" data-bind="foreach: products">
            <li data-bind="attr: { 'data-productid' : id, id : 'dragtarget' + id }" ondragstart="dragStart(event)"
              ondragend="dragEnd(event)" draggable="true" class="list-group-item product">
              
              <!-- Controls for adding or removing a product -->
              <div class="d-flex float-end position-absolute top-0 end-0">
                <span onclick="removeProduct(this)" title="Remove this product"
                class="d-none fs-5 pe-2 remove-product text-danger">
                <i class="bi bi-dash-circle-fill"></i></span>
                <span onclick="addProduct(this)" title="Add this product"
                class="fs-5 pe-2 add-product text-success">
                <i class="bi bi-plus-circle-fill"></i></span>
              </div>
              
              <!-- Product details -->
              <div class="d-flex">
                <img class="img-fluid" width="100" height="100" data-bind="attr: { src: img, alt: name }" />
                <div class="ms-2 w-100 h-100">
                  <h3 data-bind="text: name"></h3>
                  <p class="m-0 fw-semibold" data-bind="text: price"></p>
                  <div class="float-start text-warning fs-4 stars" data-bind="foreach: stars">
                    <span>★</span> <!-- Unicode character for a filled star -->
                  </div>
                </div>
              </div>
            </li>
          </ul>
        </div>
      </div>
      
      <!-- Column for dropping selected items -->
      <div class="col-12 col-md-6 h-100" title="Drop it">
        <div class="d-flex justify-content-between">
          <p class="fw-semibold">Drop Here</p>
          <p class="fw-semibold">
            <span>Total Products - </span>
            <span data-bind="text: selectedProducts().length"></span>
          </p>
        </div>
        
        <!-- Drop area for products -->
        <div id="droptarget" class="droptarget border h-100 w-100 rounded-3 p-2 list-group drop-area-element overflow-y-auto"
          ondrop="drop(event)" ondragover="allowDrop(event)">
        </div>
        
        <!-- Total price display -->
        <p class="fw-bold float-end pt-3">
          <span>Price - $</span>
          <span data-bind="text: totalSelectedProductPrice"></span>
        </p>
      </div>
    </div>
    
    <!-- Footer with additional info -->
    <hr />
    <div>
      <p class="fw-semibold" id="demo"></p>
    </div>
  </div>

 

Write needed CSS

Write needed CSS
.droptarget {
   width: 100%;
   height: 100%;
   min-height: 100px;
   height: 700px!important;
}
.drop-area-element{
   position: relative;
}
.drop-area-element .remove-product {
   display: block!important;
   cursor: pointer;
}
.add-product {
   cursor: pointer;
}
.drop-area-element .add-product{
   display: none;
}

 

JavaScript Logic

Now, let's implement the JavaScript logic to handle the drag-and-drop events and manage the data using Knockout.js.

/* Events fired on the drag target */
function dragStart(event) {
  // Store the ID of the dragged element
  event.dataTransfer.setData("Text", event.target.id);
  // Display a message indicating dragging has started
  document.getElementById("demo").innerHTML = "Started dragging";
}

function dragEnd(event) {
  // Display a message indicating dragging has ended
  document.getElementById("demo").innerHTML = "Finished dragging.";
}

/* Events fired on the drop target */
function allowDrop(event) {
  // Prevent the default behavior to allow dropping
  event.preventDefault();
}

function drop(event) {
  // Prevent the default behavior
  event.preventDefault();
  // Get the ID of the dragged element
  const data = event.dataTransfer.getData("Text");
  // Find the element by ID
  let ele = document.getElementById(data);
  if(ele.dataset && ele.dataset.productid){
    // Check if there is space available for the product
    if(model.isSpaceAvailableForProduct(ele.dataset.productid)){
      // Add the product to the selected products list
      model.addSelectedProduct(ele.dataset.productid);
      // Append the cloned element to the drop target
      event.target.appendChild(ele.cloneNode(true));
    } else {
      // Alert the user if the maximum limit is reached
      window.alert("Maximum 2 products can be added.");
    }
  }
}

function addProduct(event){
  // Get the parent element of the clicked add button
  let parent = event.parentNode.parentNode;
  
  if (parent.dataset && parent.dataset.productid) {
    // Check if there is space available for the product
    if(model.isSpaceAvailableForProduct(parent.dataset.productid)){
      // Add the product to the selected products list
      model.addSelectedProduct(parent.dataset.productid);
      // Append the cloned parent element to the drop target
      document.getElementById('droptarget').appendChild(parent.cloneNode(true));
    } else {
      // Alert the user if the maximum limit is reached
      window.alert("Maximum 2 products can be added.");
    }
  }
}

function removeProduct(event) {
  // Get the parent element of the clicked remove button
  let parent = event.parentNode.parentNode;
  
  if (parent.dataset && parent.dataset.productid) {
    // Remove the product from the selectedProducts array
    model.removeSelectedProduct(parent.dataset.productid);
    // Remove the parent element from the DOM
    parent.parentNode.removeChild(parent);
  }
}

// Class representing a Product
class Product {
  constructor(productId, name, img, price, stars) {
    this.id = productId;
    this.name = name;
    this.img = img;
    this.price = price;
    this.stars = ko.observableArray(this.generateStars(stars)); // Generate 5 stars

}

  generateStars(count) {
    // Generate an array of stars up to the given count
    return Array.from({ length: 5 }, (_, i) => i < count).filter(Boolean);
  }
}

// ViewModel class for Knockout.js
class ViewModel {
  constructor() {
    var self = this;
    self.name = ko.observable("aksdfhaksdhf");
    self.dragMessage = ko.observable("");
    self.selectedProductsIds = ko.observableArray([]);
    
    self.selectedProducts = ko.observableArray([]);
    
    // Function to get a product by ID
    self.getProductById = function(productId, dataSource) {
      return ko.utils.arrayFirst(dataSource(), function(product) {
        return product.id === parseInt(productId);
      });
    };    
    
    self.addSelectedProduct = function(productId){
      self.selectedProducts.push(self.getProductById(productId, self.products));
    };

    self.removeSelectedProduct = function(productId) {
      var index = self.selectedProducts.indexOf(self.getProductById(productId, self.selectedProducts));
      if (index !== -1) {
        self.selectedProducts.splice(index, 1);
      }
    };
    
    self.totalSelectedProductPrice = ko.computed(function() {
      var selectedProducts = self.selectedProducts(); // Assuming selectedProducts() returns an array of Product objects
      
      // Use reduce to sum up the prices of all selected products
      var totalPrice = selectedProducts.reduce(function(total, product) {
        // Parse the price and add it to the total
        return total + parseFloat(product.price.replace('$', ''));
      }, 0);
    
      // Return the total price rounded to two decimal places
      return totalPrice.toFixed(2);
    });
    
    self.countFrequencyOfNumber = function(arr, num) {
      // Use Array.prototype.reduce to count occurrences of num in arr
      return arr.reduce(function(count, element) {
        return count + (element.id === parseInt(num) ? 1 : 0);
      }, 0);
    };
    
    self.isSpaceAvailableForProduct = function(productId) {
      // Check if the count of productId is less than 2
      return self.countFrequencyOfNumber(self.selectedProducts(), productId) < 2;
    };
    
    // Create an array of Product objects
    self.products = ko.observableArray([
      new Product(1, 'Puma Shoe', 'https://rukminim2.flixcart.com/image/850/1000/l432ikw0/shoe/6/0/l/-original-imagf255et5szt5s.jpeg?q=90&crop=false', '$100.00', 5),
      new Product(2, 'Flight Sleeper', 'https://img3.junaroad.com/uiproducts/20423620/pri_175_p-1697706717.jpg', '$20.00', 4),
      new Product(3, 'Bucket', 'https://nutristar.co.in/cdn/shop/products/1_488b7df8-acf2-409c-8faa-21ae2bb08001_1024x1024.jpg?v=1579738972', '$30.00', 2),
      new Product(4, 'Socks', 'https://m.media-amazon.com/images/I/91mNKVcE0WL._AC_UY1100_.jpg', '$5.00', 3),
      new Product(5, 'Pen', 'https://5.imimg.com/data5/SELLER/Default/2020/10/PX/KF/AW/20193325/ink-pen.jpg', '$50.00', 4)
    ]);
  }
}

// Instantiate ViewModel and apply bindings
var model = new ViewModel();
ko.applyBindings(model);

 

Explanation

HTML Structure:

  • Drag Source: A list of products that can be dragged.
  • Drop Target: An area where the products can be dropped.

JavaScript Logic:

  • Drag-and-Drop Handlers: Functions to handle dragStart, dragEnd, allowDrop, and drop events.
  • Product Class: Represents a product with properties and a method to generate star ratings.
  • ViewModel Class: Manages the application's state, including the list of products, selected products, and various helper functions.

 

Conclusion

By integrating Knockout.js with drag-and-drop functionality, you can create a more interactive and engaging user interface. This guide has provided a foundational understanding of implementing these features, allowing you to enhance your web applications significantly. With further customization and optimization, the possibilities are endless. Happy coding!

 


 


Hi, my self Ravi Vishwakarma. I have completed my studies at SPICBB Varanasi. now I completed MCA with 76% form Veer Bahadur Singh Purvanchal University Jaunpur. SWE @ MindStick | Software Engineer | Web Developer | .Net Developer | Web Developer | Backend Engineer | .NET Core Developer

Leave Comment

Comments

Liked By