Skip to main content
deleted 30 characters in body
Source Link
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
      .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function() {
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function() {
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function() {
      let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
      const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if (this.isValidInput) {
        this.todos.push(newToDo);
        this.newTitle = "";
      }
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */

::-webkit-scrollbar-track {
  background: #efefef;
}


/* Handle */

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}


/* Handle on hover */

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
     .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function(){
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function(){
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function(){
     let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
     const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if(this.isValidInput){
        this.todos.push(newToDo);
        this.newTitle = "";
      }
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */

::-webkit-scrollbar-track {
  background: #efefef;
}


/* Handle */

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}


/* Handle on hover */

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
      .then(this.getUnsolvedTodos);
    this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function() {
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function() {
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function() {
      let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
      const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }

      this.validateInput();

      if (this.isValidInput) {
        this.todos.push(newToDo);
        this.newTitle = "";
      }

      // Update unsolved count
      this.getUnsolvedTodos();
    }

  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */

::-webkit-scrollbar-track {
  background: #efefef;
}


/* Handle */

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}


/* Handle on hover */

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
added 2 characters in body
Source Link
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
     .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function(){
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function(){
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function(){
     let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
     const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if(this.isValidInput){
        this.todos.push(newToDo);
      }
      this.newTitle = "";
      }
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */

::-webkit-scrollbar-track {
  background: #efefef;
}


/* Handle */

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}


/* Handle on hover */

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
     .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function(){
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function(){
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function(){
     let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
     const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if(this.isValidInput){
        this.todos.push(newToDo);
      }
      this.newTitle = "";
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */

::-webkit-scrollbar-track {
  background: #efefef;
}


/* Handle */

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}


/* Handle on hover */

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
     .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function(){
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function(){
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function(){
     let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
     const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if(this.isValidInput){
        this.todos.push(newToDo);
        this.newTitle = "";
      }
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */

::-webkit-scrollbar-track {
  background: #efefef;
}


/* Handle */

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}


/* Handle on hover */

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
added 539 characters in body
Source Link
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
      .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function() {
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function(){
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function() {
      let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
      const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if(this.isValidInput){
        this.todos.push(newToDo);
      }
      this.newTitle = "";
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

/* Bar */
::-webkit-scrollbar {
  width: 5px;
}


/* Track */ 

::-webkit-scrollbar-track {
  background: #efefef;
} 


/* Handle */ 

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
} 


/* Handle on hover */ 

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

  <div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
      .then(this.getUnsolvedTodos);
    this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function() {
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // add todo
    addTodo: function() {
      let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
      const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
      this.todos.push(newToDo);
      this.newTitle = "";

      // Update unsolved count
      this.getUnsolvedTodos();
    }
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

/* Bar */
::-webkit-scrollbar {
  width: 5px;
}


/* Track */
::-webkit-scrollbar-track {
  background: #efefef;
}

/* Handle */
::-webkit-scrollbar-thumb {
  background: #d0d0d0;
}

/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

  <div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
      </form>
    </footer>
  </div>
</div>
var app = new Vue({
  el: "#toDoApp",
  data: {
    url: "https://jsonplaceholder.typicode.com/todos?&_limit=5",
    dataIsLoaded: false,
    isValidInput: true,
    todos: [],
    unsolvedTodos: [],
    newTitle: "",
  },
  mounted() {
    axios.get(this.url)
      .then((response) => {
        this.todos = response.data;
      })
     .then(this.getUnsolvedTodos);
      this.dataIsLoaded = true;
  },
  methods: {
    getUnsolvedTodos: function(){
      this.unsolvedTodos = this.todos.filter(todo => {
        return todo.completed === false;
      });
    },
    // toggle todo
    toggleTodo: function(todo) {
      todo.completed = !todo.completed;
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // delete todo
    deleteTodo: function(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      // Update unsolved count
      this.getUnsolvedTodos();
    },
    // validate todo title
    validateInput: function(){
      this.isValidInput = this.newTitle.length >= 3 ? true : false;
    },
    // add todo
    addTodo: function(){
     let lastId = this.todos.length === 0 ? 0 : this.todos[this.todos.length - 1].id;
     const newToDo = {
        id: lastId + 1,
        title: this.newTitle,
        completed: false
      }
     
      this.validateInput();
     
      if(this.isValidInput){
        this.todos.push(newToDo);
      }
      this.newTitle = "";
      
      // Update unsolved count
      this.getUnsolvedTodos();
    }
  
  },
  created() {},
  watch: {}
});
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}

.app-wrapper {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}

#toDoApp {
  width: 320px;
  height: 320px;
  display: flex;
  flex-direction: column;
  background: #fff;
  box-shadow: rgb(99 99 99 / 20%) 0px 2px 8px 0px;
  overflow: hidden;
  border-radius: 8px;
  position: relative;
}

header {
  padding: 15px 20px;
  background: #efefef;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 18px;
  font-weight: 600;
  display: flex;
}

header .title {
  font-size: 18px;
  font-weight: 600;
  color: #323232;
}

header .count {
  margin-left: auto;
  display: inline-block;
  width: 26px;
  line-height: 26px;
  text-align: center;
  border-radius: 3px;
  font-size: 13px;
  background: #c00;
  color: #fff;
}

header .count.zero {
  background: #009688;
}

.todo-list {
  flex: 1;
  overflow-y: auto;
  list-style-type: none;
  padding: 15px 20px 5px 20px;
  color: #4f4f4f;
}

::-webkit-scrollbar {
  width: 5px;
}


/* Track */ 

::-webkit-scrollbar-track {
  background: #efefef;
} 


/* Handle */ 

::-webkit-scrollbar-thumb {
  background: #d0d0d0;
} 


/* Handle on hover */ 

::-webkit-scrollbar-thumb:hover {
  background: #d0d0d0;
}

.todo-list li {
  position: relative;
  display: flex;
  overflow: hidden;
  align-items: center;
  font-size: 13px;
  padding: 10px;
  cursor: pointer;
  margin-bottom: 7px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: #d4e7f7;
  border-radius: 2px;
}

.todo-list li:last-child {
  margin-bottom: 0;
}

.done {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: block;
  margin-bottom: -1px;
  margin-right: 7px;
}

button {
  border: none;
  background: #c00;
  color: #fff;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  top: -1px;
  bottom: -1px;
  right: -1px;
  font-size: 18px;
  width: 50px;
  transform: translateX(50px);
  transition: transform 200ms;
}

.todo-list li:hover button {
  transform: translateX(0);
}

footer {
  padding: 15px 20px 25px 20px;
  margin-top: auto;
  position: relative;
}

.error {
  position: absolute;
  top: 5px;
  right: 22px;
  font-size: 11px;
  color: #c00;
}

input[type="text"] {
  width: 100%;
  padding: 2px 4px;
  border: none;
  font-size: 16px;
  outline: none;
  border-bottom: 2px solid #b8b8b8;
  transition: border-bottom 200ms ease-in;
}

input[type="text"]:focus {
  border-bottom: 2px solid #dddddd;
}

.loader {
  border: 4px solid #ccc;
  border-top-color: transparent;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50%;
  margin-left: -25px;
  left: 50%;
  margin-top: -25px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

<div class="app-wrapper">
  <div id="toDoApp">
    <header>
      <span class="title">My todo list</span>
      <span class="count" v-bind:class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
    </header>
    <ul class="todo-list" v-if="dataIsLoaded">
      <li v-for="todo in todos.slice().reverse()" v-bind:class="{done: todo.completed}" :id="todo.id">
        <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" />
        <span class="title">{{todo.title}}</span>
        <button @click="deleteTodo(todo.id)">
          <i class="fa fa-trash" aria-hidden="true"></i>
        </button>
      </li>
    </ul>
    <div class="loader" v-else></div>
    <footer>
      <form @submit.prevent="addTodo()">
        <input type="text" placeholder="Add new todo..." v-model="newTitle">
        <span class="error" v-if="!isValidInput">Please add at least 3 characters</span>
      </form>
    </footer>
  </div>
</div>
Post Undeleted by Razvan Zamfir
Post Deleted by Razvan Zamfir
Source Link
Loading