Reputation: 25
Ive been studying javascript and following along on this online tutorial for a to-do list. I switched up and added a few of my own features, but I am not sure how would go about adding a feature where I can edit a individual list item?
I started off creating a function editTodo(key), I know I would have to append my new text to the old list text? If someone could give me a hint or guide me in the right direction?
//array that holds todo list items
let listItems = [];
//Function will create a new list item based on whatever the input value
//was entered in the text input
function addItem (text) {
const todo = {
text, //whatever user types in
checked: false, //boolean which lets us know if a task been marked complete
id: Date.now(), //unique identifier for item
};
//it is then pushed onto the listItems array
listItems.push(todo);
renderTodo(todo);
}
function checkDone(key) {
//findIndex is an array method that returns position of an element in array
const index = listItems.findIndex(item => item.id === Number(key));
//locates the todo item in the listItems array and set its checked property
//to opposite. 'true' will become 'false'
listItems[index].checked = !listItems[index].checked;
renderTodo(listItems[index]);
}
function deleteTodo(key) {
//find todo object in the listItems array
const index = listItems.findIndex(item => item.id === Number(key));
//create a new object with properties of the current list item
//delete property set to true
const todo = {
deleted: true,
...listItems[index]
};
//remove the list item from the array by filtering it out
listItems = listItems.filter(item => item.id !== Number(key));
renderTodo(todo);
}
//edits list item
function editTodo(key) {
//find todo object in the listItems array
const index = listItems.findIndex(item => item.id === Number(key));
}
//selects form element
const form = document.querySelector('.js-form');
const addGoal = document.getElementById('addBtn');
//adds a submit event listener
function selectForm(event) {
//prevent page refresh on form submission
event.preventDefault();
//select the text input
const input = document.querySelector('.js-todo-input');
//gets value of the input and removes whitespace beginning/end of string
//we then save that to new variable -> text
const text = input.value.trim();
//checks whether 2 operands are not equal, returning true or false (boolean)
//if input value is not equal to blank, add user input
if (text !== '') {
addItem(text);
input.value = ''; //value of text input is cleared by setting it to empty
input.focus(); //focused so user can add many items to list witout focusing the input
}
};
addGoal.addEventListener('click', selectForm, false);
form.addEventListener('submit', selectForm, false);
function renderTodo(todo) {
//saves local storage items, convert listItems array to JSON string
localStorage.setItem('listItemsRef', JSON.stringify(listItems));
//selects the first element with a class of 'js-to'list'
const list = document.querySelector('.js-todo-list');
//selects current todo (refer to top) list item in DOM
const item = document.querySelector(`[data-key='${todo.id}']`);
//refer to function deleteTodo(key)
if (todo.deleted) {
//remove item from DOM
item.remove();
return
}
//use the ternary operator to check if 'todo.checked' is true
//if true, assigns 'done' to checkMarked. if not, assigns empty string
const checkMarked = todo.checked ? 'done' : '';
//creates list 'li' item and assigns it to 'goal'
const goal = document.createElement('li');
//sets the class attribute
goal.setAttribute('class', `todo-item ${checkMarked}`);
//sets the data-key attribute to the id of the todo
goal.setAttribute('data-key', todo.id);
//sets the contents of the list item 'li'
goal.innerHTML = `
<input id="${todo.id}" type="checkbox" />
<label for="${todo.id}" class="tick js-tick"></label>
<span>${todo.text}</span>
<button class="edit-todo js-edit-todo"><i class="fa-solid fa-pencil"></i></button>
<button class="delete-todo js-delete-todo">X</button>
`;
//if item already exists in the DOM
if (item) {
//replace it
list.replaceChild(goal, item);
}else {
//otherwise if it doesnt (new list items) add at the end of the list
list.append(goal);
}
}
//selects entire list
const list = document.querySelector('.js-todo-list');
//adds click event listener to the list and its children
list.addEventListener('click', event => {
if (event.target.classList.contains('js-tick')) {
const itemKey = event.target.parentElement.dataset.key;
checkDone(itemKey);
}
//add this 'if block
if (event.target.classList.contains('js-delete-todo')) {
const itemKey = event.target.parentElement.dataset.key;
deleteTodo(itemKey);
}
})
//render any existing listItem when page is loaded
document.addEventListener('DOMContentLoaded', () => {
const ref = localStorage.getItem('listItemsRef');
if (ref) {
listItems = JSON.parse(ref);
listItems.forEach(t => {
renderTodo(t);
});
}
});
@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
body {
font-family: 'Montserrat', sans-serif;
line-height: 1.4;
}
.container {
width: 100%;
max-width: 500px;
margin: 0 auto;
padding-left: 10px;
padding-right: 10px;
color: rgb(43, 43, 43);
height: 90vh;
margin-top: 20vh;
margin-bottom: 5vh;
overflow-y: auto;
}
.app-title {
text-align: center;
margin-bottom: 20px;
font-size: 80px;
opacity: 0.5;
}
.todo-list {
list-style: none;
margin-top: 20px;
}
.todo-item {
margin-bottom: 10px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.todo-item span {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
font-size: 22px;
}
.done span {
background-color:#0099e5;
color:#fff;
}
input[type="checkbox"] {
display: none;
}
#addBtn {
padding: 8px 16px;
font-size:16px;
font-weight:bold;
text-decoration: none;
background-color:#0099e5;
color:#fff;
border-radius: 3px;
border: 3px solid #333;
margin-left:10px;
cursor:pointer;
}
#addBtn:hover {
background-color:#0078b4;
}
.tick {
width: 30px;
height: 30px;
border: 3px solid #333;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.tick::before {
content: '✓';
font-size: 20px;
display: none;
}
.done .tick::before {
display: inline;
}
.delete-todo {
border: none;
font-size:16px;
background-color:red;
color:#fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius:20px;
}
.edit-todo {
border: none;
font-size:16px;
background-color:green;
color:#fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius:20px;
}
.empty-warning {
flex-direction:column;
align-items:center;
justify-content:center;
display:none;
}
.todo-list:empty {
display:none;
}
.todo-list:empty + .empty-warning {
display:flex;
}
.empty-warning-title {
margin-top:15px;
opacity: 0.8;
color: rgb(43, 43, 43);
}
form {
width: 100%;
display: flex;
justify-content: space-between;
margin-left:5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 3px solid #333;
}
<!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>To-Do List</title>
<link rel = "stylesheet" href = "style.css">
<script src="https://kit.fontawesome.com/67e5409c20.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1 class="app-title">To Do List</h1>
<form class="js-form">
<input autofocus type="text" aria-label="Enter a new todo item" placeholder="Ex - Walk the dog" class="js-todo-input">
<input type="button" id="addBtn" value="Add">
</form>
<ul class="todo-list js-todo-list"></ul>
<div class="empty-warning">
<h2 class="empty-warning-title">Add your first goal</h2>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Upvotes: 1
Views: 1131
Reputation: 44078
I added the edit feature to the event handler for click events on the list:
Figure I
if (event.target.matches('.edit-todo') && event.target !== event.currentTarget) {
const text = event.target.previousElementSibling;
text.toggleAttribute('contenteditable');
if (text.contenteditable) {
text.focus();
}
}
Basically, when the user clicks an edit button the contenteditable
attribute is toggled (true
/false
) on the <span>
that sits right before the button (hence .previousElementSibling
).
I also added 2 CSS rulesets as well:
Figure II
.fa-pencil { pointer-events: none }
[contenteditable] { outline: 3px inset blue }
For some reason my mouse cannot click font-awesome icons, I have no idea why. So I disabled click events on the edit icon in order to click the edit button. Others might have the same problem as I do -- I'm 99% sure there's no harm in keeping that ruleset since it just makes the edit button 100% the origin element on the event chain. The second ruleset is a visual cue to the user that the <span>
is editable.
let listItems = [];
function addItem(text) {
const todo = {
text,
checked: false,
id: Date.now(),
};
listItems.push(todo);
renderTodo(todo);
}
function checkDone(key) {
const index = listItems.findIndex(item => item.id === Number(key));
listItems[index].checked = !listItems[index].checked;
renderTodo(listItems[index]);
}
function deleteTodo(key) {
const index = listItems.findIndex(item => item.id === Number(key));
const todo = {
deleted: true,
...listItems[index]
};
listItems = listItems.filter(item => item.id !== Number(key));
renderTodo(todo);
}
function editTodo(key) {
const index = listItems.findIndex(item => item.id === Number(key));
}
const form = document.querySelector('.js-form');
const addGoal = document.getElementById('addBtn');
function selectForm(event) {
event.preventDefault();
const input = document.querySelector('.js-todo-input');
const text = input.value.trim();
if (text !== '') {
addItem(text);
input.value = '';
input.focus();
}
};
addGoal.addEventListener('click', selectForm, false);
form.addEventListener('submit', selectForm, false);
function renderTodo(todo) {
// localStorage.setItem('listItemsRef', JSON.stringify(listItems));
const list = document.querySelector('.js-todo-list');
const item = document.querySelector(`[data-key='${todo.id}']`);
if (todo.deleted) {
item.remove();
return
}
const checkMarked = todo.checked ? 'done' : '';
const goal = document.createElement('li');
goal.setAttribute('class', `todo-item ${checkMarked}`);
goal.setAttribute('data-key', todo.id);
goal.innerHTML = `
<input id="${todo.id}" type="checkbox" />
<label for="${todo.id}" class="tick js-tick"></label>
<span>${todo.text}</span>
<button class="edit-todo js-edit-todo"><i class="fa-solid fa-pencil"></i></button>
<button class="delete-todo js-delete-todo">X</button>
`;
if (item) {
list.replaceChild(goal, item);
} else {
list.append(goal);
}
}
const list = document.querySelector('.js-todo-list');
list.addEventListener('click', function(event) {
if (event.target.classList.contains('js-tick')) {
const itemKey = event.target.parentElement.dataset.key;
checkDone(itemKey);
}
if (event.target.classList.contains('js-delete-todo')) {
const itemKey = event.target.parentElement.dataset.key;
deleteTodo(itemKey);
}
if (event.target.matches('.edit-todo') && event.target !== event.currentTarget) {
const text = event.target.previousElementSibling;
text.toggleAttribute('contenteditable');
if (text.contenteditable) {
text.focus();
}
}
})
/*
document.addEventListener('DOMContentLoaded', () => {
const ref = localStorage.getItem('listItemsRef');
if (ref) {
listItems = JSON.parse(ref);
listItems.forEach(t => {
renderTodo(t);
});
}
});*/
@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
body {
font-family: 'Montserrat', sans-serif;
line-height: 1.4;
}
.container {
width: 100%;
max-width: 500px;
margin: 0 auto;
padding-left: 10px;
padding-right: 10px;
color: rgb(43, 43, 43);
height: 90vh;
margin-top: 20vh;
margin-bottom: 5vh;
overflow-y: auto;
}
.app-title {
text-align: center;
margin-bottom: 20px;
font-size: 80px;
opacity: 0.5;
}
.todo-list {
list-style: none;
margin-top: 20px;
}
.todo-item {
margin-bottom: 10px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.todo-item span {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
font-size: 22px;
}
.done span {
background-color: #0099e5;
color: #fff;
}
input[type="checkbox"] {
display: none;
}
#addBtn {
padding: 8px 16px;
font-size: 16px;
font-weight: bold;
text-decoration: none;
background-color: #0099e5;
color: #fff;
border-radius: 3px;
border: 3px solid #333;
margin-left: 10px;
cursor: pointer;
}
#addBtn:hover {
background-color: #0078b4;
}
.tick {
width: 30px;
height: 30px;
border: 3px solid #333;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.tick::before {
content: '✓';
font-size: 20px;
display: none;
}
.done .tick::before {
display: inline;
}
.delete-todo {
border: none;
font-size: 16px;
background-color: red;
color: #fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 20px;
}
.edit-todo {
border: none;
font-size: 16px;
background-color: green;
color: #fff;
outline: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 20px;
}
.empty-warning {
flex-direction: column;
align-items: center;
justify-content: center;
display: none;
}
.todo-list:empty {
display: none;
}
.todo-list:empty+.empty-warning {
display: flex;
}
.empty-warning-title {
margin-top: 15px;
opacity: 0.8;
color: rgb(43, 43, 43);
}
form {
width: 100%;
display: flex;
justify-content: space-between;
margin-left: 5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 3px solid #333;
}
.fa-pencil {
pointer-events: none
}
[contenteditable] {
outline: 3px inset blue
}
<!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>To-Do List</title>
<link rel="stylesheet" href="style.css">
<script src="https://kit.fontawesome.com/67e5409c20.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1 class="app-title">To Do List</h1>
<form class="js-form">
<input autofocus type="text" aria-label="Enter a new todo item" placeholder="Ex - Walk the dog" class="js-todo-input">
<input type="button" id="addBtn" value="Add">
</form>
<ul class="todo-list js-todo-list"></ul>
<div class="empty-warning">
<h2 class="empty-warning-title">Add your first goal</h2>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Upvotes: 1