Tổng Quan Về GridPanel
Component Ext.grid.GridPanel là lớp trừu tượng chính để biểu diễn dữ liệu dạng lưới trong thư viện ExtJS. Thừa kế từ Ext.Panel, nó nhận diện qua thuộc tính xtype: 'grid'. Một cấu trúc bảng hoàn chỉnh luôn yêu cầu hai khối phụ thuộc: mô hình cột (để định nghĩa khung hiển thị) và bộ quản lý dữ liệu (data store) để bao gói nguồn thông tin đầu vào.
Khởi Tạo Cấu Trúc Cột Và Nguồn Dữ Liệu
Các cột được khai báo dưới dạng mảng JSON, mỗi mục ánh xạ sang một trường hiển thị trên giao diện. Trường bắt buộc nhất là header (tiêu đề) và dataIndex (khóa liên kết với bản ghi).
const danhSachCot = [
{ header: 'Mã Định Danh', dataIndex: 'maDD', sortable: true },
{ header: 'Tiêu Đề Nội Dung', dataIndex: 'tieude' },
{ header: 'Trạng Thái Cập Nhật', dataIndex: 'trangThai' }
];
Nguồn dữ liệu thô thường được chuyển hóa qua Ext.data.Store. Ví dụ với mảng song cấp:
const duLieuNhapLieu = [
['D01', 'Báo cáo tháng 1', 'Đã duyệt'],
['D02', 'Báo cáo tháng 2', 'Chờ phê duyệt'],
['D03', 'Tổng kết năm', 'Hoàn tất']
];
const luuTruDulieu = new Ext.data.ArrayStore({
data: duLieuNhapLieu,
fields: ['maDD', 'tieude', 'trangThai']
});
luuTruDulieu.load();
Ghép nối chúng vào thể hiện GridPanel:
const bienGrid = new Ext.grid.GridPanel({
renderTo: 'divNoiBat',
autoHeight: true,
forceFit: true,
store: luuTruDulieu,
columns: danhSachCot
});
Tùy Biến Hành Vi Và Giao Diện
enableColumnMove: Bật/tắt khả năng kéo thả đổi vị trí cột (truemặc định).enableColumnResize: Cho phép kéo biên để chỉnh chiều rộng cột.stripeRows: Hiệu ứng xen kẽ màu nền dòng, cài đặt trongviewConfig.loadMask: Overlay chặn tương tác khi đang fetch dữ liệu, cũng đặt trongviewConfig.
Để điền đầy đủ chiều rộng vùng chứa, bỏ giá trị width cố định trên từng cột và kích hoạt forceFit: true tại GridPanel. Framework sẽ tự động phân bổ tỷ lệ dựa trên độ ưu tiên nội dung.
Xử Lý Sắp Xếp Và Định Dạng Ngày Tháng
Kích hoạt sắp xếp tại cột bằng sortable: true. Để hỗ trợ chuẩn ngữ âm tiếng Việt, cần ghi đè phương thức so sánh gốc của Store trước khi framework khởi tạo:
Ext.data.Store.prototype.applySort = function() {
if (this.sortInfo && !this.remoteSort) {
var s = this.sortInfo, f = s.field;
var st = this.fields.get(f).sortType;
var fn = function(r1, r2) {
var v1 = st(r1.data[f]), v2 = st(r2.data[f]);
return typeof(v1) === 'string' ? v1.localeCompare(v2) : (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0));
};
this.data.sort(s.direction, fn);
}
};
Với trường ngày giờ, khai báo type: 'date' trong mô hình trường của Store, kết hợp renderer tại cột để xuất chuỗi ký tự theo template mong muốn:
// Khai báo trường
{ name: 'thoiGianTao', type: 'date', dateFormat: 'Y-m-dTH:i:s' }
// Khai báo cột
{
header: 'Ngày Tạo', dataIndex: 'thoiGianTao',
renderer: Ext.util.Format.dateRenderer('Y-m-d')
}
Hàm Render Tùy Biến Và Tô Màu Động
Thuộc tính renderer nhận payload gồm: value (giá trị ô), cellmeta (thông tin container), record (toàn bộ bản ghi), rowIdx, colIdx, store.
function hienThiIconGioiTinh(giaTri) {
const iconUrl = giaTri === 'male' ? '/assets/man.svg' : '/assets/woman.svg';
return `<img src="${iconUrl}" height="20" alt="Icon"/> ${giaTri === 'male' ? 'Nam' : 'Nữ'}`;
}
Tô màu dòng dựa trên điều kiện nghiệp vụ thông qua viewConfig.getRowClass kết hợp CSS scoped:
/* style.css */
.status-ok .x-grid-cell { background-color: #d4edda !important; }
.status-warn .x-grid-cell { background-color: #fff3cd !important; }
viewConfig: {
getRowClass: function(banKy, idx, par, store) {
const val = banKy.get('status');
if (val === 'ok') return 'status-ok';
if (val === 'warn') return 'status-warn';
return '';
}
}
Mô Hình Chọn Lựa Và Cột Số Thứ Tự
Thêm Ext.grid.RowNumberer vào đầu mảng columns để tự động sinh cột STT. Khắc phục lỗi lệch số sau thao tác xóa/dọn bộ nhớ bằng cách ép lại index trong renderer:
new Ext.grid.RowNumberer({
width: 35,
renderer: (val, meta, rec, rowIndex) => rowIndex + 1
})
Hệ thống chọn lựa được quản lý bởi selModel. Ví dụ chế độ Checkbox:
selModel: Ext.create('Ext.selection.CheckboxModel', {
injectCheckbox: 1,
checkOnly: true
})
Hoặc chế độ chọn hàng thuần túy (hỗ trợ Ctrl/Shift đa tuyển):
selModel: Ext.create('Ext.selection.RowModel', { mode: 'MULTI' })
Cơ Chế Phân Trang (Pagination)
Thanh điều hướng trang Ext.PagingToolbar gắn vào bbar. Lưu ý quan trọng: trong kiến trúc Ext 4+, thuộc tính pageSize di chuyển vào cấu hình Store, không còn đặt tại Toolbar.
const storePhanTrang = Ext.create('Ext.data.Store', {
model: 'ProductModel',
pageSize: 15, // Đặt tại đây
proxy: { type: 'ajax', url: '/api/list' }
});
// Gán vào Grid
bbar: Ext.create('Ext.PagingToolbar', {
store: storePhanTrang,
displayInfo: true,
displayMsg: 'Hiển thị {0}-{1} trên tổng {2}',
emptyMsg: 'Kho dữ liệu trống'
})
Truy Vấn Server-Side Và JSONP
Phía backend trả về JSON chuẩn có khóa tổng số (totalProperty) và mảng dữ liệu con (root). Để bypass chính sách Same-Origin, sử dụng transport jsonp.
proxy: {
type: 'jsonp',
url: 'https://external-api.com/fetch',
reader: {
type: 'json',
totalProperty: 'totalCount',
root: 'entries',
callbackKey: 'cb'
}
}
// Backend Java Servlet mẫu xử lý JSONP wrapper
String cb = request.getParameter("callback");
int start = Integer.parseInt(request.getParameter("start") != null ? request.getParameter("start") : "0");
int limit = Integer.parseInt(request.getParameter("limit") != null ? request.getParameter("limit") : "15");
response.setContentType(cb != null ? "text/javascript" : "application/json");
StringBuilder sb = new StringBuilder();
if (cb != null) sb.append(cb).append("(");
sb.append("{\"totalCount\":").append(totalRecords).append(",\"entries\":[");
// Logic query database slice(start, limit)
sb.append("]}");
if (cb != null) sb.append(")");
response.getWriter().write(sb.toString());
Phân Trang Nội Địa (Memory Paging)
Khi dữ liệu đã được pull toàn bộ về client, dùng proxy pagingmemory để virtualize màn hình mà không phát sinh request lặp lại:
proxy: {
type: 'pagingmemory',
data: cachedDataArray, // Mảng object đã load sẵn
reader: { type: 'json' }
},
pageSize: 20
Sắp Xếp Từ Xa (Remote Sort)
Set remoteSort: true trên Store để ủy quyền logic sắp xếp xuống DBMS. Khi click header cột, framework gửi tham số sort kèm chỉ thị dir. Lưu ý phiên bản gần đây đóng gói sort thành chuỗi JSON array: [{"property":"field_name","direction":"ASC"}].
Backend cần parser chuỗi này thay vì đọc raw parameter đơn giản:
String sortRaw = request.getParameter("sort");
String direction = null;
if (sortRaw != null) {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(sortRaw);
direction = node.path(0).path("direction").asText();
String targetCol = node.path(0).path("property").asText();
// Build SQL: SELECT ... FROM table WHERE ... ORDER BY targetCol ASC/DESC LIMIT ... OFFSET ...
}
Kết cấu trúc phân tách rõ ràng giữa View (GridPanel), Model (Store/Proxy) và Controller (Event Listeners/SelectionModel) tuân thủ quy chuẩn MVC, giúp module dễ mở rộng và bảo trì khi tích hợp hệ thống phức tạp.