Search code examples
javascripthtmlcssdrag-and-dropsortablejs

How do I use drag and drop with dynamically created HTML? (SortableJS)


I'm starting to learn javascript and I have a simple todo application where I want to be able to drag and drop the different todo's created. A simple way to do this was with SortableJS library, but it doesn't work the way i want to. After implementing the simple sortable function, when dragging on a todo, it grabs the whole todo-list instead of a single todo

todo list

I think the issue is because I dynamically create the html, but I'm kinda stuck and would appreciate any suggestions.

//Selectors
const todoInput = document.querySelector(".todos-input"); //input for adding a todo
const todoButton = document.querySelector(".todos-button"); //add todo-button
const todoList = document.querySelector(".todos-list"); //the todo-list

//Event listeners
todoButton.addEventListener("click", addTodo);
todoList.addEventListener("click", deleteTodo);
todoList.addEventListener("click", completeTodo);


//Functions
function addTodo(event) {
  //prevent form from submitting
  event.preventDefault();
  //create a div for the todos-list
  const todoDiv = document.createElement("div");
  //add classlist for styling
  todoDiv.classList.add("todo");
  //Create LI
  const newTodo = document.createElement("li");
  //output the value from the add-todo field
  if (todoInput.value != "") {
    newTodo.innerText = todoInput.value;
  } else {
    return false;
  }

  //classlist for styling
  newTodo.classList.add("todo-item");
  //append child to div

  todoDiv.appendChild(newTodo);
  //complete button
  const completedButton = document.createElement("button");
  completedButton.innerHTML = '<i class="fas fa-check"><i/>';
  completedButton.classList.add("completed-btn");
  todoDiv.appendChild(completedButton);
  //delete button
  const deletedButton = document.createElement("button");
  deletedButton.innerHTML = '<i class="fas fa-trash"><i/>';
  deletedButton.classList.add("deleted-btn");
  todoDiv.appendChild(deletedButton);

  //drag button
  const dragButton = document.createElement("button");
  dragButton.innerHTML = '<i class="icon fa fa-bars"><i/>';
  dragButton.classList.add("drag-btn");
  dragButton.classList.add("handle");
  todoDiv.appendChild(dragButton);

  //append div to list of todos
  todoList.appendChild(todoDiv);

  //clear input field after adding a new todo
  todoInput.value = "";

  //DRAG AND DROP
  const dragArea = document.querySelector('.todos-section');
  new Sortable(dragArea, {
    animation: 300
  });

}



//deleting todo
function deleteTodo(e) {
  //grab the item, whatever we are clicking on
  const item = e.target;
  //delete todo
  if (item.classList[0] === "deleted-btn") {
    //grab the parent element of the item, which is the todolist element in this case
    const todo = item.parentElement;
    //remove the todo
    todo.remove();
  }
}

//completing todo
function completeTodo(e) {
  //grab the item, whatever we are clicking on
  const item = e.target;
  //complete todo
  if (item.classList[0] === "completed-btn") {
    const todo = item.parentElement;
    //use the toggle because if the element has a class, then the classList.toggle method 
    //behaves like classList.remove and the class is removed from the element.
    //And if the element does not have the specified class
    //then classList.toggle, just like classList.add, adds this class to the element.
    //So it basically does the add/remove operation for us depending on the state.
    todo.classList.toggle("completed-todo");
  }
}
/*Apply to all elements*/

* {
  box-sizing: border-box;
  list-style-type: none;
  margin: 0;
  padding: 0;
}

body {
  font-family: "Merriweather Sans", sans-serif;
  background: rgba(216, 206, 206, 0.787);
}

.wrapper {
  display: flex;
  position: relative;
}


/* Add todos-section */

.todos-bar {
  position: fixed;
  top: 5%;
  left: 50%;
  font-size: 17px;
  border: 0;
  transform: translate(-50%, -50%);
  padding-left: 100px;
}

.todos-bar input {
  width: 600px;
  height: 50px;
  border: 0px;
  outline: none;
  font-size: 20px;
  padding-left: 20px;
  border-radius: 5px;
}

.todos-bar button {
  position: fixed;
  background: rgba(20, 33, 93, 0.952);
  color: white;
  font-size: 20px;
  border: 0;
  outline: none;
  height: 50px;
  padding: 10px 20px;
  right: 0px;
  border-radius: 0px 5px 5px 0px;
  cursor: pointer;
}

.todos-bar button:hover {
  background: rgb(43, 54, 73);
}


/* Todos section */

.todos-section {
  display: flex;
  position: fixed;
  top: 15%;
  left: 37%;
}

.todos-list {
  width: 600px;
}

.todo {
  margin: 1.5rem;
  background: white;
  color: black;
  font-size: 1.5rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-radius: 5px;
  padding-left: 0.5rem;
  margin: 15px;
  transition: all 0.5s ease;
}

.todo li {
  flex: 1;
}

.todo-item {
  padding: 0rem 0.5rem;
  padding-left: 2.5rem;
}

.deleted-btn,
.completed-btn {
  background: rgb(248, 56, 56);
  color: white;
  border: none;
  padding: 1rem;
  cursor: pointer;
  font-size: 1rem;
}

.completed-btn {
  background: green;
}

.deleted-btn {
  border-radius: 0px 5px 5px 0px;
}

.drag-btn {
  display: block;
  position: absolute;
  background: white;
  border: 2px solid white;
}

.fa-bars {
  padding: 5px;
  margin: 2px;
  cursor: pointer;
}

.fa-trash,
.fa-check {
  pointer-events: none;
}

.completed-todo {
  text-decoration: line-through;
  opacity: 0.5;
}
<head>
  <link rel="stylesheet" href="style.css">
  <script src="https://kit.fontawesome.com/47440aba67.js" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>

</head>

<body>
  <div class="wrapper">
    <!--ADD TODO-->
    <div class="todos-bar">
      <input type="text" class="todos-input" placeholder="Add to list...">
      <button class="todos-button" type="submit"><i class="fas fa-plus"></i></button>
    </div>
    <!--TODO LIST-->
    <div class="todos-section">
      <ul class="todos-list"></ul>
    </div>
  </div>
  <script src="script.js"></script>
</body>

</html>


Solution

  • According to the documentation,

    You can use any element for the list and its elements, not just ul/li

    What you've implemented actually fits in this description since there is a ul with div tags inside. However, you are not referencing the right element in the dragArea, because it should be the direct parent (.todos-list) of your desired draggable children.

    So, change it to .todos-list and in addition pass handle property to Sortable constructor to reference the icon in which you want to drag.

    const dragArea = document.querySelector('.todos-list');
    new Sortable(dragArea, {
        animation: 300,
        handle: '.fa-bars'
    })
    

    Working example