Робота з даними - ExtJS та Zend Framework.

Зараз, під час епідемії грипу, з'явилося трохи більше часу, і вирішив його використати для написаня статтей по програмуванню. Відразу перепрошую за оформлення - все ж намагався зробити максимально зручним для читання.
В цій статті я опишу роботу з компонентами JavaScript фреймворку ExtJS та php Zend Framework на прикладі редагування деякого контента в таблиці БД.
Для редагування списку елементів контенту будемо використовувати компонент ExtJS GridPanel, для окремого екземпляру - FormPanel.
Огляд розрахований на людей які знають основи роботи з вказаними фреймоворками, тобто не буду зупинятися на деяких подробицях, як наприклад створення з'єднання з БД, написання завантажувача, тощо.
Для розробки використовував ExtJS 3.0, Zend Framework 1.8.1.

БД

Для початку створимо таблицю в БД.
CREATE TABLE `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255),
  `shorttext` text,
  `createdate` datetime,
  `text` text,
  PRIMARY KEY (`id`)
);
Це може бути простою стрічкою новин, в таблиці передбачені поля - назва, дата, короткий та повний текст.

GridPanel

Для початку розробимо інтерфейс для роботи з данними за допомогою JavaScript.
Створюємо нову html сторінку. Підключаємо файли стилів та бібліотеки ExtJS.
var record = Ext.data.Record.create([
	{name: 'id', type: 'int'},
	{name: 'title'},
	{name: 'createdate', type: 'date', dateFormat: 'Y-m-d h:i:s'},
]);
var ds = new Ext.data.Store({
    remoteSort: false,
    proxy: new Ext.data.HttpProxy({
  	url: '/grid/grid/'
    }),
    reader: new Ext.data.JsonReader({
  	root: 'result',
	totalProperty: 'totalCount'
    }, record)
});
paddingBar = new Ext.PagingToolbar({
    pageSize: 10,
    store: ds,
    displayInfo: true,
    displayMsg: 'Displaying topics {0} - {1} of {2}',
    emptyMsg: "No topics to display"
});
var renderDate = function(value, p, record){
    return value.format("j/n/Y H:i:s");
};
var grid = new Ext.grid.GridPanel({
    store: ds,
    trackMouseOver:false,
    loadMask: true,
    columns:[{
	id: 'id',
	header: "Id",
	dataIndex: 'id',
	width: 40,
	sortable: true
    },{
	header: "Title",
	dataIndex: 'title',
	width: 300,
	sortable: true
    },{
	header: "Createdate",
	dataIndex: 'createdate',
	width: 200,
	renderer: renderDate,
	sortable: true
    }],
    tbar: [
	new Ext.Button({
            text: 'New',
	    handler: addRecord
	}),
	new Ext.Button({
	    text: 'Edit',
	    handler: editRecord
	}),
	new Ext.Button({
	    text: 'Delete',
	    handler: deleteRecords
	}),
    ],
    bbar: paddingBar
});
grid.on("rowdblclick", editRecord);
var window = new Ext.Window({
    id: 'example-window',
    title : "Grid Example",
    layout: 'fit',
    width : 800,
    height : 400,
    items: [ grid ]
});
window.show();
grid.getStore().load();
Пояснення до коду.

Створюємо запис з визначенням данних. Назви і формати полів повинні відповідати полям у таблиці БД.
Вказуємо url контроллера, що буде завантажувати данні (/grid/grid/). Дані будуть завантажуватися в форматі JSON, в result будуть знаходитися записи таблиці. Для можливості навігації по сторінках, в totalCount потрібно завантажити загальну кількість записів. Для посторінкової навігації в бібліотеках ExtJS передбачений компонент Ext.PagingToolbar. У ньому вкажемо, що на сторінці буде виводитися 10 записів.
Напишемо рендер для дати - будемо відображати дату у більш зручному для перегляду форматі.
У тулбарі визначаємо кнопки для додавання, редагування та видалення контенту.
Додав ще обробник для подвійного кліку на запис у таблиці. Незручно виділяти контент, а потім тиснути на кнопку, для його редагування.
Виведемо нашу таблицю не просто на сторінці, а у вікні. В завершенні - завантажуємо дані у таблицю.

Отже, даний код виводить на сторінці вікно, у якому буде розміщена таблиця.

GridController

Далі перейдемо до php і почнемо створювати контролер, який буде взаємодіяти з клієнтською частиною.
class GridController extends Zend_Controller_Action
{
...
}
Створимо модель даних, що буде взаємодіяти з БД і виконувати операції з данними.
class Grid extends Zend_Db_Table_Abstract
{
    public function getCountRows()
    {
        $select = $this->select()->from(array('p' => $this->_name),array('c' => 'COUNT(*)'));
        $stmt = $select->query();
        $result = $stmt->fetchAll();
        return $result[0][0];
    }
}
Модель наслідує клас Zend_Db_Table_Abstract і у ній я дописав один метод, що буде виводити загальну кількість записів (не знайшов у фреймворку стандартної функції, яка б це робила).

Створюємо метод для контроллера Grid, що буде завантажувати данні в таблицю.
public function gridAction()
{
        $this->_helper->viewRenderer->setNoRender();
        require_once 'Grid.php';
        $grid = new Grid(array('name' => 'articles'));
        $totalCount = $grid->getCountRows();
        $where = null;
        $order = "id";
        $limit = $this->getRequest()->getParam('limit', 5);
        $start = $this->getRequest()->getParam('start', 0);
        $rows = $grid->fetchAll($where, $order, $limit, $start);
        $data = array(
            'totalCount' => $totalCount,
            'result' => $rows->toArray()
        );
        echo json_encode($data);
}
У методі знаходимо загальну кількість записів та вибираємо сторінку записів. При посторінковій навігації також у якості параметрів будуть передаватися значення start та limit. Формуємо масив з загальною кількістю елементів та данними і конвертуємо його у формат JSON.

FormPanel

Переходимо до клієнтської частини і JavaScript. Для початку створюємо два обробника - для додавання та редагування сторінки.
addRecord = function() {
    recordEditDialog('new');
};
editRecord = function() {
    var sm = grid.getSelectionModel();
    if (!(row = sm.getSelected())){
        alert('Record not selected');
        return false;
    }
    recordEditDialog(row.id);
};
addRecord - містить функцію recordEditDialog(), що додає новий запис у таблицю, у якості параметра передається 'new'.
editRecord - визначає виділений запис у таблиці і передає його id у функцію recordEditDialog().
recordEditDialog = function(id) {
    var form = new Ext.form.FormPanel({
    autoScroll: true,
    bodyStyle:'padding:5px',
    reader : new Ext.data.JsonReader({
        root: "data", id: "id"
    }, [
        {name: 'id',  type: 'int'},
        'title', 'createdate', 'shorttext', 'text'
    ]),
    defaultType: 'textfield',
    items: [{
        fieldLabel: 'Name',
        name: 'title',
        allowBlank:false,
        anchor:'98%'
    },{
        fieldLabel: 'Createdate',
        name: 'createdate',
        xtype:'datefield',
        format: 'Y-m-d h:i:s',
    },{
        xtype:'textarea',
        fieldLabel: 'Short text',
        name: 'shorttext',
        anchor:'98%'
    },{
        xtype:'htmleditor',
        fieldLabel: 'Text',
        name: 'text',
        anchor: '98%',
        height: 260
    }
]
});
if (id != 'new') {
    form.getForm().load({
    url:'/grid/form/',
    params: {
        id: id
    }
});
}
var editWindow = new Ext.Window({
    title: (id == 'new' ? 'Add record' : 'Edit Record'),
    width:700,
    height:500,
    modal: true,
    maximizable: true,
    buttonAlign: 'center',
    layout: 'fit',
    items: [ form ],
    buttons: [{
        text: 'OK',
        handler: function() {
            if (form.getForm().isValid()) {
                saveRecord(id);
                editWindow.close();
            }
        }
    },{
        text: 'Cancel',
        handler: function() {
            editWindow.close();
        }
    }]
});
editWindow.show();
saveRecord = function(id){
    data = form.getForm().getValues();
    var record = new Object;
    record.id = id;
    record.values = data;
    var dataCode = Ext.util.JSON.encode(record);
    Ext.Ajax.request({
        url: '/grid/save/',
        params: { data: dataCode },
        success: function ( options, success, response ) {
            grid.getStore().load();
        },
        failure: function (result, request) {
        }
    });
};
};
Для редагування екземпляра контента створюємо форму на основі компоненту Ext.form.FormPanel. Визначимо JsonReader який буде завантажувати данні у форму, вказуємо всі поля запису та опишемо всі поля форми.
Якщо контент редагується, а не створюється новий, то завантажуємо його у форму. В url вказуємо контроллер, що буде завантажувати запис (/grid/form).
Створюємо вікно, у якому буде виводитися форма. Зробимо його модальним, щоб під час редагування не було доступу до таблиці. У вікні задамо дві кнопки, 'OK' - зберігає запис і закриває вікно, 'Cancel' - відміна редагування.

saveRecord - функція, що зберігає дані форми.
Вибираємо значення всіх полів, та конвертуємо їх у формат JSON. Туди ж записуємо id. Якщо id буде 'new', то запис буде додаватися, інакше оновлюватися. Передавання JSON я використовую тому, що на сервері зручніше обробляти один об'єкт, ніж коли кожне поле прийде у POST окремо.
В url прописуємо метод контроллера, що буде обробляти запит. З допомогою AJAX-запиту відправляємо данні на сервер. При отриманні успішної відповіді таблиця перезавантажується.

GridController, робота з формою

Повертаємося до нашого контроллера і допишемо два метода - для завантаження та для редагування форми.
public function formAction()
{
    $this->_helper->viewRenderer->setNoRender();
    $id = $this->getRequest()->getParam('id', 0);
    if ($id) {
        require_once 'Grid.php';
        $grid = new Grid();
        $where = array('id = ?' => $id);
        $row = $grid->fetchRow($where);
        $row = $row->toArray();
        $data = array(
            'data' => array($row)
        );
        echo json_encode($data);
    } else echo "{success: false}";
}
formAction() - завантажує у форму запис з отриманим параметром id. В принципі все просто - виконується запит до БД і результат заноситься у масив $data та виводиться у форматі JSON.
public function saveAction()
{
    $this->_helper->viewRenderer->setNoRender();
    $data = $this->getRequest()->getParam('data', 0);
    if ($data) {
        $data = json_decode(stripslashes($data), 1);
        $id = $data['id'];
        $data = $data['values'];
        require_once 'Grid.php';
        $grid = new Grid();
        if($id == 'new') {
            $grid->insert($data);
        } else {
            $where = array('id = ?' => $id);
            $grid->update($data, $where);
        }
        echo "{success: true}";
    } else echo "{success: false}";
}
saveAction() - додає або редагує запис.
В якості параметру приймає данні у форматі JSON, що конвертуються у звичайний масив. Якщо id = 'new' - додаємо данні до БД.
Інакше виконуємо оновлення данних по заданому id.

Видалення данних

Залишилася ще одна операція - видалення данних.

Для початку напишемо обробник на JavaScript.
deleteRecords = function(){
    var sm = grid.getSelectionModel();
    if (!sm.getSelected()){
        alert('Record not selected');
        return false;
    }
var rows = sm.getSelections();
if (rows.length >= 1){
    data = [];
    for(var i = 0; i < rows.length; ++i){
        row = rows[i];
        data.push(row.data.id);
    }
    var dataCode = Ext.util.JSON.encode(data);
    Ext.Ajax.request({
        url: '/grid/delete/',
        params: { data: dataCode, classname: '' },
        success: function ( options, success, response ) {
            success = Ext.util.JSON.decode(options.responseText).success;
            if (success) {
                grid.getStore().load();
            }
        },
        failure: function (result, request) {
        }
    });
}
};
Спочатку вибираємо id виділенних записів. Видалити можна відразу декілька записів.
Конвертуємо їх у JSON.
Виконуємо AJAX-запит, в url вказуємо метод контроллера, що буде видаляти записи (/grid/delete/).
В разі успішного виконання запиту оновлюємо таблицю.

І підсумки

В даному огляді я розглянув тільки основні функції роботи з данними. Насправді в реальній роботі все набагато складніше. Необхідно сортувати записи, працювати з підтримкою мультимовності, редагувати окремі поля відразу в таблиці, фільтрувати записи та інше. Окремий функціонал потрібно також додати, якщо необхідно розділити права користувачів на редагування. Якщо таблиць для редагування декілька, то зручніше написати свій компонент, який вже буде вміщувати в собі всі ці функції, і використовувати його у роботі.

коментарі:

webdevbyjoss 05.11.2009 22:40
ух... ледве до кінця дочитав
але було цікаво бо давно заглядаю в бік ExtJS але якось він мене все відлякує відсутністю часу то все вивчити
sky 05.11.2009 23:13
в принципі, документації і прикладів по ньому є достатньо)
webdevbyjoss 05.11.2009 23:25
так, я писав про брак часу.
досить таки громадна машина, і його або грунтовно розібратися і писати на ньому щось серйозне або ні
не можна як jQuery - трішки підюзати... хоча може я і помиляюсь
Enetri 06.11.2009 07:50
Будь ласка, трохи косметичної правки: дещо зменшіть ілюстрації (щоб уникнути скролингу), застрільте крапки в заголовках та зайві брейки..

І вітаємо з успішним почином :)
+1sky 06.11.2009 10:01
ok
+1-=0m3r=- 06.11.2009 08:02
Чудова статья, шкода що її раніше не було.
ЗІ: кілька дрібних ремарок
1) не echo json_encode($data); а $this->_helper->json->sendJson($data) потім можна буде хелпер перевизначити.
2) url: '/grid/delete/', ===> url: baseUrl + '/grid/delete/',

3) беру так var selectedItems = grid.getSelectionModel().selections.items;
for (var i = 0; i < selectedItems.length; i++) {data[i] = selectedItems[i].id;}
...
params: {data: Ext.encode(data)},
...
Влад 06.11.2009 11:02
ще би сурс-код і було би просто казка

додати коментар: