пятница, 5 марта 2010 г.

Как я делал динамический список на jQuery #1

Все началось с того, что мне нужно было сделать динамический список, то есть такой список элементы которого я бы мог редактировать и удалять, а так же добавлять новые.
- Черт возьми! - думал я. - Как я устал от того, что любое нажатие, даже вход в режим редактирования элемента списка, вызывает перезагрузку страницы! С этим определенно нужно что-то делать.
Я знал, что существует такая библиотека, как jQuery, но это все что я про нее знал. К тому же я совсем не владею JavaScript.
- Ладно, наверняка Google нам поможет. - подумал я и открыл Яндекс.
(Я тут не буду дословно приводить как именно я искал, в наше время "жужлить" должны уметь все. Так же, как правило, я открывал первые результаты в поиске, так что приводимые ссылки никак нельзя назвать "лучшее по теме", это просто первое, что нашлось.)
Первая серия ссылок очень помогла получить общее представление о jQuery:
(в действительности мне понадобились только первая и третья части)
И тут уже можно было с чего-то начинать. Во-первых я скачал библиотеку jQuery (официальный сайт).

Во-вторых сделал файл index.html:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>jQuery list demo</title>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<script type="text/javascript" src="jquery-1.3.2.js"></script>

</head>
<body>

<h1>List</h1>
<hr />

<ul id="dynamic_list_1">
<li id="0">First item</li>
<li id="1">Second item</li>
<li id="2">Third item</li>
</ul>

</body>
</html>

1. Все-таки динамический список
Это был совсем не динамический список. Я решил для начала сделать так, чтобы элементы списка подгружались динамически, для этого в заголовок страницы я добавил такой скрипт:
<script type="text/javascript">
$(document).ready(function(){ 
 $.getJSON('db.php', {act: "getall"}, function(json){

  $('#dynamic_list_1').html('');

  $.each(json,function(index,data){
   $('#dynamic_list_1').append(
    '<li id="' + index + '">' +
    '<div class="item_content">' + data + '</div>' +
    '<div class="controls"><a href="#" id="edit">edit</a> <a href="#" id="delete">delete</a></div>' +
    '</li>\n'
   );
  });
 }); 
});
</script>

И тут я хотел бы сказать кое-что о синтаксисе. Наверняка это хорошо известно тем, кто программирует на JavaScript, но меня тут ожидало большое открытие. Это передача функции и ее кода как параметра другой функции.
Смотрите, вот с самого начала мы вызываем функцию ready для документа, которая будет вызвана браузером сразу же, но какие параметры мы ей передаем? Мы ей передаем функцию, которая должна быть вызвана как только DOM документа будет полностью загружен. Если убрать все несущественное, то останется только:
jQuery(document).ready( function(){ /*...*/ } );
Вот эта function(){ /*...*/ } и есть тот параметр, который мы передали в функцию ready и она теперь "знает" какой код нужно выполнять когда DOM будет загружен.
Если вы никогда не использовали такие штуки, то вам может показаться, что это все очень не удобно, но стоит привыкнуть и вы поймете, что на самом деле это очень изящная парадигма передачи функций как параметров. К тому же весь jQurey построен на этом принципе.
Еще один пример встречается чуть ниже: итератор each.
jQurey.each(json,function(index,data){ /*...*/ });
Тут вызывается функция each, которой передается переменная-коллекция json и функция, которая будет вызвана на каждой итерации, в которую, в свою очередь, будет передан индекс (index) элемента коллекции и сам элемент коллекции в переменную data.
Кстати, про each я жужлил отдельно и нашел информацию на api.jquery.com. Запомните этот адрес там мно-о-о-ого полезного.

Вернемся к коду. Основная функция тут getJSON. Она делает вызов db.php и при помощи метода GET, передает в скрипт параметр с именем act и значением "getall". Так же функции getJSON передается функция, которая будет вызвана в случае успешной передачи данных в которую, в свою очередь, будет передана информация, которую вернул скрипт.

Строки $('#dynamic_list_1').html(''); и $('#dynamic_list_1').append('...'); подробно разбирать не будем. Первая очищает все внутри нашего списка, вторая добавляет в него элементы, полученные от скрипта db.php

Кстати, скрипт db.php по смыслу будет "серверной стороной" нашего списка и будет обслуживать запросы от index.html: изменять БД с нашим списком. Для примера, пусть список хранится в простом текстовом файле items.txt:
First item
Second item
Third item

А вот сам db.php
<?php
 $act=$_GET['act'];
   
 file_put_contents('debug.txt',var_export($_GET,true));
 
 if($act=='getall') {
  $items=file('items.txt');
  $items = array_map("rtrim", $items);
  echo json_encode($items);
 } 
?>

Запись в файл debug.txt (строка 4) тут нужна исключительно для отладки, чтобы отслеживать запросы ajax.
Функция json_encode сворачивает данные php в формат JSON (о формате JSON вы можете почитать в той серии ссылок, что я приводил выше, а о json_encode на оф.сайте php.net) А данные, в свою очередь, будет получены и переданы в итератор each, как я уже писал выше.

Итак, в двух словах: мы сделали код, который после загрузки страницы index.html обратится к скрипту db.php, получит у него список и выведет его.

2. Удалить элемент
Вы наверняка заметили, что в каждый элемент я добавил две ссылки: edit и delete. По нажатию на эти ссылки элемент должен редактироваться и удаляться, соответственно. Начнем с удаления, потому что это проще.

Реализовать удаление я предполагал следующим образом: послать запрос на удаление в php скрипт с индексом удаляемого элемента и если запрос успешно отработан удалить элемент со страницы. Для этого в index.html я добавил такой код:

$("div.controls #delete").live("click", function(){

  var item=$(this).parents("li");
  var item_id = item.attr('id'); 
  
  $.get('db.php', {act: 'del', id: item_id}, function(){
   item.animate({ opacity: 'hide' }, "fast");
   item.remove();
  });
    
 });

Давайте разберемся. Во-первых несмотря на то, что для отлавливания клика в "jQuery для начинающих. Часть 1." используется метод .click я использовал метод .live. Его я нашел на сайте перевода официальной документации. Разница в том, что .click работает только для статических элементов страницы, а .live так же и для тех, что были созданы самим скриптом.
В третьей строке мы получаем тэг-родитель li, собственно весь элемент списка, а в четвертой его id. (Как получить свойства тэга я нашел тут).
А дальше следует метод вызов метода .get (описание тут). Я решил не использовать .getJSON, поскольку мне не нужно принимать сложную структуру данных, а только убедиться в том, что вызов прошел успешно. В качестве параметров скрипту передается act="del" и id с номером элемента списка.
После, если вызов прошел успешно, со страницы удаляется элемент списка. Метод .animate я взял из "jQuery для начинающих", а .remove с форума JavaScript.ru.

db.php так же придется доработать. Я дополнил условие следующим кодом:
} elseif ($act=='del') {
  $items=file('items.txt');
  
  unset($items[$_GET['id']]);
  
  $items = array_map("rtrim", $items);
  $str = implode("\n", $items);
  file_put_contents ("items.txt", $str);
 }
Тут все очевидно, поэтому останавливаться не будем.

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

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

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

Дополним index.html формой для ввода данных нового элемента. Для этого после нашего списка вставим такой код:
<div class="add_item">
 <input class="item_content" type="text" value="">
 <div class="controls"><a href="#" id="add">add</a></div>
</div>

А в скрипт вставим обработку:
$("div.add_item div.controls #add").click(function(){

  $("#dynamic_list_1 li.editing").remove();
  $("#dynamic_list_1 li").show();
  
  var context=$("div.add_item .item_content");
  
  $.get('db.php', {act: 'add', cont: context.attr('value')}, function(index){
   $('#dynamic_list_1').append(
          '<li id="' + index + '">' +
       '<div class="item_content">' + context.attr('value') + '</div>' +
       '<div class="controls"><a href="#" id="edit">edit</a> <a href="#" id="delete">delete</a></div>' +
       '</li>\n'
      );
   context.attr('value','');
  });

 });

Тут все достаточно очевидно, кроме 3-й и 4-й строки, которые мы рассмотрим, когда будем добавлять редактирование элементов.
В переменную context мы сразу запоминаем элемент страницы input в котором хранятся данные для нового элемента списка, потом делаем запрос к скрипту db.php, который возвращает номер добавленного элемента. И добавляем новый элемент на страницу. В самом конце поле input очищается (установка аттрибутов).

В db.php тоже добавим обработку вызова:
} elseif ($act=='add') {
  $items=file('items.txt');

  $items[]=$_GET['cont'];
  echo count($items)-1;
  
  $items = array_map("rtrim", $items);
  $str = implode("\n", $items);
  file_put_contents ("items.txt", $str);
 }

4. И, наконец, редактировать элемент
С редактированием элемента будет лишь немного сложнее, чем со всем остальным. Редактирование мы разделим на два этапа:
  1. Вход в режим редактирования
  2. Сохранение результатов, выход из режима редактирования

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

Для этого в index.html добавим такой скрипт:

$("div.controls #edit").live("click", function(){

  $("#dynamic_list_1 li.editing").remove();
  $("#dynamic_list_1 li").show();
  
  var item=$(this).parents("li");
  var item_id = item.attr('id');
  var context=item.children('div.item_content'); 
  
  item.after(
   '<li class="editing" id="' + item_id + '">' +
   '<input class="item_content" type="text" value="' + context.html() + '">' +
   '<div class="controls"><a href="#" id="ok">ok</a> <a href="#" id="cancel">cancel</a></div>' +
   '</li>\n'
  );
  item.hide();
      
 });


По щелчку на ссылке edit скрипт добавляет код с полем ввода после элемента в котором кликнули edit (метод .after), а потом скрывает сам элемент методом .hide.

Третья и четвертые строки тут нужны вот для чего: если какой-то элемент уже был в режиме редактирования, то третья строка удаляет поле ввода редактирования, а четвертая показывает элемент, который был скрыт так как редактировался. Таким образом предыдущее редактирование отменяется и только после этого в режим редактирования "входит" новый элемент.
Аналогичные есть строки есть в блоке добавления элемента (см выше). Это нужно для сброса режима редактирования при добавлении новых элементов списка. В принципе это не обязательно, но интерфейсно понятнее для пользователя.

Добавим обработку ссылки cancel для режима редактирования:

$("li.editing div.controls #cancel").live("click", function(){
  $("#dynamic_list_1 li.editing").remove();
  $("#dynamic_list_1 li").show();
 });

И, наконец, отправка информации, которую мы отредактировали:

$("li.editing div.controls #ok").live("click", function(){

  var item=$(this).parents("li"); 
  var item_id = item.attr('id'); 
  var context=item.children("input.item_content");
  
  $.get('db.php', {act: 'edit', id:item_id, cont: context.attr('value')}, function(){
   item.after(
    '<li id="' + item_id + '">' +
    '<div class="item_content">' + context.attr('value') + '</div>' +
    '<div class="controls"><a href="#" id="edit">edit</a> <a href="#" id="delete">delete</a></div>' +
    '</li>\n'
   );
   item.remove();
   $("#dynamic_list_1 li:hidden").remove();
   
  });

 });

Общая логика такова: мы отправляем скрипту db.php информацию о редактировании с id редактируемого элемента и содержимым поля input. В случае успешного завершения вызова добавляем новый отредактированный элемент в список, а старый элемент и поле ввода режима редактирования удаляем.

Соответственно нужно добавить код и в db.php:
} elseif ($act=='edit') {
  $items=file('items.txt');
  
  $items[$_GET['id']]=$_GET['cont'];
  
  $items = array_map("rtrim", $items);
  $str = implode("\n", $items);
  file_put_contents ("items.txt", $str);
 }

Вот, собственно и все. Теперь у нас есть страница со списком, который мы можем редактировать не перегружая самой страницы.

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

Конечно, приведенный код далеко не оптимальный, возможно имеет баги и проблемы безопасности, поэтому его нельзя использовать в ваших проектах. К тому же для работы со списками (и даже таблицами) есть специализированные framework-и созданные профессионалами. А этот пример нужен только для того, чтобы продемонстрировать основные принципы работы с jQuery и динамическими списками. А так же продемонстрировать то, что весь мир находится на грани катастрофы, потому что если кто-то захочет поработить человечество ему достаточно будет только пожужлить.

Итак, согласно принципам agile мы максимально быстро наупырили какое-то количество корявого кода. На следующем шаге я приведу его в порядок и обязательно поделюсь с вами.

Всем спасибо.

UPD.: Как я делал динамический список на jQuery #2

3 комментария:

Ragnar комментирует...

Многабукаф, лень читать. Но крута, палюбому!

Kand комментирует...

в скачанный вариант не работает: в третьей строке db.php необходимо вместо == сделать !=
В приведенном выше коде строка $("div.add_item div.controls #add").click(function(){ напрочь отказалась работать - пришлось заменить ее на .live("click", function(){

Green FiLin комментирует...

2Kand:
Не совсем понятно в связи с чем в третьей строке нужно неравенство? Это же нарушит логику работы программы.

Отправить комментарий