Реализация todo-list на Vue.js

Недавно начал изучать Vue.js

Для обкатки знаний поставил себе следующую задачу:

Реализовать todo-list (план дел на ближайшее время) со следующими простыми возможностями:

1) Добавление новой задачи
2) Удаление задачи
3) Отметка задачи как выполненной/не выполненной
4) Синхронизация состояния задач с удалённым хранилищем (чтобы после перезагрузки страницы данные не пропадали)

Получилось что-то вроде этого:

https://web-finder.ru/files/todo-checklist/

Особенности реализации читайте под катом.

Собственно начнём с вёрстки, но с учетом использования Vue.

Нам понадобится 2 шаблона:

  • шаблон блока с задачами (task-list)
  • шаблон задачи (task-item)

Собственно index.html:

<div id="app">
	<task-list :tasks="tasks"></task-list>
</div>

<template id="task-list">
	<section class="tasks">
		<div class="tasks__new input-group">
			<input type="text" placeholder="Название задачи" class="input-group-field" v-model="newTask" @keyup.enter ="addTask"> 
			<span class="input-group-button">
				<button class="button" @click="addTask">
					<i class="fa fa-plus"></i> Добавить
				</button>
			</span>
		</div>

		<ul class="tasks__list no-bullet">
			<task-item  v-for="(task, index) in tasks" @remove="removeTask(index)" @complete="completeTask(task)" :task="task" :key ="task.id"></task-item>
		</ul>
	</section>
</template>

<template id="task-item">
    <li class="tasks__item">
      	<button :class="className" @click.self="$emit('complete')">
        	{{ task.title }}
      	</button>
      	<button class="tasks__item__remove button alert pull-right" @click="$emit('remove')">
        	<i class="fa fa-times"></i>
      	</button>
    </li>
</template>

JS код же из себя представляет 2 компонента Vue:

  • один для работы с записью (task-item)
  • второй для работы с списком записей (task-list)

Кроме того нам понадобится сам экземпляр Vue ( см. переменную app)

Vue.component('task-list', {
  template: '#task-list',
  props: {
    tasks: {default: []}
  },
  data() {
    return {
      newTask: ''
    };
  },
  methods: {
    addTask() {
      if (this.newTask) {
        this.tasks.push({
          title: this.newTask,
          completed: false
        });
        this.newTask = '';
      }
    },
    completeTask(task) {
      task.completed = ! task.completed;
    },
    removeTask(index) {
      this.tasks.splice(index, 1);
    }
  }
});

Vue.component('task-item', {
  template: '#task-item',
  props: ['task'],
  computed: {
    className() {
      let classes = ['tasks__item__toggle'];
      if (this.task.completed) {
        classes.push('tasks__item__toggle--completed');
      }
      return classes.join(' ');
    }
  }
});

let app = new Vue({
  el: '#app',
  data: {
    tasks: []
  },
  flag_rewrite: false,
  watch: {
    tasks: { 
      handler: function (newVal) {
        if (this.flag_rewrite){
          axios({
            method: 'post',
            url: '/files/todo-checklist/ajax.php',
            data: {
              action: 'set-storage',
              'data-storage': JSON.stringify(newVal)
            }
          })
          .then(function (response) {
            if (response.data == 'error'){
              alert('Ошибка: не удалось сохранить данные');
            }
          })
          .catch(function (){
            alert('Ошибка: не удалось сохранить данные');
          })
        }
        if (!this.flag_rewrite) this.flag_rewrite = true;
      },
      deep: true
    }  
  }
});

document.addEventListener('DOMContentLoaded', function(){
  axios({
    method: 'post',
    url: '/files/todo-checklist/ajax.php',
    data: {
      action: 'get-storage'
    }
  })
  .then(function (response) {
    data = response.data;
    if (data !== 'error'){
      app.tasks = data;
    } else {
      alert('Ошибка: не удалось загрузить данные');
    }
  })
  .catch (function (){
    alert('Ошибка: не удалось загрузить данные');
  })
});

Компонент task-item управляет наличием класса tasks__item__toggle—completed, который перечеркивает задачу, если она выполнена.

Компонент task-list реализует методы на добавление задачи, удаление и установку флага «выполнено»

Сохранение состояния задач производится за счёт обработчика метода-наблюдателя (см. watch в экземпляре приложения Vue — app)

Опция deep установлена в true для того, чтобы слежение реагировало на изменения во вложенных объектах — в данном случае поля задачи completed.

Собственно в момент загрузки DOM (см. document.addEventListener(‘DOMContentLoaded’, function(){…} ) производится post обращение к скрипту /files/todo-checklist/ajax.php через action = ‘get-storage’

Он в свою очередь отдаёт нам все задачи в формате JSON и загружает их в экземпляр Vue.

ajax.php:

<?
header('Content-Type: application/json');
$data = 'error';
$post = json_decode(file_get_contents('php://input'), true);
if ($post['action'] == 'get-storage'){
	$data = file_get_contents('storage.storage');
}
if ($post['action'] == 'set-storage'){
	$data_storage = json_decode($post['data-storage']);
	if ($data_storage!==false && $data_storage!==NULL){
		if (file_put_contents('storage.storage', $post['data-storage'])){
			$data = 'success';
		}
	}
}
echo $data;
?>

В момент именения задачи в методе-наблюдателе объекта Vue производится обращение к ajax.php но уже с action = ‘set-storage’, который сохраняет json представление задач в файле storage.storage