Введение
Я рад приветствовать вас, дорогие читатели в третьей статье цикла, посвященного плагину jqGrid. Прошу прощения за столь долгое отсутствие, сейчас все свободное время отдаю изучению и освоению другого монстра – ExtJS.
Напомню, что в предыдущей статье (jqGrid Часть II: Базовые возможности) мы перешли от basic к advanced уровню использования этого замечательного плагина. Но прежде чем мы начнем я хотел бы сообщить, что jqGrid обновилась до версии 3.6.5 и соответственно и мы будем использовать новую версию.

Итак план работ на эту статью:


I.jqGrid и «деревья»

1) Краткие сведения о деревьях
Конечно здесь речь пойдет не о садово-огородных работах, а об представлении древовидных структур данных. В очередной раз обратимся за определением к Википедии:

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

В принципе более или менее сносное определение, несмотря на то, что оно навивает академическую скуку я все же настоятельно рекомендую ознакомиться с материалом по древовидным структурам. Поверьте моему не большому опыту, абсолютно каждый программист рано или поздно станет перед необходимостью использовать дерево, и тут одним из помошников может стать jqGrid. Плагин может работать с двумя самыми распространенными в вычислительной технике типами деревьев: на основе «вложенных множеств» (Nested Sets) и «матрицы смежностей» (adjacency matrix). Вообще говоря тема деревьев очень серьезна, это один из разделов теории графов на изучение которых отводится не одна лекция в ВУЗах, но от себя посоветую те материалы которые мне в свое время очень помогли. Столкнувшись впервые с проблемой хранения деревьев я наткнулся на материал по подходу вложенных множеств (Хранение древовидных структур в Базах данных) и сразу же остановился на нем, наверное потому что для работы с таким деревом существует готовый PHP класс. Но в процессе написания статьи я все же прочел статью Работа с «MySQL. Деревья» в которой рассматривается подход матрицы смежностей.

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

2) jqGrid и SQL деревья
Для того, чтобы jqGrid могла работать с древовидными структурами, она должна быть загружена с модулем Tree Grid. Плагин может работать с обоими типами деревьев, упомянутых в предыдущем пункте. При этом jqGrid может работать с деревьями подгрузив все дерево за один раз или же подгружая ветви и листья по мере необходимости. Собственно загрузку дерева за один раз я рассматривать не буду, не интересно, а вот автоподгрузку мы разберем детально. И на этой ноте давайте перейдем к просмотру ДЕМО3.1, ссылка для скачивания

$(function(){
     $('#table_nested').jqGrid({
                     treeGrid: true,
                     treeGridModel: 'nested',
                     ExpandColumn: 'title',
                     ExpandColClick: true,
                     url: 'p3e1_n.php',
                     datatype: "json",
                     mtype: "POST",
                     colNames:["id","Раздел"],
                     colModel:[
                                   {name:'cid',index:'cid', width:1, hidden:true, key:true},
                                   {name:'title',index:'title', width:180}
                                  ],
                     treeIcons: {plus:'ui-icon-folder-collapsed',minus:'ui-icon-folder-open',leaf:'ui-icon-document'},
                     height: 'auto',
                     caption: 'вложенныe множества',
                     hidegrid: false
     });

     $('#table_adjacency').jqGrid({
                     treeGrid: true,
                     treeGridModel: 'adjacency',
                     ExpandColumn: 'title',
                     ExpandColClick: true,
                     url: 'p3e1_a.php',
                     datatype: "json",
                     mtype: "POST",
                     colNames:["cid","Раздел"],
                     colModel:[
                                   {name:'cid',index:'cid', width:1, hidden:true, key:true},
                                   {name:'title',index:'title', width:180}
                                   ],
                     treeIcons: {plus:'ui-icon-circle-plus',minus:'ui-icon-circle-minus',leaf:'ui-icon-person'},
                     height: 'auto',
                     caption:'матрица смежностей',
                     hidegrid: false
    });
});

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

  • treeGrid: true – самый главный параметр, указывает что jqGrig будет работать с древовидной структурой, а не с простыми табличными данными
  • treeGridModel: ‘adjacency’ / ‘nested’ – это свойство определяет какой алгоритм дерева использовать
  • ExpandColumn: ‘title’ – определяет при клике по какому столбцу происходит открытие и(или) загрузка ветви
  • ExpandColClick: true – необходимо, чтобы открытие ветви происходило при клике на колонку, а не на иконку
  • colModel:[... key:true},...] – свойство key в массиве colModel позволяет указать данные какого столбца следует использовать как уникальный идентификатор записи при запросе данных с сервера. Т.е. благодаря этому свойству, можно переопределить идентификатор. Но только в одном столбце может быть указан key. Плагин будет использовать первый, найденный столбец как ключевой.
  • treeIcons: – позволяет переопределить стандартные ({plus:’ui-icon-triangle-1-e’,minus:’ui-icon-triangle-1-s’,leaf:’ui-icon-radio-off’}) иконки листьев и ветвей.

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

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

В режиме ‘nested’ (на примере метода POST)

  • POST['nodeid'] – идентификатор узла (ветви)
  • POST['n_left'] – позиция узла «слева»
  • POST['n_right'] – позиция узла «справа»
  • POST['n_level'] – уровень вложенности узла

В режиме ‘adjacency’ (на примере метода POST)

  • POST['nodeid'] – идентификатор узла (ветви)
  • POST['parentid'] – идентификатор родительского узла
  • POST['n_level'] – уровень вложенности узла

кроме этого, на сервер также передаются стандартные для jqGrid параметры (на примере метода POST)

  • POST['page'] – номер страницы, для «листалки»
  • POST['rows'] – количество записей, для ограничения при выборке данных
  • POST['sidx'] – значение index: в colModel, имя столбца по которому необходимо отсортировать результат
  • POST['sord'] – направление сортировки
  • После получения данных, серверный скрипт выполняет запрос данных из БД и сроит и выводит результат в следующих форматах.

    В режиме ‘nested’ (на примере XML ответа)

    • пользовательские столбцы
    • level – уровень узла в иерархии
    • lft – позиция узла «слева»
    • rgt – позиция узла «справа»
    • isLeaf – логический признак (true/false), определяет этот узел является «веткой»(есть подузлы) или «листом»(нет подузлов)
    • expanded – логический признак (true/false), нужно ли показывать подузлы если этот узел является веткой, т.е. загрузить «открытым» или «закрытым»

    В режиме ‘adjacency’ (на примере XML ответа)

    • пользовательские столбцы
    • level – уровень узла в иерархии
    • parent – идентификатор родительского узла
    • isLeaf – логический признак (true/false), определяет этот узел является «веткой»(есть подузлы) или «листом»(нет подузлов)
    • expanded – логический признак (true/false), нужно ли показывать подузлы если этот узел является веткой, т.е. загрузить «открытым» или «закрытым»

    Ядро (ридеры) jqGrid разбирают ответ и вставляют данные в таблицу. Вот теперь серверные скрипты будут более-менее понятны.

    Вложенные множества (p3e1_n.php)(опущены подключение к БД и вывод результата)

    // Получаем параметры от таблицы
        # ВНИМАНИЕ!!!
        # Данный код не имеет проверок запрашиваемых данных
        # что может стать причиной взлома! Обязательно проверяйте все данные
        # поступающие от клиента
    
        $node  = intval($_POST['nodeid']);
        $left  = intval($_POST['n_left']);
        $right = intval($_POST['n_right']);
        $level = intval($_POST['n_level']);
    
    // Настраиваем условие для выбора ветки
        $WHERE = '';
        if($node > 0) {
           $level = $level + 1;  // нужно выбрать следующий уровень
           $WHERE = " AND cleft > ".$left." AND cright < ".$right." AND clevel = ".$level;
        }else{
           $level = 0;
        }
    
    // Выбираем подразделы
        $query = "SELECT cid, title, clevel, cleft, cright FROM tree_nested_sets WHERE cleft BETWEEN cleft AND cright ".$WHERE." GROUP BY title ORDER BY cleft;";
        $result = mysql_query($query);
    
    // Формируем результат
        $response->page = 1;
        $response->total = 1;
        $response->records = 1;
    
        $i = 0;
        while($row = mysql_fetch_assoc($result)) {
            //-------------------------------------
            // Определяем является ли выбранный узел "листом"
            if($row['cright'] == $row['cleft']+1)
                $leaf = 'true';
            else
                $leaf='false';
            //-------------------------------------
    
            if($level == $row['clevel']){
                $response->rows[$i]['cell'] = array($row['cid'], $row['title'], $row['clevel'], $row['cleft'], $row['cright'], $leaf, 'false');
            }
            $i++;
        }
    

    Матрица смежностей(p3e1_a.php)(опущены подключение к БД и вывод результата)

    // Выбираем все разделы без подразделов (листья)
        $query_feafs = "SELECT t1.cid FROM tree_adjacency_matrix AS t1 LEFT JOIN tree_adjacency_matrix AS t2 ON t1.cid = t2.id_parent WHERE t2.cid IS NULL";
        $sql_leafs = mysql_query($query_feafs);
    
        while($row = mysql_fetch_assoc($sql_leafs)) {
           $leafs[] = $row['cid']; // Массив для хранения "листьев"
        }
        unset($row);
    
    // Получаем параметры от таблицы
        # ВНИМАНИЕ!!!
        # Данный код не имеет проверок запрашиваемых данных
        # что может стать причиной взлома! Обязательно проверяйте все данные
        # поступающие от клиента
    
        $level  = intval($_POST['n_level']);
        $node   = intval($_POST['nodeid']);
    
    // Настраиваем условие для выбора ветки
        if($node > 0) {
           $level = $level + 1;            // нужно выбрать следующий уровень
           $WHERE = 'id_parent ='.$node;
        } else {
           $WHERE = 'id_parent IS NULL';   // выбрать корень(ни)
        } 
    
    // Выбираем подразделы
        $query  = "SELECT cid, title, id_parent FROM tree_adjacency_matrix WHERE ".$WHERE;
        $result = mysql_query($query);
    
    // Формируем результат
        $response->page      = 1;
        $response->total     = 1;
        $response->records   = 1;
    
        $i = 0;
        while($row = mysql_fetch_assoc($result)) {
           //-------------------------------------
           // Определяем ID родителя этого узла
           if(!$row['id_parent'])
               $parent = 'NULL';
           else
              $parent = $row['id_parent'];
           //-------------------------------------
           // Определяем является ли выбранный узел "листом"
           if(in_array($row['cid'], $leafs))
               $is_leaf = 'true';
           else
               $is_leaf = 'false';
           //--------------------------------------
    
           $response->rows[$i]['cell'] = array($row['cid'], $row['title'], $level, $parent, $is_leaf, FALSE);
    
           $i++;
        }

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

    3)jqGrid и статические деревья
    Название статические деревья оносительно, того что мы делали немного ранее. Безусловно данные «загруженные» таким способом могут быть построены и скриптом динамически, но все же бывают такие задачи, когда дерево уже есть необходимо лишь вывести его в дружественной для пользователя форме как в ДЕМО3.2 . Не правда ли что-то напоминает? Да! это меню из демо jqGrid от разработчика.

    Листинг клиентской части:

      $('#table').jqGrid({
                url: "tree.xml",
                datatype: "xml",
                height: "auto",
                pager: false,
                loadui: "disable",
                colNames: ["id","Items","url"],
                colModel: [
                    {name: "id",width:1,hidden:true, key:true},
                    {name: "menu", width:150, resizable: false, sortable:false},
                    {name: "url",width:1,hidden:true}
                ],
                treeGrid: true,
                        caption: "jqGrid Demos",
                ExpandColumn: "menu",
                width: 200,
                rowNum: 200,
                ExpandColClick: true,
                treeIcons: {leaf:'ui-icon-document-b'}
     });
    

    Полностью идентичные параметры плагина, за исключением того, что я отключил обработчик события onSelectRow.
    Из листига должно стать понятно, что таблица запрашивает статичный(оносительно) файл tree.xml, который выглядит так

    <?xml version='1.0' encoding="utf-8"?>
    <rows>
        <page>1</page>
        <total>1</total>
        <records>1</records>
        <row><cell>1</cell><cell>Loading Data</cell><cell></cell><cell>0</cell><cell>1</cell><cell>10</cell><cell>false</cell><cell>false</cell></row>
        <row><cell>2</cell><cell>XML Data</cell><cell>xmlex.html</cell><cell>1</cell><cell>2</cell><cell>3</cell><cell>true</cell><cell>true</cell></row>
        <row><cell>3</cell><cell>JSON Data</cell><cell>jsonex.html</cell><cell>1</cell><cell>4</cell><cell>5</cell><cell>true</cell><cell>true</cell></row>
    
    ...
    
    </rows>
    

    Т.е. jqGrid просто разбирает данные из этого файла и строит TreeGrid.

    II. Связывание данных в jqGrid

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

    1)Простая подтаблица (subgrid)

    Это весьма полезная опция у этого плагина. На ДЕМО3.3 обратите внимание на крестик в крайнем правом столбце таблицы. При клике на нем под выбранной строкой распахивается «карман» в котором находится небольшая табличка с названиями городов и прочей «рыбой». Вот пример группировки. Теперь давайте перейдем к листингам.

    $('#table').jqGrid({
           url:'p3e3.php',
           datatype: 'json',
           mtype: 'POST',
           colNames:['Код страны','Cтрана',''],
           colModel :[
              {name:'country_code', index:'country_code', width:80, hidden: true},
              {name:'country_name', index:'country_name', width:60},
              {name:'description', width:200, sortable: false}
              ],
           pager: $('#tablePager'),
           rowNum: 100,
           scroll: true,
           viewrecords: true,
           sortname: 'country_name',
           sortorder: 'asc',
           width: 700,
           height: 400,
           subGrid: true,
             subGridUrl: 'p3e3.php?get=subgrid',
             subGridModel:[{
                         name :  ['&nbsp;','Город', 'Широта', 'Долгота'],
                         width:  [40, 200, 150, 150],
                         align:  ['center','left','right','right'],
                         params: ['country_code','country_name']
                     }]
    });
    

    Как видите, абсолютно обыкновенный jqGrid, но в «настройках» есть несколько интересных моментов:

    • subGrid: – в строке 19. Эта опция позволяет использовать подтаблицу.
    • subGridUrl: – указывает путь к скрипту, который должен вернуть данные для построения подтаблицы. В данном случае происходит запрос к тому же скрипту, который «обслуживает» и таблицу-родителя при этом передав ему параметр get со значением subgrid
    • subGridModel: – свойство которое задает параметры подтаблицы. Аналогично colModel таблицы-родителя

    subGridModel: – представляет из себя массив с объектом, у которого есть свои параметры. name, width и align уверен их имена говорят сами за себя, это тексты заголовков столбцов, ширины соответствующих столбцов и выравнивание в них соответственно. Более интересным будет свойство params. Оно позволяет передать значения из каких то ячеек родительской таблицы на сервер при запросе подтаблицы. Иными словами, когда вы нажмете на крестик , открывающий подтаблицу, происходит запрос к серверу которому передается клоч (id) записи (в нашем случае это country_code) и плос к этим данным можно передать дополнительные (в нашем случае серверу будет передан еще и country_name). А вот на сервере получив значения нужных параметров (в этом примере серверный скрипт просто игнорирует значение параметра country_name) вы выполняем запрос к БД и выводим JSON объект с данными.

    ...
    $get = $_REQUEST['get'];  // Параметр указывающий на то
                                        //  что мы запросили информацию для подтаблицы
    ...
    switch ($get){
        case 'subgrid':
            // Выбрать города запрошенной страны
            $id =$_REQUEST['id'];
    
            // Запрос данных
            $query = "SELECT id, country_code, region_code, city, latitude, longitude, nbip FROM cities  WHERE country_code LIKE '".$id."'";
    
            $result = mysql_query($query);
            if(mysql_num_rows($result) >= 1){
                // Строки данных для таблицы
                $i = 0;
                while($row = mysql_fetch_assoc($result)) {
                    $data->rows[$i]['cell'] = array($i+1,'<strong><em>'.$row['city'].'</em></strong>',$row['latitude'],$row['longitude']);
                    $i++;
                }
            }
            break;
    
        default:
            // Выбрать названия стран и сосчетать города
            ...
    
    }
    // Вывести результат
    ...
    

    Серверный скрипт особо ничем не отличается от рассмотренных в предыдущих примерах. При этом он разбит на две части оператором switch. Который по умолчанию выполняет «стандартную» обработку и вывод данных для главной таблицы. А вот если этот скрипт запустить передав ему параметр get (я выбрал такое имя только ради наглядности) тогда будет выполнена выборка данных для подтаблицы. Ну и если у вас чешутся руки поковырять пример, вы можете скачать его.

    Подтаблицы это замечательный инструмент, который позволяет очень элегантно организавать данные, но при этом имеет один существенный недостаток – подтаблица «не живая». Т.е. данные в подтаблице не могут сортироваться, нет возморжности реализовать поиск и т.д. А это очень критично если в подтаблице будет выводиться много записей. Однако это ограничение отпадает само собой при использовании jqGrid как подтаблицы.

    2)jqGrid как subgrid
    Итак начнем с промотра ДЕМО3.4. Как видите все тот же «родной» jqGrid с названиями стран, и кол-вом городов в этой стране, но при этом подтаблица теперь представляет из себя самую настоящую jqGrid со всеми свойствами и методами. Согласитесь, что так работать намного приятнее и удобнее.

    А вот здесь самое интересное – клиентский код (в листинге опущены все настройки родительской таблицы, кроме одного свойства):

    ...
    subGrid: true,
    subGridRowExpanded: function(subgrid_id, row_id) {
        var subgrid_table_id;
        subgrid_table_id = subgrid_id+'_t';
    
        $('#'+subgrid_id).html('<table id="'+subgrid_table_id+'"></table><div id="'+subgrid_table_id+'_pager"></div>');
        $('#'+subgrid_table_id).jqGrid({
            url: 'p3e4.php',
            datatype: 'json',
            mtype: 'POST',
            postData: {'get':'subgrid', 'id':row_id},
            colNames: ['Город', 'Широта', 'Долгота'],
            colModel: [
                {name: 'city', index: 'city', width:130},
                {name: 'latitude', index: 'latitude', width:80, align: 'right'},
                {name: 'longitude', index: 'longitude', width:80, align: 'right'}
                ],
            height: 'auto',
            autowidth: true,
            rownumbers: true,
            rownumWidth: 40,
            rowNum: 10,
            sortname: 'city',
            sortorder: 'asc',
            pager: $('#'+subgrid_table_id+'_pager'),
            rowNum:10,
            rowList:[10,20,50,100]
        });
    }
    ...
    

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

    subGridRowExpanded – это событие возникает когда пользователь кликает на крестик раскрытия подтаблицы и выполняет определенную функцию при распахивании «кармана» для подтаблицы. При этом в функцию обработчик будут переданы 2 параметра:

    • pID – уникальный идентификатор «кармана» (контейнера, если хотите) который открывается под выбранной строкой
    • id – идентификатор записи под которой открылся «карман»

    Безусловно существуют еще события, о которых вы можете узнать из документации. А мы пока пробежимся по листингу. Итак когда пользователь «тыцнул» на крестик, открывается контейнер и выполняется функция. Которая получает 2 параметра subgrid_id и row_id. Далее в строке 5 создаем строковое значение для атрибута id и в 6й строке используем метод jQuery, который вставляет в «карман» (кстати сказать что это обыкновенный DIV) html разметку таблицы и DIV’a. А далее идет бональная инициализация jqGrid для только что созданной разметки. Т.е. фактически мы просто напросто динамически создали разметку и создали jqGrid! В этот момент я воскликнул «Тааак!» и потер руки. Почему? Да потому, что зная id объекта (<div id="someid">…</div>) с помощью jQuery можно сотворить с ним все что угодно. Но прежде чем мы перейдем к следующему примеру я приведу ссылку на раздаточный материал по теме – это пример который только что расмотрели.
    А теперь зная id контейнера давайте, еще добавим наглядности нашему примеру со странами и городами, например как на ДЕМО3.5. Как видите все эта же, скучная таблица со странами, но теперь при клике на крестик открывается подробная информация и флаг выбранной строки. Под которой свернутая jqGrid таблица с перечнем городов. Этот пример вы можете скачать перейдя по этой ссылке. Внутри архива все скрипты, но наибольший интерес представляет клиентский код. Я условно разделю его на 2 части, первая (нижняя) часть:

    $('#table').jqGrid({
              ...
              subGrid: true,
              subGridRowExpanded: function(subgrid_id, row_id) {
                           getCountryDetails(subgrid_id, row_id);
                       }
    
    });
    

    Это таблица из предыдущего примера, но теперь при возникновении события subGridRowExpanded будет выполнена пользовательская функция getCountryDetails(), которая и является 2 половиной клиентского скрипта:

    function getCountryDetails(container, countryCode){
        $.ajax({
                type: 'post',
                url: 'p3e5.php',
                data: ({'id': countryCode, 'get': 'details'}),
                dataType: 'json',
                success: function(server){
                    $('#'+container).html('<div class="c_detail"><img src="files/images/'+server.flag+'"><ul><li><strong>Столица:</strong> '+server.capital+'</li><li><strong>Дата независимости:</strong> '+server.independence_date+'</li><li><strong>Официальные языки:</strong> '+server.off_lng+'</li><li><strong>Форма правления:</strong> '+server.government+'</li><li><strong>Глава правительства:</strong> '+server.president+'</li><li><strong>Валюта:</strong> '+server.currency+'</li><li><strong>Интернет-домены:</strong> '+server.domains+'</li><li><strong>Телефонный код:</strong> '+server.dialing_code+'</li><li><strong>Часовой пояс:</strong> '+server.time_zone+'</li></ul></div>');
    
                    var subgrid_table_id = container+'_t';
    
                    $('#'+container).append('<table id="'+subgrid_table_id+'"></table><div id="'+subgrid_table_id+'_pager"></div>');
                    $('#'+subgrid_table_id).jqGrid({
                              caption: 'Города',
                              hiddengrid: true,
                              url: 'p3e4.php',
                              datatype: 'json',
                              mtype: 'POST',
                              postData: {'get':'subgrid', 'id':countryCode},
                              colNames: ['Город', 'Широта', 'Долгота'],
                              colModel: [
                                {name: 'city', index: 'city', width:130},
                                {name: 'latitude', index: 'latitude', width:80, align: 'right'},
                                {name: 'longitude', index: 'longitude', width:80, align: 'right'}
                              ],
                              height: 'auto',
                              autowidth: true,
                              rownumbers: true,
                              rownumWidth: 40,
                              rowNum: 10,
                              sortname: 'city',
                              sortorder: 'asc',
                              pager: $('#'+subgrid_table_id+'_pager'),
                              rowNum:10,
                              rowList:[10,20,50,100]
                    });
                }
        });
    }
    

    В теле этой функции выполняется AJAX запрос, передающий серверному скрипту 2 параметра методом POST:

    • id – идентификатор записи, в нашем случае это сокращенный код страны
    • get – этот параметр указывает, что мы хотим получить с сервера детальную информацию по выбранной стране

    После того как запрос будет успешно выполнен (см. строку 8), в открывшийся под выбранной строкой контейнер будет помещена какая-то HTML разметка с полученными с сервера данными. В данном примере такая разметка представлена не нумированным списком в картинкой – флагом страны. После этого в этот же контейнер помещается разметка для дочерней таблицы и производится инициализация дочерней таблицы.

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

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

    3)Ведущая и ведомая jqGrid

    Однако не всегда можно сгруппировать данные и засунуть их в подтаблицу. Иногда данные могут быть полностью самостоятельными и при этом иметь некую связь. Например однажды передо мной стала задача вывести пользователю перечень счетов и платежей по этим счетам, и при этом обеспечить возможность просмотра счетов, платежей и платежей по выбранному счету. И тут то мне очень помогла методика «ведущий-ведомый». Работа которой проиллюстрирована в ДЕМО5.6.

    Имеется две jqGrid, отображающие страны и города, которые можно менять местами благодаря плагину sortable. Также в верхней части документа есть checkbox который включает функцию фильтрации содержимого ведомой (нижней) таблицы, на основе выбранной записи в ведущей (верхней) таблице. При этом вся фильтровка осуществляется на стороне сервера, с этого и начнем разбор примера. Для удобства две таблицы запрашивают данные у двух отдельных серверных скриптов, один для городов, другой для стран.
    Скрипт для таблицы «Города» (т.к. основная структура скрипта «стандартна» я привожу лишь основные моменты):

    // Получение параметров page, rows и т.д.
    ...
    if($_REQUEST['filterBy'] != 'null')  // Фильтр
        $WHERE = " WHERE country_code = '".$_REQUEST['filterBy']."' ";
    else
        $WHERE = '';
    ...
    // Запрос подсчета суммарного кол-ва записей
    $result = mysql_query("SELECT COUNT(*)AS count FROM cities".$WHERE);
    ...
    // Запрос выборки данных
    $result = mysql_query("SELECT city, latitude, longitude FROM cities ".$WHERE." ORDER BY ".$sidx." ".$sord." LIMIT ".$start.", ".$limit);
    

    В данном случае клиентский код при запросе данных с сервера помимо «стандартных» параметров, отправляет еще и дополнительный параметр filterBy, который по сути и определяет критерий по которому нужно выбрать записи из БД и вернуть клиенту. Что и проиллюстрировано в последующих запросах.

    Скрипт для таблицы «Страны»:

    // Получение параметров page, rows и т.д.
    ...
    if($_REQUEST['filterBy'] != 'null'){
         $country = @mysql_fetch_array(mysql_query("SELECT country_code FROM cities WHERE id = ".$_REQUEST['filterBy']));
         $WHERE = " WHERE country_code = '".$country['country_code']."' ";
    }else{
         $WHERE = '';
    }
    ...
    // Запрос подсчета суммарного кол-ва записей
    $result = mysql_query("SELECT COUNT(*)AS count FROM countries ".$WHERE);
    ...
    // Запрос выборки данных
    $result = mysql_query("SELECT * FROM countries ".$WHERE." ORDER BY ".$sidx." ".$sord." LIMIT ".$start.", ".$limit);
    

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

    <input type="checkbox" id="link">
    <label for="link">Связать таблицы</label>
    
    <div id="interfaceBody" style="margin: 10px 10px 10px 10px; padding: 0px 10px 10px 10px; border: 2px solid gray">
    <div class="grid" id="countries" style="padding: 10px 10px 10px 10px; margin-top: 10px; border: 2px solid red">
        <table id="table_countries"></table>
        <div id="table_countriesPager"></div>
    </div>
    <div class="grid" id="cities" style="padding: 10px 10px 10px 10px; margin-top: 10px; border: 2px solid green">
        <table id="table_cities"></table>
        <div id="table_citiesPager"></div>
    </div>
    </div>
    

    Если не обращать внимание на ужасно длинные inline-стили, то разметка достаточно проста. Сначала идет код checkbox’a с меткой, а следом контейнер всего интерфейса, внутри которого два дочерних контейнера, по одному для каждой таблицы.

    И наконец клиентский скрипт:

    //-------------------------------------------------------------------
    // Функция-обработчик
    function stickTogether(){
        if($('#link').is(':checked')){
            // Флаг "Связать таблицы" утановлен
            var masterId        = $('#interfaceBody > DIV:first').attr('id');                //Определяем ID контейнера "ведущей таблицы"
            var masterPostData  = $('#table_'+masterId).jqGrid('getGridParam','postData');   //Получаем весь объект postData "ведущей таблицы"
            var selId    = $('#table_'+masterId).jqGrid('getGridParam','selrow');            //Получаем id записи в выделенной строке
                                                                                             //не путайте с rowIndex
            // Проверяем какое текущее значение фильтра ведущей таблицы, он должен быть сброшен (например null)
            if(masterPostData.filterBy){
                $('#table_'+masterId).jqGrid('setGridParam',{'postData':{'filterBy':null}}); //Сбрасываем значение фильтра
                $('#table_'+masterId).trigger('reloadGrid');                                 //Перезагружаем данные в таблицу
            }
    
            var slaveId  = $('#interfaceBody > DIV:last').attr('id');                        //Определяем ID контейнера "ведомой таблицы"
            $('#table_'+slaveId).jqGrid('setGridParam',{'postData':{'filterBy':selId}});     //Применяем в качестве фильтра ведомой таблицы
                                                                                             //id выбранной записи в ведущей таблице
            $('#table_'+slaveId).trigger('reloadGrid');                                      //Перезагружаем данные в таблицу
        }else{
            // Флаг "Связать таблицы" не утановлен
            var coutriesT       = $('#table_countries');
            var countriesPost   = coutriesT.jqGrid('getGridParam','postData');
    
            if(countriesPost.filterBy){ //Сбрасываем фильтр т.к. ничего не нужно фильтровать
                coutriesT.jqGrid('setGridParam',{'postData':{'filterBy':null}})
                coutriesT.trigger('reloadGrid');
            }
            //-----------------------
            var citiesT       = $('#table_cities');
            var citiesPost    = citiesT.jqGrid('getGridParam','postData');
    
            if(citiesPost.filterBy){ //Сбрасываем фильтр т.к. ничего не нужно фильтровать
                citiesT.jqGrid('setGridParam',{'postData':{'filterBy':null}})
                citiesT.trigger('reloadGrid');
            }
        }
    }
    //-------------------------------------------------------------------
    // Настройка плагинов
    var tcountries = $('#table_countries').jqGrid({
                              caption: 'Страны',
                              hidegrid: false,
                              url:'p3e6_countries.php',
                              postData: {'filterBy':null},
                              datatype: 'json',
                              mtype: 'POST',
                              colNames:['Код страны','Cтрана','Столица','Домены'],
                              colModel :[
                                        {name:'country_code', index:'country_code', width:80, hidden: true},
                                        {name:'country_name', index:'country_name', width:60},
                                        {name:'capital', index:'capital', width:60},
                                        {name:'domains', index:'domains', width:60}
                                        ],
                              pager: $('#table_countriesPager'),
                              rowNum: 100,
                              scroll: true,
                              viewrecords: true,
                              sortname: 'country_name',
                              sortorder: 'asc',
                              height: 200,
                              autowidth: true,
                              onSelectRow: stickTogether
                    });
    
    var tcities = $('#table_cities').jqGrid({
                              caption: 'Города',
                              hidegrid: false,
                              url: 'p3e6_cities.php',
                              postData: {'filterBy':null},
                              datatype: 'json',
                              mtype: 'POST',
                              colNames: ['Город', 'Широта', 'Долгота'],
                              colModel: [
                                {name: 'city', index: 'city', width:130},
                                {name: 'latitude', index: 'latitude', width:80, align: 'right'},
                                {name: 'longitude', index: 'longitude', width:80, align: 'right'}
                              ],
                              height: 200,
                              autowidth: true,
                              rownumbers: true,
                              rownumWidth: 40,
                              rowNum: 100,
                              sortname: 'city',
                              sortorder: 'asc',
                              pager: $('#table_citiesPager'),
                              scroll: true,
                              viewrecords: true,
                              onSelectRow: stickTogether
                    });
    
    $("#interfaceBody").sortable({
            connectWith: '.grid',
            placeholder: 'ui-state-error',
            opacity: 0.7,
            stop: stickTogether
    });
    
    //-------------------------------------------------------------------
    // Назначаем обработчик на событие
    $('#link').change(stickTogether);
    

    При всем своем объеме код достаточно груб и прост. Начнем пожалуй с назначения обработчика на изменение checkbox’a, самая последняя строка в листинге. Как видите при изменении checkbox’a будет вызвана функция stickTogether. В теле которой происходит проверка состояния флажка и если он не установлен, то произойдет сброс всех «фильтров» таблиц и перезагрузка данных в таблице.
    Но самое интересное это код в «положительной» ветке if’а, когда флажок установлен. Мы знаем, что ведущая таблица расположена вверху, значит для начала узнаем какая же таблица в данный момент находится там. Для этого получаем значение атрибута id первого контейнера (строка 6). Теперь зная какая таблица ведущая – получаем объект postData, который, напомню, содержит все параметры и их значения для отправки серверу. В 7й строке получим Id записи в выделенной строке ведущей таблицы. Далее проверив значение фильтра ведущей таблицы, при необходимости сбросим его. Это необходимо если пользователь поменял таблицы местами.
    В строке 16 узнаем id контейнера ведомой таблицы. И применяем фильтр, записав id выделенной строки в ведущей таблице, и перезагружаем данные.
    У данного скрипта есть один недостаток – расплата за универсальность. Из-за того что на событие onSelectRow: в обоих таблицах определен один и тот же обработчик stickTogether, при выделении строки в ведомой таблице, происходит постоянное обновление ее записей. Но уверен что те кому потребуется такой функционал jqGrid без труда смогут это исправить.

    Заключение
    Подведя итоги проделанной работы можно сказать, что в данной статье мы рассмотрели практически все варианты расширенного использования плагина. Научились работать с древовидными данными и подтаблицами, а также попробовали управлять содержимым ведомой таблицы посредством данных из ведущей. В следующей части я рассмотрю еще одну экзотическую интересную опцию jqGrid – «drag’n'drop записей между jqGrid’ами».
    Ну и как всегда если у вас возникли вопросы задавайте их на форуме ну а комментарии используйте для предложений и пожеланий.

    Итак приблизительный план работ на следующий выпуск:

    • Расширенные возможности jqGrid
      • drag’n'drop записей между jqGrid’ами
    • Поиск данных в jqGrid
      • «inline» поиск
      • поиск с использованием встроенной формы
      • расширенный поиск с использованием встроенной формы
      • поиск с использованием внешней формы
    Поделиться в FaceBookПоделиться ВКонтактеДобавить в TwitterПоделиться в Моём МиреСохранить закладку в GoogleОтправить в Живую ленту GoogleДобавить в Яндекс.ЗакладкиПоделиться в ОдноклассникахОпубликовать в LiveJournal