zshutoski
zshutoski

Reputation: 25

How to add a edit to-do list item feature?

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

Answers (1)

zer00ne
zer00ne

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

Related Questions