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:
some things i tried: On clicking the edit icon,
contentEditable=true
for text, so we can edit it(task text) in realtime.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/
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>