Google таблицы как база данных для веб-приложения. Часть 2

В первой части статьи мы:

  • узнали как создать веб-приложение на Google Apps Script, подключиться к таблице и выполнить первую публикацию;
  • заложили основу под наш будущий роутинг запросов;
  • рассмотрели как можно записывать данные в таблицу и получать их из нее;
  • разобрались как можно удалять и обновлять записи в таблице;
  • узнали как переопубликовывать проект, чтобы внесенные изменения вступили в силу.

Теперь мы готовы к созданию клиентской части.

Создание структуры клиентской части

Для создания клиентской части мы будем использовать CSS фреймворк Bootstrap. Создадим папку с проектом и внутри создадим два файла:

  1. index.html;
  2. main.js;

Содержание “index.html”:

<!doctype html>
<html lang="ru">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Мои задачи</title>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body>

    <div class="container">
      <!-- контейнер, в котором мы будем размещать нашу таблицу с задачами -->
      <div id="tasksTableDiv" class="table-responsive mt-5"></div>
    </div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js"></script>
    <script type="text/javascript" src="main.js"></script><!-- скрипт с нашим кодом -->

  </body>
</html>

Далее открываем наш файл “main.js” и вставляем туда следующий код, не забыв заменить уникальный идентификатор приложения.

var myApp = "https://docs.google.com/spreadsheets/d/1M1OKuBxGoXXXXXXXXXXXXXXXXXXXXXXXXXXXXS3EWj6Bs/edit#gid=0";//URL нашего приложения

$( document ).ready(function() {//функция запускается, как только страница будет готова для просмотра пользователю
	getTasks ();//запускаем функцию для получения списка задач
});

function getTasks () {
    var action = "getTasks";
    var url = myApp+"?action="+action

    //подготавливаем и выполняем GET запрос
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
        	//в случае успеха преобразуем полученный ответ в JSON и передаем отдельной 
                функции, которая сформирует нам таблицу
        	var data = JSON.parse(xhr.response);
        	tasksTable (data);
        }
    };
    try { xhr.send(); } catch (err) {console.log(err) }
}

//функция, которая обработает данные, полученные при выполнении запроса и сформирует из них таблицу
function tasksTable (data) {
	$('#tasksTableDiv').html( function () {
		var th = '';
		var td = '';

		for ( i = 0; i < data.length; i++ ) {
			//первый элемент массива представляет собой заголовки таблицы
			if ( i == 0 ) {
				th +=   '<tr>'+
					     '<th>'+data[i][0]+'</th>'+
					     '<th>'+data[i][1]+'</th>'+
					     '<th>'+data[i][2]+'</th>'+
					 '</tr>';
				continue;
			}

			//остальные элементы считаем строками таблицы, из которых мы 
                        сформируем содержание
			var status = ( data[i][2] == 0 ) ? 'В очереди' : 'Выполнена';
			td +=   '<tr>'+
					'<td>'+data[i][0]+'</td>'+
					'<td>'+data[i][1]+'</td>'+
					'<td>'+status+'</td>'+
				'</tr>';
		}
		//собираем и возвращаем готовую таблицу
		return '<table class="table"><thead>'+th+'</thead><tbody>'+td+'</tbody></table>'
	})
}

Сохраняем, запускаем index.html и, если все сделали правильно, то мы должны увидеть вот такую симпатичную картину.

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

Пользователь создает новую задачу

Для того чтобы добавлять задачи нам на стороне HTML потребуется форма с полем для ввода и кнопка, по нажатию на которую будет срабатывать скрипт сохранения новой задачи. Все это дело мы будем выводить в модальном окне, которое будет появляться при нажатии на отдельную кнопку.

Добавим после контейнера с идентификатором «tasksTableDiv» следующий html код:

<!-- кнопка для вызова модального окна -->
<button type="button" class="btn btn-primary mt-3" data-toggle="modal" data-target="#addTaskModal">Новая задача</button>

Перед скриптами внутри тэга body добавляем html код с содержанием модального окна.

<!-- модальное окно для новой задачи -->
<div class="modal fade" id="addTaskModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLongTitle">Новая задача</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        <form id="addTaskForm" onsubmit="return false;">
          <div class="form-group">
            <label for="task">Задача</label>
            <input id="task" name="task" class="form-control form-control-sm" type="text">
          </div>
        </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
        <button type="button" class="btn btn-success" onclick="addTask()">Сохранить</button>
      </div>
    </div>
  </div>
</div>

Теперь добавляем функцию addTask() в main.js:

function addTask () {
	var task = $('#task').val();
	var action = "addTask";

	var xhr = new XMLHttpRequest();
	var body = 'task=' + encodeURIComponent(task) + '&action=' + encodeURIComponent(action);
	xhr.open("POST", myApp, true);
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
        	alert(xhr.response);
        	document.getElementById("addTaskForm").reset();//сбрасываем форму
		$('#addTaskModal').modal('hide');//скрываем модалку
		getTasks ();//обновляем список задач
        }
    };
	try { xhr.send(body);} catch (err) {console.log(err) }
}

Пользователь удаляет задачу

Для возможности удаления задач, нам потребуется повесить событие на какой-нибудь элемент, например, на кнопку-ссылку в дополнительной колонке таблицы и написать соответствующую функцию.

Модифицируем функцию вывода списка задач следующим образом:

function tasksTable (data) {
	$('#tasksTableDiv').html( function () {
		var th = '';
		var td = '';

		for ( i = 0; i < data.length; i++ ) {
			//первый элемент массива представляет собой заголовки таблицы
			if ( i == 0 ) {
				th +=   '<tr>'+
						'<th>'+data[i][0]+'</th>'+
					  	'<th>'+data[i][1]+'</th>'+
					  	'<th>'+data[i][2]+'</th>'+
					  	'<th>Удалить</th>'+
					  '</tr>';
				continue;
			}

			//остальные элементы считаем строками таблицы, из которых мы сформируем содержание
			var status = ( data[i][2] == 0 ) ? 'В очереди' : 'Выполнена';
			td +=   '<tr>'+
					'<td>'+data[i][0]+'</td>'+
					'<td>'+data[i][1]+'</td>'+
					'<td>'+status+'</td>'+
					'<td><button type="button" class="btn btn-link" onclick="deleteTask(\''+data[i][1]+'\')">Удалить</button></td>'+
				'</tr>';
		}
		//собираем и возвращаем готовую таблицу
		return '<table class="table"><thead>'+th+'</thead><tbody>'+td+'</tbody></table>'
	})
}

Функция для удаления задачи:

function deleteTask (task) {
	var action = "deleteTask";
	var xhr = new XMLHttpRequest();
	var body = 'task=' + encodeURIComponent(task) + '&action=' + encodeURIComponent(action);
	xhr.open("POST", myApp, true);
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
        	alert(xhr.response);
		getTasks ();//обновляем список задач
        }
    };
	try { xhr.send(body);} catch (err) {console.log(err) }
}

Пользователь меняет статус задачи

В данном случае будем задействовать универсальное модальное окно с генерируемым содержимым посредством JS. На стороне сервера мы предусмотрели возможность редактирования статуса задачи и ее названия. Мы рассмотрим вариант редактирования на примере изменения статуса. При необходимости можно реализовать возможность редактирования названия по аналогии с примером ниже, дополнив кейсы для switch в функциях ниже.

Добавляем еще одно модальное после первого:

<!-- модальное окно для обновления задачи -->
<div class="modal fade" id="updateTaskModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title"></h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body"></div>
      <div class="modal-footer"></div>
    </div>
  </div>
</div>

Модифицируем функцию вывода списка задач:

function tasksTable (data) {
	$('#tasksTableDiv').html( function () {
		var th = '';
		var td = '';

		for ( i = 0; i < data.length; i++ ) {
			//первый элемент массива представляет собой заголовки таблицы
			if ( i == 0 ) {
				th +=   '<tr>'+
						'<th>'+data[i][0]+'</th>'+
					  	'<th>'+data[i][1]+'</th>'+
					  	'<th>'+data[i][2]+'</th>'+
					  	'<th>Удалить</th>'+//
					 '</tr>';
				continue;
			}

			//остальные элементы считаем строками таблицы, из которых мы 
                        сформируем содержание
			var status = ( data[i][2] == 0 ) ? 'В очереди' : 'Выполнена';
			var color = ( data[i][2] == 1 ) ? 'class="table-success"' : '';
			td +=   '<tr '+color+'>'+
					'<td>'+data[i][0]+'</td>'+
					'<td>'+data[i][1]+'</td>'+
					'<td><button type="button" class="btn btn-link" onclick="updateTaskModal(\''+data[i][1]+'\', \''+data[i][2]+'\', \'status\')">'+status+'</button></td>'+
					'<td><button type="button" class="btn btn-link" onclick="deleteTask(\''+data[i][1]+'\')">Удалить</button></td>'+
				 '</tr>';
		}
		//собираем и возвращаем готовую таблицу
		return '<table class="table"><thead>'+th+'</thead><tbody>'+td+'</tbody></table>'
	})
}

Добавляем функцию, которая будет отвечать за содержание формы модального окна, т.к. если мы меняем статус, то нам нужно показать пользователю выпадающий список со статусами, а если мы меняем название задачи, то нам следует показать пользователю поле для редактирования текущего названия.

function updateTaskModal(task, currentValue, where) {
	var title = 'Редактировать задачу';
	switch (where) {
		case "status":
			var input = '<div class="form-group">'+
			                '<label for="status">Статус</label>'+
			                '<select id="status" name="status" class="form-control form-control-sm">'+
			                	'<option value="0" '+isSelected(currentValue, 0)+'>В очереди</option>'+
			                	'<option value="1" '+isSelected(currentValue, 1)+'>Выполнена</option>'+
			                '</select>'+
			            '</div>';
			break;
	}

	var form = '<form id="updateTaskForm" onsubmit="return false;">'+input+'</form>';
	var buttons = '<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>'+
            	      '<button type="button" class="btn btn-success" onclick="updateTask(\''+task+'\', \''+where+'\')">Сохранить</button>';

	$('#updateTaskModal .modal-header .modal-title').html(title);
	$('#updateTaskModal .modal-body').html(form);
	$('#updateTaskModal .modal-footer').html(buttons);
	$("#updateTaskModal").modal("show");
}

function isSelected(currentValue, value) {
	if (currentValue == value) return "selected";
}

И наконец добавляем функцию, которая отправит новое значение для сохранения на сервере.

function updateTask(task, where) {
	var action = "updateTask";

	switch (where) {
		case "status":
			var newValue = $('#status').val();
			break;
	}
	
	var xhr = new XMLHttpRequest();
	var body = 'task=' + encodeURIComponent(task) + '&where=' + encodeURIComponent(where) + '&newValue=' + encodeURIComponent(newValue) + '&action=' + encodeURIComponent(action);
	xhr.open("POST", myApp, true);
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
        	$("#updateTaskModal").modal("hide");
        	alert(xhr.response);
		getTasks ();//обновляем список задач
        }
    };
	try { xhr.send(body);} catch (err) {console.log(err) }

}