Использование jQuery для манипуляции и фильтрации данных

Настройка таблиц

Перед тем как перейти к JavaScript-коду, стоит сделать несколько важных замечаний. Структура нашей таблицы будет такой же, как у любой другой таблицы, за исключением того, что нам понадобятся два тега, которые многие разработчики обычно отбрасывают. Заголовок таблицы должен быть заключён в тег thead. Тело таблицы, которое будет содержать все отображаемые данные, должно быть заключено в тег tbody.

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

Теги заголовка и тела таблицы


    ...
  
First Name Last Name City State
Mannix Bolton

Чересполосица

Чересполосица (зебра) — это довольно распространённая техника организации данных, чрезвычайно полезная, и, вместе с тем, лёгкая для реализации. По своей сути, чересполосица — это применение различных стилей к чётным и нечётным строкам для улучшения читаемости данных по горизонтали. Это особенно важно в таблицах с большим числом столбцов — пользователь может просмотреть один столбец и с лёгкостью прочесть данные, соответствующие остальным столбцам строки. Примеры, которые я буду использовать в этой статье, содержат список людей с четырьмя параметрами: имя, фамилия, город и штат. Обратите внимание на то, как изменяется цвет фона и шрифта соседних строк.

Пример чересполосицы

Теперь, перейдём непосредственно к реализации. Начать следует с внешнего CSS-файла, включаемого в страницу. Сперва укажем стили для таблицы:

table {
  background-color: white;
  width: 100%;
}

Это довольно тривиально — мы устанавливаем белый цвет фона таблицы и растягиваем её на 100% от ширины родительского элемента. Теперь, займёмся ячейками таблицы. Некоторым это может показаться странным — почему именно ячейками, ведь можно задать стиль для целой строки? Оказывается, для кроссбраузерности лучше изменять цвет фона именно у ячеек:

tbody td {
  background-color: white;
}
tbody td.odd {
  background-color: #666;
  color: white;
}

Здесь мы создаём класс для нечётных (odd) строк таблицы, в котором определяем альтернативный цвет фона и шрифта. Мы также указываем стиль по умолчанию для всех элементов td, который будет являться стилем чётных строк. Это весь необходимый CSS-код. Я же говорил, что это просто! Теперь, перейдём к коду jQuery. С помощью селекторов jQuery сделать это ничуть не сложнее. Всё, что нужно — это определить нужные ячейки и применить функцию addClassName.

$(document).ready(function() {
  zebraRows('tbody tr:odd td', 'odd');
}); 
// Используется для применения различных стилей строк
function zebraRows(selector, className)
{
  $(selector).removeClass(className).addClass(className);
}

Несмотря на небольшой объём, в этом коде есть на что посмотреть. Во-первых, обратите внимание на то, как мы абстрагировали реализацию с помощью функции, это необходимо, поскольку если мы будем изменять данные в таблице асинхронно, без обновления страницы, то нужно быть уверенным, что стиль строк всё ещё чередуется. И именно поэтому мы применяем функцию removeClass, так что, если строка изначально была нечётной, а стала чётной, то класс odd будет убран. Сейчас это может показаться запутанным, всё прояснится немного позже, когда мы начнём разбираться с фильтрацией. Исходя из принципа повторного использования кода, селектор и имя класса передаются в функцию, теперь она может быть использована в проектах с различными именами классов и требованиями к селекторам (к примеру, на сайте с несколькими таблицами). Если вы обратите внимание на функцию ready(), функцию jQuery, выполняющуюся по окончании загрузки страницы, вы обнаружите там вызов функции zebraRows(). Здесь мы передаём селектор и имя класса. Селектор использует специальный синтаксис jQuery (:odd), он вернёт нам все нечётные строки. Затем мы находим дочерние элементы строки, то есть ячейки. Этот код понятен тем, кто уже использовал jQuery, но из-за своей простоты, он должен быть доступен и всем остальным.

Замечание. Хотя использование jQuery для добавления чересполосицы — это простое решение, оно неприменимо для пользователей, у которых отключён JavaScript. Я бы порекомендовал применять стили на стороне сервера, например, в PHP-коде, или же непосредственно в HTML, но это уже выходит за рамки текущей статьи.

Эффекты при наведении мыши

Отличным эффектом является подсветка строки, на которую пользователь наводит указатель мыши. Это великолепный способ выделить интересующие его данные. Такой эффект чрезвычайно просто реализовать с помощью jQuery, но сначала немного CSS:

...
td.hovered {
  background-color: lightblue;
  color: #666;
}
...

Это весь CSS-код, который нам понадобится. Мы хотим, чтобы при наведении мыши на строку, все ячейки в этой строке подсвечивались светло-синим фоном, а текст становился серым. Код jQuery, претворяющий наши планы в жизнь, такой же простой:

...
$('tbody tr').hover(function(){
  $(this).find('td').addClass('hovered');
}, function(){
  $(this).find('td').removeClass('hovered');
});
...

Мы используем функцию hover() из библиотеки jQuery. Она принимает два аргумента — функции, которые должны выполняться, когда указатель мыши наводится на элемент и когда покидает его, соответственно. При наведении указателя на строку, мы находим все её ячейки и добавляем им класс hovered. Когда указатель покидает элемент, мы удаляем этот класс. Это всё, что нам нужно сделать, можете сами убедиться!

Демонстрация эффекта при наведении мыши

Фильтрация данных

Перейдём к более содержательному материалу — собственно манипуляции отображаемыми данными. Если на сайте показывается большое число записей, в моём примере — 1000 строк, более чем необходимо дать пользователю возможность отсеивать их каким-либо образом. Один из эффективных способов, который стал широко распространён в качестве составляющей концепции Web 2.0/AJAX, — это фильтрация. Её также активно использует Apple в своих приложениях, например в iTunes. Наша задача — позволить пользователю вводить поисковый запрос в обычное текстовое поле, и фильтровать строки таблицы на лету, показывая лишь те из них, что содержат введённый текст. Безусловно, это сложнее реализовать при чередовании стилей строк, но в целом кода нужно совсем немного, благодаря встроенной функциональности jQuery.

Вначале мы напишем общую функцию, принимающую в качестве параметров селектор и текстовую строку. Эта функция будет просматривать все элементы, соответствующие селектору в поисках указанной строки. Найденные элементы будут показаны, им будет присвоен класс visible, а все остальные — скрыты. Зачем присваивать класс visible? Дело в том, что после фильтрации мы снова будем вызывать функцию zebraRows, но в ней нам не надо учитывать скрытые строки, поэтому, лучшее, что я мог придумать — использование дополнительного класса visible.

Сам поиск производится с помощью JavaScript-функции, названной как нельзя кстати, search(). Исходя из принципов работы DOM, если мы не применим jQuery-функцию text(), поиск будет производиться в HTML-коде строки. Мы немного расширим функциональность, реализовав поиск не по точной строке, а по всем словам запроса. Это отличное решение, поскольку позволяет реализовать «ленивый поиск» — пользователю не нужно запоминать точной строки, достаточно знать лишь какие-то её части. Функция search() в качестве параметра принимает регулярное выражение, поэтому мы должны обрезать все пробельные символы в начале и в конце строки и разделить слова символом «|» — ИЛИ, чтобы реализовать нужную нам функциональность. Регулярные выражения довольно сложны, поэтому вам придётся принять мой код таким, как он есть, или же обратиться к видео-роликам Регулярные выражения для чайников в блоге ThemeForest.

// Фильтрация результатов по запросу
function filter(selector, query) {
  query =   $.trim(query); // Обрезать пробелы
  query = query.replace(/ /gi, '|'); // Добавим ИЛИ к регулярному выражению
 
  $(selector).each(function() {
    ($(this).text().search(new RegExp(query, "i")) < 0) ? $(this).hide().removeClass('visible') : $(this).show().addClass('visible');
  });
}

На шестой строке начинает работать магия, поэтому, вероятно, стоит её пояснить. Начиная с пятой строки, мы проходимся циклом по всем элементам (строкам), которые соответствуют селектору, и затем выполняем для каждого из них код на строке 6. Новичкам в программировании он может показаться запутанным, будет гораздо проще разобраться, если мы разобьём его на части. Всё, что идёт до вопросительного знака, может быть рассмотрено как вопрос. Если ответ на этот вопрос — истина, то выполняется код слева от двоеточия и справа от вопросительного знака. Если ответ — ложь, выполняется код после двоеточия. По сути, это оператор if, записанный в более лаконичной форме, известной как тернарный оператор. Такая запись равнозначна следующему коду:

...
  if ($(this).text().search(new RegExp(query, "i")) < 0) {
    $(this).hide().removeClass('visible')
  } else {
    $(this).show().addClass('visible');
  }
...

Мы сравниваем результат функции search() с нулём, поскольку в нём содержится позиция искомого текста в строке или -1, если текст не найден. Так как -1 всегда меньше нуля, то условие составлено верно. Теоретически, нет ничего неправильного в проверке результата на равенство (==) -1, но на практике безопасней всё же сравнивать его с нулём.

Теперь, когда у нас есть готовая функция фильтрации, привяжем её к вводу с помощью событий jQuery. Чтобы реализовать нужный нам эффект, необходимо отслеживать событие, происходящее при отпускании пользователем клавиши во время ввода текста — событие, называемое keyup в JavaScript. Важно не забыть присвоить полю ввода атрибут ID, чтобы иметь доступ к нему с помощью jQuery:


Теперь вернёмся к нашей функции ready, в ней нужно добавить следующий код после вызова zebraRows():

...
  // По умолчанию все строки имеют класс visible
  $('tbody tr').addClass('visible');
 
  $('#filter').keyup(function(event) {
    // Если нажат Escape или ничего не введено
    if (event.keyCode == 27 || $(this).val() == '') {
      // Если нажат Escape, нужно очистить строку поиска
      $(this).val('');
 
      // Отобразим все строки, так как если
      // ничего не введено, все строки должны быть видны
      $('tbody tr').removeClass('visible').show().addClass('visible');
    }
 
    // Если введён текст, будем фильтровать
    else {
      filter('tbody tr', $(this).val());
    }
 
    // Включим чересполосицу
    $('.visible td').removeClass('odd');
    zebraRows('.visible:odd td', 'odd');
...

Этот код намного более сложный, нежели в предыдущих примерах, поэтому разберём его строка за строкой.

1. В строке с addClass('visible') мы добавляем класс visible всем строкам, по умолчанию все они должны быть видимы.
2. Следующая строка кода начинается с селектора, который в моём случае указывает на поле ввода фильтра. При возникновении события отпускания нажатой клавиши для элемента, задаваемого селектором, будет выполнена следующая ниже функция. Заметьте, что мы передаём в неё параметр event, который содержит различную информацию о произошедшем событии.
3. Затем, на следующей строке, мы используем этот параметр, проверяя в условии, не нажал ли пользователь клавишу Escape. Обратите внимание, что каждой клавише соответствует числовой код, по которому мы можем определить, какую клавиша была нажата. Это полезная функция, поскольку пользователи могут с лёгкостью отменить фильтрацию и снова просмотреть все данные. Многие приложения с возможностью фильтрации используют эту возможность, и мы тоже не хотим от них отставать.
4. В том же условии мы учитываем тот случай, когда поле с фильтром пустое (пользователь нажимал Backspace и удалил все символы). Если уж так произошло, мы, разумеется, должны показать все строки, но это придётся сделать отдельно, потому что написанная нами функция фильтрации стала бы искать строки без содержимого, и, соответственно, скрыла бы все строки, содержащие какой-либо текст, то есть совершенно не то, что нам нужно! (Оставляем это высказывание на совести автора. Функция search() для пустой строки всегда возвращает 0, а следовательно все строки будут отображены, а не скрыты. Тем не менее, разумеется, в случае пустого ввода поиск был бы излишним. — Прим. пер.)
5. Если выполнено одно из этих условий, то есть если был нажат Escape или введена пустая поисковая строка, мы очищаем поле с фильтром, хотя, в последнем случае это ничего не изменит.
6. После этого мы, как и хотели, показываем все строки таблицы и добавляем им класс visible. Опять же, для безопасности уберём уже присвоенные классы visible, чтобы не устанавливать их дважды. Фильтрацию же мы произведём только в том случае, если поле с фильтром не пусто и пользователь не нажимал Escape.
7. Теперь, в блоке else мы вызываем нашу функцию фильтрации, скрывая строки таблицы, не содержащие введённой строки.
8. В конце концов, скрыв и показав всё, что нужно, необходимо вновь применить функцию zebraRows к оставшимся строкам. Сначала мы убираем имеющиеся классы, чтобы учесть случаи, когда строки изменяют чётность. Этот вызов zebraRows абсолютно идентичен самому первому вызову после загрузки страницы, за исключением лишь такой дополнительной проверки.

Фильтрация записей в действии

Замечание. Было бы неплохо скрыть поле фильтра с помощью CSS и динамически показывать его прямо перед обработчиком keyup, чтобы пользователи с отключенным JavaScript не были бы обмануты наличием фильтрации, которая на самом деле неприменима. Код выглядел бы так:

style.css:

...
#filter { display: none; }
...

application.js:

...
$('#filter').show();
...

Да уж, сколько кода мы разобрали! Можете сделать перерыв, перед тем как мы перейдём к следующей части — сортировке...

Сортировка столбцов

Ну что, готовы? Тогда, вперёд!

Последней возможностью, которую мы реализуем, будет сортировка таблицы по произвольному столбцу. Нажатие на заголовок столбца для сортировки — довольно распространённая и знакомая пользователям практика. При первом нажатии на заголовок мы сортируем таблицу по возрастанию значений столбца, при повторном — по убыванию значений. Этот код не для слабых духом — он довольно сложен. Общие принципы были заимствованы из книги «Learning jQuery 1.3». Я немного переработал код, чтобы он соответствовал нашим требованиям о простоте, тем не менее, если вы лучшего контроля, обратитесь к седьмой главе книги, в которой очень детально обсуждается работа jQuery с таблицами.

Перед тем, как мы перейдём к самому коду, хотелось бы обсудить идею решения поставленной задачи. Мы будем использовать встроенный метод JavaScript, sort(), принимающий в качестве параметра массив, и упорядочивать этот массив с помощью собственной функции. В данном случае нам нужна обычная алфавитно-цифровая сортировка, поэтому мы просто будем сравнивать два элемента и решать, в каком порядке они должны следовать друг за другом. Поскольку мы хотим сортировать как по возрастанию, так и по убыванию, нам придётся присваивать CSS-класс, указывающий на то, в каком направлении отсортирован каждый столбец, и при необходимости изменять его. После каждой сортировки мы будем заново вставлять строки по одной в таблицу. Это может показаться трудоёмким, но JavaScript чертовски быстр, и для пользователя это будет практически незаметно. Все эти действия будут происходить по щелчку на заголовке столбцов таблицы.

Как обычно, сперва разберёмся с CSS-кодом, как с наиболее простым:

th.sortable {
    color: #666;
    cursor: pointer;
    text-decoration: underline;
}
th.sortable:hover { color: black; }
th.sorted-asc, th.sorted-desc  { color: black; }

Все заголовки столбцов, по которым можно будет сортировать таблицу, имеют класс sortable. Мы используем CSS-модификатор hover, чтобы они выглядели как ссылки. Также мы используем классы sorted-asc и sorted-desc, чтобы показывать пользователю, по какому столбцу отсортирована таблица, об этом мы уже упоминали ранее. Хотя я не использовал это в коде, было бы неплохо поместить фоновые изображения со стрелками вверх или вниз в качестве подсказки пользователям. Теперь перейдём к JavaScript-коду и нашей сложной сортировке, которая станет проще благодаря jQuery. Приведённый ниже код относится к функции ready(), которую мы изменяли с самого начала статьи. Лучше всего поместить его в конце функции:

// Получим все заголовки столбцов
$('thead th').each(function(column) {
  $(this).addClass('sortable').click(function(){
    var findSortKey = function($cell) {
      return $cell.find('.sort-key').text().toUpperCase() + ' ' + $cell.text().toUpperCase();
    };
    var sortDirection = $(this).is('.sorted-asc') ? -1 : 1;
 
    // Пройдёмся вверх по дереву, чтобы получить строки с данными для сортировки
    var $rows = $(this).parent().parent().parent().find('tbody tr').get();
 
    // Циклом пройдём по всем строкам в поиске данных
    $.each($rows, function(index, row) {
      row.sortKey = findSortKey($(row).children('td').eq(column));
    });
 
    // Сравнение и алфавитная сортировка строк
    $rows.sort(function(a, b) {
        if (a.sortKey < b.sortKey) return -sortDirection;
        if (a.sortKey > b.sortKey) return sortDirection;
        return 0;
    });
 
    // Добавим строки в правильном порядке в начало таблицы
    $.each($rows, function(index, row) {
        $('tbody').append(row);
        row.sortKey = null;
    });
 
    // Определим порядок сортировки столбца
    $('th').removeClass('sorted-asc sorted-desc');
    var $sortHead = $('th').filter(':nth-child(' + (column + 1) + ')');
    sortDirection == 1 ? $sortHead.addClass('sorted-asc') : $sortHead.addClass('sorted-desc');
 
    // Определим столбец, по которому ведётся сортировка
    $('td').removeClass('sorted')
                .filter(':nth-child(' + (column + 1) + ')')
                .addClass('sorted');
 
    $('.visible td').removeClass('odd');
    zebraRows('.visible:even td', 'odd');
  });
});

Да, кода многовато. Давайте-ка разобьём его на небольшие кусочки. Первый кусочек находит все заголовки и проходится по ним циклом. В цикле каждому заголовку добавляется класс sortable и присваивается обработчик нажатия:

...
// Получим все заголовки столбцов
$('thead th').each(function(column) {
  $(this).addClass('sortable').click(function(){
...

Заметьте, что код легко изменить так, чтобы сделать возможным сортировку только по определённым столбцам, достаточно убрать вызов addClass() и поменять селектор с thead th на что-нибудь вроде thead th.sortable. Конечно, вам придётся вручную указывать сортируемые столбцы, добавляя class="sortable" нужным заголовкам в HTML-коде.

Следующий кусочек кода — это объявление функции, присваиваемой переменной. Он может показаться немного странным новичкам в программировании, но, на самом деле, это довольно распространённая практика. Это даст нам возможность легко ссылаться на функцию, в особенности в контексте заголовка, с которым мы работаем. Такое объяснение может показаться несколько запутанным, но точное обоснование выходит за рамки этой статьи. Задача функции findSortKey — определить столбец, по которому ведётся сортировка, мы можем это сделать, поскольку знаем, что индекс этого столбца совпадает с индексом заголовка, по которому щёлкнул пользователь. К примеру, если пользователь щёлкнул на третьем заголовке, нам надо просмотреть третий стобец каждой строки, чтобы определить, в каком порядке они должны идти. После объявления функции мы определяем направление сортировки: по возрастанию или по убыванию. Для этого мы проверяем наличие класса sorted-asc у заголовка таблицы, в случае его наличия, направление сортировки изменится на убывающее, иначе направление уже было убывающим и необходимо сменить его на возрастающее. Этот кусочек кода возвращает 1 или -1, почему — объясним позже.

...
var findSortKey = function($cell) {
  return $cell.find('.sort-key').text().toUpperCase() + ' ' + $cell.text().toUpperCase();
};
var sortDirection = $(this).is('.sorted-asc') ? -1 : 1;
...

Теперь мы хотим получить нужные нам ячейки столбца и поместить их значения в массив, это делается с помощью метода jQuery get(), который помещает все строки в массив, поддерживаемый функцией sort(). Поскольку текущим селектором являлся заголовок таблицы, нам приходится делать три шага назад в DOM-дереве, чтобы найти table>tbody>tr>td. Выглядит немного запутанно, в реальности же это очевидно. После этого мы проходимся циклом по всем только что найденным строкам и определяем столбцы, которые нужно сортировать. Нам нужны те столбцы, чей индекс (порядковый номер, считая от первого столбца таблицы и начиная с нуля) равен индексу нажатого заголовка. Эти столбцы передаются в функцию findSortKey, и мы присваиваем специальному атрибуту sortKey строку, содержащую заголовок столбца и текст текущей ячейки, приведённые к верхнему регистру, чтобы сделать поиск регистронезависимым. (Эта строка возвращается функцией findSortKey, см. её объявление выше. — Прим. пер.) Этим мы упрощаем сортировку, делая её более быстрой на больших объёмах данных.

...
    // Пройдёмся вверх по дереву, чтобы получить строки с данными для сортировки
    var $rows = $(this).parent().parent().parent().find('tbody tr').get();
 
    // Циклом пройдём по всем строкам в поиске данных
    $.each($rows, function(index, row) {
      row.sortKey = findSortKey($(row).children('td').eq(column));
    });
...

Теперь настал черёд самой функции sort(), к которой мы, собственно, и стремились. Она вызывается для массива строк, созданного с помощью функции get(). Единственным передаваемым в неё аргументом является функция, задающая порядок сортировки. Эта функция принимает два параметра для сравнения и возвращает 1, если первый больше, -1, если второй больше и 0, если параметры равны. Здесь используется переменная sortDirection, принимающая значение 1 или -1, которое затем умножается на 1 или -1, в зависимости от результата сравнения. Так реализуется сортировка в нужном нам направлении.

...
// Сравнение и алфавитная сортировка строк
$rows.sort(function(a, b) {
    if (a.sortKey < b.sortKey) return -sortDirection;
    if (a.sortKey > b.sortKey) return sortDirection;
    return 0;
});
...

Следующий кусочек кода просто добавляет каждую строку нового отсортированного массива обратно в DOM-структуру. Это делается с помощью функции append, которая отлично справляется со своей задачей, так как не копирует строку, а удаляет её из текущей позиции и вставляет в нужную — в данном случае, в конец таблицы. После выполнения этого кода все строки окажутся на своих местах. Также, чтобы немного прибраться, удалим ранее установленный атрибут sortKey.

...
// Добавим строки в правильном порядке в начало таблицы
$.each($rows, function(index, row) {
    $('tbody').append(row);
    row.sortKey = null;
});
...

После того, как произведены наиболее трудоёмкие операции, можно привести всё в порядок. Мы получаем все ячейки, удаляя у них объявления классов, затем отфильтровываем ячейки в сортируемом столбце и присваиваем им класс sorted. Это удобно и в CSS, например, если мы хотим, чтобы сортируемый столбец был другого цвета, мы могли бы использовать такой код:

...  
.sorted { background-color: green; }
...

В конце концов, мы удаляем классы odd и применяем чересполосицу, так же, как и в главе про фильтрацию.

...
$('.visible td').removeClass('odd');
zebraRows('.visible:even td', 'odd');
...

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

Сортировка записей в действии

Заключение

В этой статье мы постепенно научились управлять таблицами с помощью jQuery. Это полезно как для нас, так и для наших пользователей. Пользователи получают ожидаемые элементы управления сортировкой и фильтрацией данных, а мы — небольшой и понятный код. Поскольку мы писали его самостоятельно, то можем расширить его функциональность, как пожелаем. Наши методы отлично подходят для простой манипуляции, но если вам нужны продвинутые возможности, рекомендую обратить внимание на плагин jQuery Data Tables. Буду рад ответить на все ваши вопросы в комментариях или в Твиттере. Спасибо за внимание!