Search code examples
javascriptif-statementdomdom-eventsevent-propagation

Javascript: Event properties get lost on replacing an element


I am trying to create a todoList app with vanilla js. So far i have finished markup, design and as well as added other functionalities such as adding submitted task to ui, deleting task, marking the task as completed.

Now i am stuck at adding the edit functionality.(And of course there are still other things to do like validation , implementing localStorage, adding reminder, making it responsive etc..)

Things to achieve:

  1. user should be able to click the edit icon and it should change to save icon,
  2. After edit icon is clicked, users should be able to edit the text in realtime
  3. And finally after clicking the save icon, the text should no longer be editable and the icon should change back to edit icon

some things i tried: On clicking the edit icon,

  1. changed contentEditable=true for text, so we can edit it(task text) in realtime.
  2. replaced editBtn with saveBtn(newly created element)

But after replacing the element i couldn't find a way to revert it. When i tried to access it with the eventTarget(a variable i used to store target property of an event) i didn't get anything. I also tried grabbing it with document.querySelector('.save') but that does only works as per document flow.(i meant if we clicked the 2nd button, in dom the first button gets changed)

Now i would like to have the functionality to replace the saveBtn back to the editBtn and change contentEditable back to false or inherit

Here is function which handles the ui events :

static taskEvents(eventTarget) {
        const targetClassName = eventTarget.classList 
        
        if(targetClassName.contains('complete')) {
            targetClassName.toggle('statusIcon');
            eventTarget.parentElement.nextElementSibling.classList.toggle('task');
        } 
        
        else if(targetClassName.contains('edit')) {
            // let textEditableStatus = eventTarget.parentElement.parentElement.previousElementSibling;
            // textEditableStatus.contentEditable=true;

            // const editBtn = eventTarget.parentElement

            // const saveBtn = document.createElement('a');
            // saveBtn.className = "btn";
            // saveBtn.id = "saveBtn";
            // saveBtn.innerHTML = '<i class="fas fa-save save"></i>';

            // editBtn.replaceWith(saveBtn)

        } 
        
        else if(targetClassName.contains('delete')) {
            eventTarget.parentElement.parentElement.parentElement.remove();
        }
    }

complete code:

class UI {
  // dummy data; for now.
  static displayTasks() {
    const tasks = ['Take out trash', 'Do laundry', 'Visit part'];

    tasks.forEach(task => UI.addTask(task));
  }

  static addTask(task) {
    const tbody = document.querySelector('#tasks');

    const taskRow = document.createElement('tr');
    taskRow.className += 'task';
    taskRow.innerHTML = `
            <td><i class="far fa-check-circle complete"></i></td>
            <td>${task}</td>
            <td><a href="#" class="btn" id="editBtn"><i class="fas fa-edit edit"></i></a></td>
            <td><a href="#" class="btn" id="deleteBtn"><i class="fas fa-trash delete"></i></a></td>
        `;
    tbody.appendChild(taskRow);
    document.querySelector('#todoInput').value = '';
  }

  static taskEvents(eventTarget) {
    const targetClassName = eventTarget.classList

    if (targetClassName.contains('complete')) {
      targetClassName.toggle('statusIcon');
      eventTarget.parentElement.nextElementSibling.classList.toggle('task');
    } else if (targetClassName.contains('edit')) {
      // let textEditableStatus = eventTarget.parentElement.parentElement.previousElementSibling;
      // textEditableStatus.contentEditable=true;

      // const editBtn = eventTarget.parentElement

      // const saveBtn = document.createElement('a');
      // saveBtn.className = "btn";
      // saveBtn.id = "saveBtn";
      // saveBtn.innerHTML = '<i class="fas fa-save save"></i>';

      // editBtn.replaceWith(saveBtn)

    } else if (targetClassName.contains('delete')) {
      eventTarget.parentElement.parentElement.parentElement.remove();
    }
  }
}

// Ui events 
document.addEventListener('DOMContentLoaded', UI.displayTasks);

const tbody = document.querySelector('#tasks');
tbody.addEventListener('click', event => {
  UI.taskEvents(event.target);
})
.statusIcon {
  font-weight: bold;
  color: rgb(48, 158, 81);
}

td.task {
  opacity: .6;
  text-decoration: line-through;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Todo List App</title>
  <!-- Custom css -->
  <link rel="stylesheet" href="style.css">
  <!-- fontawesome script-->
  <script src="https://kit.fontawesome.com/39350fd9df.js"></script>
</head>

<body>

  <div class="main-container">

    <div class="container">
      <div class="input-group">
        <input type="text" id="todoInput" placeholder="Enter new task...">
        <a href="#" class="btn addBtn"><i class="fas fa-plus"></i></a>
      </div>
      <table class="taskLister">
        <thead>
          <tr>
            <th>Status</th>
            <th>Task</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody id="tasks"></tbody>
      </table>
    </div>
  </div>

  <!-- Custom script -->
  <script src="app.js"></script>
</body>

</html>

A quick note: I am using pure javascript

Here is the fiddle: https://jsfiddle.net/Pixie_Dust/Lcnwu4ao/5/


Solution

  • Using innerHTML to rewrite element content will destroy the handlers defined on it. If you use event delegation, this problem will not occur, because the delegated handler uses the actual elements to determine which action is needed. Here's a minimal reproducable example for a table with one entry.

    Here's a rewritten version of your jsFiddle, and here's a (somewhat extended) stackblitz version of it.

    document.addEventListener("click", handle);
    
    function handle(evt) {
      const origin = evt.target;
      if (origin.dataset.edit) {
        const entryEdit = origin.closest("tr").querySelector("td:nth-child(2)");
        entryEdit.contentEditable = true;
        return entryEdit.focus();
      }
    
      if (origin.dataset.save) {
        const entry = origin.closest("tr");
        const value = entry.querySelector("td:nth-child(2)")
        if (value.contentEditable === "true") {
          value.contentEditable = false;
          return entry.querySelector("td:nth-child(5)").textContent = "saved!";
        };
    
        return entry.querySelector("td:nth-child(5)").textContent = "nothing to do";
      }
      
      if (origin.dataset.setstatus) {
        const row = origin.closest("tr");
        const nwStatus = origin.dataset.setstatus === "Todo" ? "Done" : "Todo";
        row.dataset.status = nwStatus;
        origin.dataset.setstatus = nwStatus;
        row.querySelectorAll("td > button")
          .forEach(btn => 
            btn[nwStatus === "Done" 
              ? 'setAttribute' 
              : 'removeAttribute']("disabled", true));
        return row.querySelector("td:nth-child(5)").textContent = "";
      }
    }
    body {
      margin: 2rem;
      font: 12px/15px normal verdana, arial;
    }
    th {
      background: black;
      color: white;
      text-align: left;
      padding: 2px;
    }
    td {
      padding: 2px;
    }
    th:nth-child(5) {
      min-width: 75px;
    }
    
    th:nth-child(2) {
      min-width: 200px;
    }
    
    td[data-setstatus]:after {
      content: attr(data-setstatus);
    }
    tr[data-status='Done'] td:nth-child(2) {
      text-decoration: line-through;
    }
    <table>
      <thead>
        <tr>
          <th>Status</th>
          <th>Task</th>
          <th>edit</th>
          <th>save</th>
          <th>result</th>
        </tr>
      </thead>
      <tbody>
        <tr data-status="Todo">
          <td data-setstatus="Todo"></td>
          <td>Hi, I am a task</td>
          <td><button data-edit="1">edit</button></td>
          <td><button data-save="1">save</button></td>
          <td></td>
        </tr>
      </tbody>
    </table>