Привет, ребята, добро пожаловать в этот новый учебник. В этом руководстве вы узнаете, как создать простое приложение Todo List с использованием HTML, CSS и Javascript.

Этот урок был первоначально опубликован в моем блоге, если вам интересно увидеть больше подобных руководств, перейдите по этой ссылке — https://thecodingpie.com/

Прежде чем приступить к делу, взгляните на готовый продукт здесь, в Codepen → Todo List Codepen.

Вы можете скачать готовый проект с GitHub → Todo List App Github

Предпосылки

Я предполагаю, что вы хорошо разбираетесь в HTML, CSS и Javascript. Я не буду объяснять CSS, вместо этого вы можете просто скопировать код для файла CSS. Мы собираемся сосредоточиться на HTML и, в основном, на Javascript. Вы также должны иметь представление о цикле forEach, функции фильтра, == и ===, Условном (тернарном) операторе, функциях, шаблонных литералах и Объекте Javascript.

Но в любом случае не стесняйтесь следовать, я постараюсь сделать этот урок простым.

Я собираюсь использовать код Visual Studio в качестве текстового редактора. Вы можете использовать любой редактор кода, который вам нравится. И, наконец, убедитесь, что вы установили Live Server от Ritwick Dey в Visual Studio Code. Если у вас не установлен Live Server, то:

  • Открытый код визуальной студии
  • Перейти на вкладку «Расширения» (Ctrl + Shift + X)
  • Поиск «Live Server» от Ritwick Dey
  • Затем установите его

Но подождите, как мы собираемся построить эту штуку?

Разрушая логику

Логика довольно проста. Наш список задач может делать следующее:

  • Добавьте новый элемент списка дел.
  • Зачеркните элемент списка дел, если он выполнен.
  • Удалить элемент списка дел.
  • Затем всегда сохраняйте элементы списка дел в LocalStorage.

Это то, что мы все хотим сделать, и поверьте мне, это довольно просто 😀. Теперь давайте начнем…

Начальные настройки

Создайте структуру папок, как показано ниже:

  • Сначала создайте корневую папку, в которой хранится все, и назовите ее «Todo List».
  • Затем откройте эту папку в коде Visual Studio.
  • Затем непосредственно внутри этой корневой папки создайте файл с именем «index.html» → Он будет содержать наш HTML.
  • Затем снова прямо внутри нашей корневой папки создайте две папки:
  • Один с именем «css» → Внутри этой папки создайте файл с именем «styles.css».
  • Затем еще одна папка с именем «js» → Внутри этой папки создайте файл с именем «script.js».

Вот и все. Теперь у вас должна быть структура папок, аналогичная приведенной выше. Теперь часть HTML…

HTML

Откройте «index.html» и введите следующее:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" type="text/css" href="css/styles.css">
  <title>Todo List</title>
</head>
<body>
<div class="container">
    <h1>Todo</h1>
<form class="todo-form">
      <input type="text" class="todo-input" placeholder="Add a Todo...">
      <button type="submit" class="add-button">Add</button>
    </form>
<ul class="todo-items">
      <!-- dummy item -->
      <li class="item" data-key="1594003133171">
        <input class="checkbox" type="checkbox">
        Go to Gym
        <button class="delete-button">X</button>
      </li>
    </ul>
  </div>
  
  <script type="text/javascript" src="js/script.js"></script>
</body>
</html>

Наш HTML довольно прост.

  • Внутри нашего <body></body> у нас есть .container. Внутри этого у нас есть <h1>, который говорит «Todo».
  • Затем ниже у нас есть <form> с class=”todo-form”.
  • Внутри <form> у нас есть <input> коробка type=”text” с class=”todo-input”.
  • Затем у нас есть <button> из type=”submit” и class=”add-button”.
  • Затем под <form> у нас есть <ul> с class=”todo-items”. Здесь будут храниться все наши задачи.
  • Внутри .todo-items у нас пока есть манекен .item. Каждое задание .item должно выглядеть следующим образом:

Каждый .item имеет атрибут data-key, который служит идентификатором, чтобы отличить его. Значение data-key — это объект Date.

  • Затем внутри каждого .item у нас есть блок <input> из type=”checkbox” и class=”checkbox”.
  • Затем фактический текст todo.
  • Наконец .delete-button.

Теперь откройте его с Live Server, щелкнув правой кнопкой мыши файл «index.html» (внутри кода Visual Studio), затем прокрутите вниз и нажмите «Открыть с Live Server» и посмотрите на него в браузере. Теперь вы не можете видеть никаких стилей. Потому что мы только связали файл «styles.css», но не написали никаких стилей CSS. Итак, теперь давайте сделаем это. Давайте добавим несколько стилей…

CSS

Откройте созданный вами файл «styles.css» и скопируйте следующее:

/* common styles */
* {
  padding: 0;
  margin: 0;
}
body {
  width: 100vw;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  background: linear-gradient(#F00000, #DC281E);
  font-family: sans-serif;
}
button:hover {
  cursor: pointer;
  background-color: #73E831;
}
ul {
  list-style-type: none; /* get rid of bullet points on side of list items */
}
/* common style ends */
/* container */
.container {
  min-width: 700px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}
h1 {
  color: #fff;
  font-size: 3rem;
}
/* todo-form */
.todo-form {
  margin: 40px 0px;
}
.todo-input {
  width: 250px;
  border: none;
  outline: none;
  border-radius: 5px;
  padding: 10px;
  margin-right: 10px;
  font-size: 1rem;
}
.add-button {
  background-color: #0000ff;
  color: #fff;
  border: none;
  outline: none;
  border-radius: 5px;
  padding: 7px;
  font-size: 1.2rem;
}
/* todo-form style ends */
/* todo-items */
.todo-items {
  min-width: 350px;
}
/* each li with class="item" */
.item {
  background-color: #fff;
  padding: 10px;
  font-size: 1.1rem;
}
.item:first-child {
  border-top-left-radius: 7px;
  border-top-right-radius: 7px;
}
.item:last-child {
  border-bottom-left-radius: 7px;
  border-bottom-right-radius: 7px;
}
/* item style end */
.checkbox {
  margin-right: 10px;
}
.delete-button {
  float: right;
  background-color: #dc143c;
  border: none;
  outline: none;
  border-radius: 7px;
  padding: 2px 5px;
  margin-left: 10px;
  font-size: 1.1rem;
  font-weight: 550;
}
/* applied when the todo item is checked */
.checked { 
  text-decoration: line-through;
}
/* todo-items style ends */
/* container style ends */

CSS здесь не является нашим основным приоритетом. Поэтому я не буду это объяснять.

Теперь, если вы посмотрите в браузере, вы должны увидеть страницу с примененными стилями.

Подождите, но наше приложение списка дел не работает, верно? Это то, что мы собираемся делать дальше.

Javascript

С этого момента я буду делать все по крупицам, чтобы вы могли лучше понять, что мы делаем…

У нас есть несколько дел. Поэтому я собираюсь обернуть каждую функциональность в отдельный function. В итоге у вас будут следующие функции:

  • addTodo(item)
  • renderTodos(todos)
  • addToLocalStorage(todos)
  • getFromLocalStorage()
  • toggle(id)
  • deleteTodo(id)

Теперь давайте начнем кодировать все, что нам нужно, чтобы наш список задач работал.

Первое, что нам нужно, это получить ссылку на все, что нам нужно, из DOM.

Откройте файл «script.js» и введите следующее:

// select everything
// select the todo-form
const todoForm = document.querySelector('.todo-form');
// select the input box
const todoInput = document.querySelector('.todo-input');
// select the <ul> with class="todo-items"
const todoItemsList = document.querySelector('.todo-items');

Это возьмет .todo-form, .todo-input, .todo-items из DOM и сохранит их в соответствующих константах.

Теперь ниже сделайте такой массив:

// array which stores every todos
let todos = [];

Этот массив будет содержать все наши задачи. Каждый элемент будет объектом Javascript, как показано ниже:

Он имеет статус id, name и completed.

Следующее, что нам нужно сделать, это всякий раз, когда пользователь вводит новую задачу в поле <input>, нам нужно взять это значение и создать объект, подобный приведенному выше, и поместить его в массив todos.

Для этого под массивом todos введите следующее:

// add an eventListener on form, and listen for submit event
todoForm.addEventListener('submit', function(event) {
  // prevent the page from reloading when submitting the form
  event.preventDefault();
  addTodo(todoInput.value); // call addTodo function with input box current value
});

Приведенный выше код будет прослушивать событие submit на сервере form. Всякий раз, когда это происходит, javascript по умолчанию перезагружает страницу. Поэтому, чтобы этого не произошло, мы вызываем event.preventDefault().

Затем передайте введенное пользователем значение функции addTodo(). Мы можем получить значение, введенное пользователем, из todoInput.value. Помните, что todoInput — это настоящая коробка input.

функция addTodo()

Теперь создайте функцию addTodo() под приведенным выше кодом:

// function to add todo
function addTodo(item) {
  // if item is not empty
  if (item !== '') {
    // make a todo object, which has id, name, and completed properties
    const todo = {
      id: Date.now(),
      name: item,
      completed: false
    };
// then add it to todos array
    todos.push(todo);
    renderTodos(todos); // then renders them between <ul>
// finally clear the input box value
    todoInput.value = '';
  }
}

Эта функция addTodo() примет элемент в качестве первого аргумента. Потом:

  • Он проверит, является ли элемент пустым или нет. Если он не пустой, то:
  • создайте новый объект todo, как мы обсуждали ранее. Этот объект должен иметь свойства id, name и completed.
  • Затем он помещает этот объект в наш массив todos.
  • Затем вызывает функцию renderTodos(). Эта функция отвечает за отображение каждого элемента на экране.
  • Наконец, он очистит поле input.

визуализироватьTodos()

Теперь давайте создадим функцию renderTodos() под функцией addTodo():

// function to render given todos to screen
function renderTodos(todos) {
  // clear everything inside <ul> with class=todo-items
  todoItemsList.innerHTML = '';
// run through each item inside todos
  todos.forEach(function(item) {
    // check if the item is completed
    const checked = item.completed ? 'checked': null;
// make a <li> element and fill it
    // <li> </li>
    const li = document.createElement('li');
    // <li class="item"> </li>
    li.setAttribute('class', 'item');
    // <li class="item" data-key="20200708"> </li>
    li.setAttribute('data-key', item.id);
    /* <li class="item" data-key="20200708"> 
          <input type="checkbox" class="checkbox">
          Go to Gym
          <button class="delete-button">X</button>
        </li> */
    // if item is completed, then add a class to <li> called 'checked', which will add line-through style
    if (item.completed === true) {
      li.classList.add('checked');
    }
li.innerHTML = `
      <input type="checkbox" class="checkbox" ${checked}>
      ${item.name}
      <button class="delete-button">X</button>
    `;
    // finally add the <li> to the <ul>
    todoItemsList.append(li);
  });
}

Теперь объяснение:

  • Эта функция, прежде всего, очищает все внутри .todo-items — это наш <ul>, в котором хранятся все наши задачи.
  • Затем он будет перебирать каждый объект item внутри массива todos, который мы получаем в качестве аргумента. И для каждого item будет создан <li>, как показано ниже:

Но значения data-key и фактический текст будут заменены соответствующим значением item.

  • Наконец, мы добавляем <li> (мы создали) к .todo-items <ul>.
  • Две другие вещи, которые мы делаем в приведенном выше коде:
  • checked константа, которая содержит значение ‘checked’ или null. Это значение будет зависеть от завершенного состояния каждого элемента. Это значение будет указано внутри файла <input class=”checkbox” type=”checkbox”>. Если мы дадим ‘checked’, как показано ниже:

Тогда галочка будет отображаться внутри .checkbox. Если ничего или null не указано, то .checkbox будет снято.

Затем где-то в середине функции мы делаем это:

Эта строка отвечает за добавление класса .checked в the<li>. Этот класс добавляет зачеркивание над фактическим текстом задачи. Это снова зависит от статуса completed каждого item.

На этом этапе вы можете удалить/закомментировать фиктивный элемент, который мы сначала ввели вручную в файле «index.html». А потом взгляните на браузер. Попробуйте добавить некоторые пункты списка дел.

Теперь мы можем добавлять элементы в наше приложение списка дел. Но когда мы обновим страницу, все элементы исчезнут.

Поэтому нам нужно сохранить их, сохранив в локальном хранилище браузера.

addToLocalStorage() и getFromLocalStorage()

Если вы не знаете о локальном хранилище, то прочитайте это → https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

Вы можете просмотреть локальное хранилище вашего браузера, как показано ниже:

Давайте добавим возможности локального хранилища. Введите следующий код под приведенным выше кодом:

// function to add todos to local storage
function addToLocalStorage(todos) {
  // conver the array to string then store it.
  localStorage.setItem('todos', JSON.stringify(todos));
  // render them to screen
  renderTodos(todos);
}

Мы можем хранить предметы наlocalStorage, используя setItem(). Но Нам нужен ключ и значение.

Мы собираемся назвать наш ключ как ‘todos’, а значением будет сам наш массив todos. Но мы не можем хранить массив внутри localStorage 😔. Мы должны преобразовать его в строку. Это то, что мы делаем, используя JSON.stringify().

Наконец, мы вызываем renderTodos(). Всякий раз, когда мы добавляем что-то к localStorage, мы должны отображать эти изменения на экране. Вот почему мы добавляем функцию renderTodos() в конце.

Теперь замените renderTodos() внутри функции addTodo() на addToLocalStorage(), как показано ниже:

Потому что нам нужно обновлять наш localStorage всякий раз, когда пользователь вставляет новые элементы списка дел.

Теперь, если вы посмотрите на localStorage, вы увидите, что предметы хранятся там. Это круто 😀.

Но тем не менее, они исчезают с экрана, когда мы обновляем экран.

Чтобы решить эту проблему, создайте функцию с именем getFromLocalStorage() под предыдущим кодом, а затем вызовите ее.

// function helps to get everything from local storage
function getFromLocalStorage() {
  const reference = localStorage.getItem('todos');
  // if reference exists
  if (reference) {
    // converts back to array and store it in todos array
    todos = JSON.parse(reference);
    renderTodos(todos);
  }
}
// initially get everything from localStorage
getFromLocalStorage();

Эта функция поможет нам анализировать все элементы из localStorage всякий раз, когда мы загружаем нашу веб-страницу. Используемый здесь JSON.parse() предназначен для преобразования строкового массива обратно в реальный массив. Остальное говорит само за себя.

Ура, мы почти закончили! Две другие функции, которые необходимо добавить, — это функции проверки и удаления.

переключить() и удалитьTodo()

Введите следующее ниже getFromLocalStorage()

// after that addEventListener <ul> with class=todoItems. Because we need to listen for click event in all delete-button and checkbox
todoItemsList.addEventListener('click', function(event) {
  // check if the event is on checkbox
  if (event.target.type === 'checkbox') {
    // toggle the state
    toggle(event.target.parentElement.getAttribute('data-key'));
  }
// check if that is a delete-button
  if (event.target.classList.contains('delete-button')) {
    // get id from data-key attribute's value of parent <li> where the delete-button is present
    deleteTodo(event.target.parentElement.getAttribute('data-key'));
  }
});

Убедитесь, что вы добавили приведенный выше код под вызовом функции getFromLocalStorage().

Здесь мы слушаем событие «click» на всем самом <ul>. Итак, нам нужно выяснить, где пользователь щелкнул, будь то .checkbox, весь <li> или .delete-button. Это то, что мы делаем внутри функции.

  • Если пользователь нажал на .checkbox, то вызовите функцию toggle(), передав id.
  • Если пользователь нажал на .delete-button, то вызовите функцию deleteTodo(), передав id.

Мы можем получить id из свойства data-key файла <li>. Это то, что мы делаем внутри каждого вызова функции. (Помните, event.target — это сам элемент, в котором произошло событие).

Теперь давайте создадим функцию toggle() и функциюdeleteTodo(). Введите следующий код над вызовом функции getFromLocalStorage().

// toggle the value to completed and not completed
function toggle(id) {
  todos.forEach(function(item) {
    // use == not ===, because here types are different. One is number and other is string
    if (item.id == id) {
      // toggle the value
      item.completed = !item.completed;
    }
  });
addToLocalStorage(todos);
}
// deletes a todo from todos array, then updates localstorage and renders updated list to screen
function deleteTodo(id) {
  // filters out the <li> with the id and updates the todos array
  todos = todos.filter(function(item) {
    // use != not !==, because here types are different. One is number and other is string
    return item.id != id;
  });
// update the localStorage
  addToLocalStorage(todos);
}

Для этого я не буду объяснять. Скорее я призываю вас попробовать расшифровать его самостоятельно 😉. Попробуйте расшифровать их по крупицам. Если вы застряли, просто погуглите. Вот как вы изучаете вещи.

Вы также можете оставить комментарий. Я всегда здесь, чтобы помочь вам.

Поздравляю, вы дошли до конца 🍻.

Вот окончательный код для файла «script.js». Убедитесь, что код, который вы ввели до этого момента внутри «script.js», точно такой же, как следующий:

// select everything
// select the todo-form
const todoForm = document.querySelector('.todo-form');
// select the input box
const todoInput = document.querySelector('.todo-input');
// select the <ul> with class="todo-items"
const todoItemsList = document.querySelector('.todo-items');
// array which stores every todos
let todos = [];
// add an eventListener on form, and listen for submit event
todoForm.addEventListener('submit', function(event) {
  // prevent the page from reloading when submitting the form
  event.preventDefault();
  addTodo(todoInput.value); // call addTodo function with input box current value
});
// function to add todo
function addTodo(item) {
  // if item is not empty
  if (item !== '') {
    // make a todo object, which has id, name, and completed properties
    const todo = {
      id: Date.now(),
      name: item,
      completed: false
    };
// then add it to todos array
    todos.push(todo);
    addToLocalStorage(todos); // then store it in localStorage
// finally clear the input box value
    todoInput.value = '';
  }
}
// function to render given todos to screen
function renderTodos(todos) {
  // clear everything inside <ul> with class=todo-items
  todoItemsList.innerHTML = '';
// run through each item inside todos
  todos.forEach(function(item) {
    // check if the item is completed
    const checked = item.completed ? 'checked': null;
// make a <li> element and fill it
    // <li> </li>
    const li = document.createElement('li');
    // <li class="item"> </li>
    li.setAttribute('class', 'item');
    // <li class="item" data-key="20200708"> </li>
    li.setAttribute('data-key', item.id);
    /* <li class="item" data-key="20200708"> 
          <input type="checkbox" class="checkbox">
          Go to Gym
          <button class="delete-button">X</button>
        </li> */
    // if item is completed, then add a class to <li> called 'checked', which will add line-through style
    if (item.completed === true) {
      li.classList.add('checked');
    }
li.innerHTML = `
      <input type="checkbox" class="checkbox" ${checked}>
      ${item.name}
      <button class="delete-button">X</button>
    `;
    // finally add the <li> to the <ul>
    todoItemsList.append(li);
  });
}
// function to add todos to local storage
function addToLocalStorage(todos) {
  // conver the array to string then store it.
  localStorage.setItem('todos', JSON.stringify(todos));
  // render them to screen
  renderTodos(todos);
}
// function helps to get everything from local storage
function getFromLocalStorage() {
  const reference = localStorage.getItem('todos');
  // if reference exists
  if (reference) {
    // converts back to array and store it in todos array
    todos = JSON.parse(reference);
    renderTodos(todos);
  }
}
// toggle the value to completed and not completed
function toggle(id) {
  todos.forEach(function(item) {
    // use == not ===, because here types are different. One is number and other is string
    if (item.id == id) {
      // toggle the value
      item.completed = !item.completed;
    }
  });
addToLocalStorage(todos);
}
// deletes a todo from todos array, then updates localstorage and renders updated list to screen
function deleteTodo(id) {
  // filters out the <li> with the id and updates the todos array
  todos = todos.filter(function(item) {
    // use != not !==, because here types are different. One is number and other is string
    return item.id != id;
  });
// update the localStorage
  addToLocalStorage(todos);
}
// initially get everything from localStorage
getFromLocalStorage();
// after that addEventListener <ul> with class=todoItems. Because we need to listen for click event in all delete-button and checkbox
todoItemsList.addEventListener('click', function(event) {
  // check if the event is on checkbox
  if (event.target.type === 'checkbox') {
    // toggle the state
    toggle(event.target.parentElement.getAttribute('data-key'));
  }
// check if that is a delete-button
  if (event.target.classList.contains('delete-button')) {
    // get id from data-key attribute's value of parent <li> where the delete-button is present
    deleteTodo(event.target.parentElement.getAttribute('data-key'));
  }
});

Не стесняйтесь взломать то, что вы только что сделали!

Подведение итогов

Надеюсь, вам понравился этот урок. Если у вас есть какие-либо сомнения, пожалуйста, прокомментируйте их ниже.

И, что более важно, если вам понравился этот урок, жмите кнопку хлопать столько раз, сколько хотите.

Спасибо 😇.