這兩周與 GridPanel 奮戰終於要近尾聲了, 剩下可編輯的表格測完就大功告成. 前面兩篇參見 :
- ExtJS 4 測試 : GridPanel (一)
- ExtJS 4 測試 : GridPanel (二)
4.2.2 版的 API :
# http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.grid.Panel
可編輯的 GridPanel 就像 Excel 那樣, 點任何一個儲存格即可編輯. 在 ExtJS 3 時使用 Ext.grid.EditorGridPanel 類別來製作可編輯表格, 而 ExtJS 4 則是以外掛 (plugin) 形式整合在 GridPanel 中, 用 plugins 屬性設定來達成.
GridPanel 的 plugins 都放在 Ext.grid.plugin 函式庫內, 4.2 版有 7 個外掛, 都繼承自 Ext.AbstractPlugin 類別, 而可編輯表格會用到的外掛有兩個, 其中 CellEditing 為可編輯儲存格 (一次只能編輯一個儲存格), 而 RowEditing 則為可編輯列 (一次可編輯一列) :
Ext.AbstractPlugin
|__ Ext.grid.plugin.Editing
|__ Ext.grid.plugin.CellEditing
|__ Ext.grid.plugin.RowEditing
欲使 GridPanel 表格可編輯, 必須在兩個地方動手腳 :
- GridPanel 要加上 plugins 屬性, 並建立一個 CellEditing/RowEditing 物件作為其值 :
plugins : [{Ext.create("Ext.grid.plugin.CellEditing", {clickToEdit: 1})}]
plugins : [{Ext.create("Ext.grid.plugin.RowEditing", {clickToEdit: 1})}]
此屬性值為一個陣列, 亦即可以指定一個以上的外掛. 此處屬性 clickToEdit 設為 1 表示點一下儲存格即進入編輯模式 (預設為 2, 表示需 double clicks 才會進入編輯模式).
這也可以用 ptype 屬性來設定 :
[{ptype:"cellediting",clicksToEdit:1}] 或
[{ptype:"rowediting",clicksToEdit:1}]
其次還須設定選擇模式 selType 屬性 :
selType : "cellmodel" 或 "rowmodel"
- 欄位模式 columns 中需添加 editor 屬性, 可指定 allowBlank 與 xtype 等屬性 :
editor : {allowBlank:false,xtype:"textfield"}
其中 xtype 預設為 textfield, allowBlank 是欄位檢查屬性, 預設為 true, 即允許空值. 若指定值為空物件 {} 即套用預設值. editor 屬性是一個 Ext.form.field.Field 物件, 因此可以使用 textfield, checkbox, 或 combobox 作為編輯欄位.
首先來看儲存格編輯模式 (cellmodel), 如下列範例 25 所示 :
測試範例 25 : http://http://yhhuang1966.000a.biz/test/extjstest/extjs_grid_25.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var columns=[{header:"股票名稱",dataIndex:"name"},
{header:"股票代號",dataIndex:"id"},
{header:"收盤價 (元)",dataIndex:"close",
editor:{}
},
{header:"成交量 (張)",dataIndex:"volumn",
editor:{}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119],
["中華電","2412",96.4,5249],
["中碳","1723",192.5,918],
["創見","2451",108,733],
["華擎","3515",118.5,175],
["訊連","5203",97,235]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"action"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:450,
forceFit:true,
selType:"cellmodel",
plugins:[
Ext.create("Ext.grid.plugin.CellEditing")
]
});
}); //end of onReady
此例中收盤價與成交量兩個欄位有 editor 屬性, 因此只有此兩欄位可編輯, 兩欄位之 editor 屬性設為空物件, 故預設為允許空白, 且單擊即進入編輯模式. 只要有進入編輯模式過, 該儲存格左上角會有一個三角形, 表示此資料為 dirty (可能有被修改之意).
當我們離開編輯的儲存格時, 輸入的資料便會修改 Store 中所儲存的資料. 上例中建立 CellEditing 物件時未指定 clicksToEdit 屬性, 預設是要 double clicks 才會進入編輯模式. 在下面範例 25-1 中, 編輯欄位時不允許空白, 且單擊即可編輯儲存格.
測試範例 25-1 : http://tony1966.xyz/test/extjstest/extjs_grid_25_1.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var columns=[{header:"股票名稱",dataIndex:"name"},
{header:"股票代號",dataIndex:"id"},
{header:"收盤價 (元)",dataIndex:"close",
editor:{allowBlank:false}
},
{header:"成交量 (張)",dataIndex:"volumn",
editor:{allowBlank:false}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119],
["中華電","2412",96.4,5249],
["中碳","1723",192.5,918],
["創見","2451",108,733],
["華擎","3515",118.5,175],
["訊連","5203",97,235]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"action"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:450,
forceFit:true,
selType:"cellmodel",
plugins:[
Ext.create("Ext.grid.plugin.CellEditing",{clicksToEdit:1})
]
});
}); //end of onReady
此例中我們將兩個可編輯欄位設為單擊即進入編輯模式, 且不允許空值, 若清空會出現紅色框並回復原值.
上面兩個範例中, plugins 屬性是一個 Ext.grid.plugin.CellEditing 物件, 其實也可以在物件實體中用 ptype 屬性 (plugin type) 來設定, Ext.grid.plugin.CellEditing 物件的 ptype 為 "cellediting" 字串, 我們將上面範例 25-1 改為下列範例 25-2 (只改 plugins 部分) :
測試範例 25-2 : http://tony1966.xyz/test/extjstest/extjs_grid_25_2.htm [看原始碼]
plugins:[{ptype:"cellediting",clicksToEdit:1}]
上面提到 editor 預設編輯欄位是 textfield, 如果要使用與資料類型相同的編輯欄位, 可以用 xtype 加以指定, 常用有下列四種 :
- checkboxfield : 核取方塊
- numberfield : 數字調整器, 可使用 step 屬性指定步階值
- combo : 下拉式選單
- datefield : 日期選單
其中下拉式選單 combo 必須先建立一個 Store 作為選項的資料來源.
在下列範例 25-3 中使用了較多欄位資料來演示編輯欄位之用法, 我們增加了類股欄位來測試 combo 編輯欄位的功能, 並預先建立另一個 Store 物件 category 做為 combo 的資料來源 :
測試範例 25-3 : http://tony1966.xyz/test/extjstest/extjs_grid_25_3.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var category=Ext.create('Ext.data.Store', {
fields:['name'],
data:[
{"name":"半導體"},
{"name":"通信"},
{"name":"塑化"},
{"name":"模組"},
{"name":"主機板"},
{"name":"軟體"}
]
});
var columns=[{header:"股票名稱",dataIndex:"name",width:60},
{header:"股票代號",dataIndex:"id",width:60},
{header:"收盤價 (元)",dataIndex:"close",width:60},
{header:"成交量 (張)",dataIndex:"volumn",width:60,
editor:{
xtype:"numberfield",
step:1
}
},
{header:"股東會日期",dataIndex:"meeting",
xtype:"datecolumn",format:"Y-m-d",
editor:{
xtype:"datefield"
}
},
{header:"董監改選",dataIndex:"election",width:50,
xtype:"booleancolumn",trueText:"是",falseText:"否",
editor:{
xtype:"checkboxfield"
}
},
{header:"類股",dataIndex:"category",width:50,
editor:{
xtype:"combo",
allowBlank:false,
displayField:"name",
store:category
}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119,"2014/06/04",false,"半導體"],
["中華電","2412",96.4,5249,"2014/06/15",false,"通信"],
["中碳","1723",192.5,918,"2014/07/05",true,"塑化"],
["創見","2451",108,733,"2014/06/30",false,"模組"],
["華擎","3515",118.5,175,"2014/07/20",true,"主機板"],
["訊連","5203",97,235,"2014/05/31",false,"軟體"]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"meeting"},
{name:"election"},
{name:"category"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:600,
forceFit:true,
selType:"cellmodel",
plugins:[{ptype:"cellediting",clicksToEdit:1}]
});
}); //end of onReady
此例中我們將後面四個欄位設為可編輯, 可見利用 editor 的 xtype 就可以讓編輯介面更方便, 更直覺
其實 editor 屬性提供了許多子屬性可對輸入值進行限制, 如下列範例 25-4 所示 :
測試範例 25-4 : http://tony1966.xyz/test/extjstest/extjs_grid_25_4.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var category=Ext.create('Ext.data.Store', {
fields:["value","text"],
data:[
{value:0,text:"半導體"},
{value:1,text:"通信"},
{value:2,text:"塑化"},
{value:3,text:"模組"},
{value:4,text:"主機板"},
{value:5,text:"軟體"}
]
});
var columns=[{header:"股票名稱",dataIndex:"name",width:60},
{header:"股票代號",dataIndex:"id",width:60},
{header:"收盤價 (元)",dataIndex:"close",width:60},
{header:"成交量 (張)",dataIndex:"volumn",width:60,
editor:{
xtype:"numberfield",
step:1,
minValue:100,
maxValue:100000
}
},
{header:"股東會日期",dataIndex:"meeting",
xtype:"datecolumn",format:"Y-m-d",
editor:{
xtype:"datefield",
minValue:"2014-06-10",
maxValue:"2014-06-26",
disabledDays:[0,6],
disabledDaysText:"假日"
}
},
{header:"董監改選",dataIndex:"election",width:50,
xtype:"booleancolumn",trueText:"是",falseText:"否",
editor:{
xtype:"checkboxfield"
}
},
{header:"類股",dataIndex:"category",width:50,
editor:{
xtype:"combo",
store:category,
allowBlank:false,
valueField:"value",
displayField:"text",
editable:false
}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119,"2014/06/04",false,"半導體"],
["中華電","2412",96.4,5249,"2014/06/15",false,"通信"],
["中碳","1723",192.5,918,"2014/07/05",true,"塑化"],
["創見","2451",108,733,"2014/06/30",false,"模組"],
["華擎","3515",118.5,175,"2014/07/20",true,"主機板"],
["訊連","5203",97,235,"2014/05/31",false,"軟體"]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"meeting"},
{name:"election"},
{name:"category"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:600,
forceFit:true,
selType:"cellmodel",
plugins:[{ptype:"cellediting",clicksToEdit:1}]
});
}); //end of onReady
本例中, 我們採用另一種方式來定義 combo 的 Store, 以便能在 editor 中使用限制輸入之屬性. 其中 Store 中定義了兩個欄位 : text 與 value, 我們可以在 valueField 中指定一個欄位作 value, 在 displayField 中指定一個欄位當選項的顯示欄位. 而 editable 則是限制使用者能否編輯這個選單.
其次在 datefield 編輯欄位中, 利用 minValue 與 maxValue 可以限制可選日期的範圍, disabledDays 為不可選日期 (0 為周日, 1 為周一, ....), disabledDaysText 則為其提示語.
接下來看看列編輯 RowEditing 功能, 由 Ext.grid.plugin.RowEditing 類別提供, 其 API 參見 :
#
http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.grid.plugin.RowEditing
只要將 selType 改為 RowEditing, 並建立 RowEditing 物件即可, 如下範例 26 所示 :
測試範例 26 : http://tony1966.xyz/test/extjstest/extjs_grid_26.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var columns=[{header:"股票名稱",dataIndex:"name"},
{header:"股票代號",dataIndex:"id"},
{header:"收盤價 (元)",dataIndex:"close",
editor:{}
},
{header:"成交量 (張)",dataIndex:"volumn",
editor:{}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119],
["中華電","2412",96.4,5249],
["中碳","1723",192.5,918],
["創見","2451",108,733],
["華擎","3515",118.5,175],
["訊連","5203",97,235]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"action"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:450,
forceFit:true,
selType:"rowmodel",
plugins:[
Ext.create("Ext.grid.plugin.RowEditing",{clicksToEdit:1})
]
});
}); //end of onReady
當單擊任何一列時, 該列的可編輯欄位就會出現編輯框 (預設 textfield), 以及 Update 與 Cancel 兩個按鈕. 編輯完按 Update 就會在可編輯欄位左上角出現小三角, 標示此資料為 "dirty".
同樣地, plugins 屬性也是可以用 ptype 來設定, Ext.grid.plugin.RowEdititng 物件的 ptype 為 "rowediting", 如下列範例 26-1 所示 (只改 plugins 部分) :
測試範例 26-1 : http://tony1966.xyz/test/extjstest/extjs_grid_26_1.htm [看原始碼]
plugins:[{ptype:"rowediting",clicksToEdit:1}]
其次, 編輯列時出現的兩個按鈕 "Update" 與 "Cancel" 是否能修改呢? 可以的, 只要傳入 saveBtnText 與 cancelBtnText 這兩個屬性即可, 如下列範例 26-2 所示 :
測試範例 26-2 : http://tony1966.xyz/test/extjstest/extjs_grid_26_2.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var columns=[{header:"股票名稱",dataIndex:"name"},
{header:"股票代號",dataIndex:"id"},
{header:"收盤價 (元)",dataIndex:"close",
editor:{}
},
{header:"成交量 (張)",dataIndex:"volumn",
editor:{}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119],
["中華電","2412",96.4,5249],
["中碳","1723",192.5,918],
["創見","2451",108,733],
["華擎","3515",118.5,175],
["訊連","5203",97,235]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"action"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:450,
forceFit:true,
selType:"rowmodel",
plugins:[{ptype:"rowediting",
clicksToEdit:1,
saveBtnText:"更新",
cancelBtnText:"取消"}
]
});
}); //end of onReady
同樣地, 編輯欄位也是可以用 xtype 屬性定義資料格式, 使輸入介面更直覺, 我們將範例 25-3 修改為下面的範例 26-3 :
測試範例 26-3 : http://tony1966.xyz/test/extjstest/extjs_grid_26_3.htm [看原始碼]
Ext.onReady(function() {
//定義表頭欄位
var category=Ext.create('Ext.data.Store', {
fields:['name'],
data:[
{"name":"半導體"},
{"name":"通信"},
{"name":"塑化"},
{"name":"模組"},
{"name":"主機板"},
{"name":"軟體"}
]
});
var columns=[{header:"股票名稱",dataIndex:"name",width:60},
{header:"股票代號",dataIndex:"id",width:60},
{header:"收盤價 (元)",dataIndex:"close",width:60},
{header:"成交量 (張)",dataIndex:"volumn",width:60,
editor:{
xtype:"numberfield",
step:1
}
},
{header:"股東會日期",dataIndex:"meeting",
xtype:"datecolumn",format:"Y-m-d",
editor:{
xtype:"datefield"
}
},
{header:"董監改選",dataIndex:"election",width:50,
xtype:"booleancolumn",trueText:"是",falseText:"否",
editor:{
xtype:"checkboxfield"
}
},
{header:"類股",dataIndex:"category",width:50,
editor:{
xtype:"combo",
allowBlank:false,
displayField:"name",
store:category
}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119,"2014/06/04",false,"半導體"],
["中華電","2412",96.4,5249,"2014/06/15",false,"通信"],
["中碳","1723",192.5,918,"2014/07/05",true,"塑化"],
["創見","2451",108,733,"2014/06/30",false,"模組"],
["華擎","3515",118.5,175,"2014/07/20",true,"主機板"],
["訊連","5203",97,235,"2014/05/31",false,"軟體"]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"meeting"},
{name:"election"},
{name:"category"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
columns:columns,
store:store,
renderTo:"grid",
width:600,
forceFit:true,
selType:"rowmodel",
plugins:[{ptype:"rowediting",
clicksToEdit:1,
saveBtnText:"更新",
cancelBtnText:"取消"}]
});
}); //end of onReady
本例只是將範例 25-3 的 selType 與 ptype 屬性改成列編輯模式而已, 在 editor 中使用 xtype 讓 UI 介面再進化, 特別是 combo 適合用在須限制使用者能輸入的範圍時.
接者我們要來測試如何在 Store 儲存中新增一筆資料. 在
前篇範例 16 時, 我們曾經使用 tbar 設置一個按鈕來搜尋 Store 資料, 此處則要設置新增紀錄與刪除的按鈕, 如下列範例 27 所示 :
測試範例 27 : http://tony1966.xyz/test/extjstest/extjs_grid_27.htm [看原始碼]
Ext.QuickTips.init();
Ext.onReady(function() {
//定義表頭欄位
var category=Ext.create('Ext.data.Store', {
fields:['name'],
data:[
{"name":"半導體"},
{"name":"通信"},
{"name":"塑化"},
{"name":"模組"},
{"name":"主機板"},
{"name":"軟體"}
]
});
var columns=[{header:"股票名稱",dataIndex:"name",width:60,
editor:{}
},
{header:"股票代號",dataIndex:"id",width:60,
editor:{}
},
{header:"收盤價 (元)",dataIndex:"close",width:60,
editor:{}
},
{header:"成交量 (張)",dataIndex:"volumn",width:60,
editor:{
xtype:"numberfield",
step:1
}
},
{header:"股東會日期",dataIndex:"meeting",
xtype:"datecolumn",format:"Y-m-d",
editor:{
xtype:"datefield"
}
},
{header:"董監改選",dataIndex:"election",width:50,
xtype:"booleancolumn",trueText:"是",falseText:"否",
editor:{
xtype:"checkboxfield"
}
},
{header:"類股",dataIndex:"category",width:50,
editor:{
xtype:"combo",
displayField:"name",
store:category
}
}
];
//定義原始資料
var data=[["台積電","2330",123,4425119,"2014/06/04",false,"半導體"],
["中華電","2412",96.4,5249,"2014/06/15",false,"通信"],
["中碳","1723",192.5,918,"2014/07/05",true,"塑化"],
["創見","2451",108,733,"2014/06/30",false,"模組"],
["華擎","3515",118.5,175,"2014/07/20",true,"主機板"],
["訊連","5203",97,235,"2014/05/31",false,"軟體"]
];
//轉成 Store 物件
var store=Ext.create("Ext.data.ArrayStore", {
autoLoad:true,
data:data,
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"meeting"},
{name:"election"},
{name:"category"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
title:"台股",
tools:[{id:"refresh",qtip:"重載",
handler:function(){store.load();}
}
],
columns:columns,
store:store,
renderTo:"grid",
width:600,
forceFit:true,
selType:"rowmodel",
plugins:[{ptype:"rowediting",
clicksToEdit:2,
saveBtnText:"儲存",
cancelBtnText:"取消"}
],
tbar:{
xtype:'toolbar',
frame:true,
border:false,
padding:2,
items:[
"->", //也可用 {xtype:"tbfill"}
"-", //也可用 {xtype:"tbseparator"}
{xtype:"button",text:"新增",
handler:addRecord},
"-",
{xtype:"button",text:"刪除",
handler:delRecord},
"-"
]
}
});
function addRecord() {
var rec={name:"",id:"",close:"",volumn:"",meeting:"",election:"",category:""};
store.insert(0,rec);
}
function delRecord() {
var sm=grid.getSelectionModel(); //取得選擇模型
var rec=sm.getSelection()[0]; //取得選擇之列 (紀錄)
if (rec==undefined) {Ext.Msg.alert("訊息","請選擇欲刪除之紀錄!");}
else {
Ext.Msg.confirm("確認","確定要刪除?",function(btn){
if (btn=="yes") {store.remove(rec);}
});
}
}
}); //end of onReady
按下新增鈕時, 會在最前面插入一個空白列, 雙擊該列會進入編輯模式, 輸入完後按儲存即存入 Store 中, 這時此新增列每個欄位左上角都有一個小三角, 表示為 dirty 資料 (與 Store 之 data 不一致者), 若按下右上角的重新載入鈕, 新增資料將消失.
點選此新增列, 按下刪除鈕, 將詢問是否真的要刪除, 按確定就會從 Store 物件中移除此紀錄 :
注意, 此例中, 為了新增後能輸入新紀錄, 因此每一個欄位都有設 editor 屬性以便進行編輯, 同時改為雙擊才進入編輯模式.
以上可編輯欄位的操作都是在本地的 Store 儲存內 (記憶體), 重新整理網頁時所有更改的數據將消失, 若要保存起來必須回存至遠端資料庫. 接著便來測試以 Ajax 將可編輯表格資料回存遠端資料庫的功能.
首先在後端用 phpMyAdmin 製做一個資料表 stocks, 其結構如下 :
CREATE TABLE IF NOT EXISTS `stocks` (
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`id` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`close` float NOT NULL,
`volumn` int(11) NOT NULL,
`meeting` date NOT NULL,
`election` tinyint(1) NOT NULL,
`category` varchar(255) COLLATE utf8_unicode_ci NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
執行 SQL 指令插入預設資料 :
INSERT INTO `stocks` (`name`, `id`, `close`, `volumn`, `meeting`,
`election`, `category`) VALUES
('台積電', '2330', 123, 4425119, '2014-06-04', 0, '半導體'),
('中華電', '2412', 96.4, 5249, '2014-06-15', 0, '通信'),
('中碳', '1723', 192.5, 918, '2014-07-05', 0, '塑化'),
('創見', '2451', 108, 733, '2014-06-30', 0, '模組'),
('華擎', '3515', 118.5, 175, '2014-07-20', 1, '主機板'),
('訊連', '5203', 97, 235, '2014-05-31', 0, '軟體');
完整 SQL 匯出檔如下 :
#
http://tony1966.xyz/test/extjstest/stocks.sql
然後寫一個 PHP 程式 get_stocks.php 來輸出 JSON 資料給前端使用 :
lt;?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com";
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$SQL="SELECT COUNT(*) FROM `stocks`";
$RS=mysql_query($SQL, $conn);
list($total)=mysql_fetch_row($RS); //紀錄總筆數
$SQL="SELECT * FROM `stocks`";
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$stock=array();
for ($i=0; $i<mysql_numrows($result); $i++) { //走訪紀錄集 (列)
$row=mysql_fetch_array($result); //取得列陣列
$stock[$i]=array("name" => $row["name"],
"id" => $row["id"],
"close" => $row["close"],
"volumn" => $row["volumn"],
"meeting" => $row["meeting"],
"election" => $row["election"],
"category" => $row["category"]
);
} //end of for
$arr=array("totalProperty" => $total, "root" => $stock);
echo json_encode($arr); //將陣列轉成 JSON 資料格式傳回
?>
此 PHP 程式會輸出下列資料 :
#
http://tony1966.xyz/test/extjstest/get_stocks.php
這樣就可以開始寫前端 ExtJS 程式了, 我們修改上述範例 27 的 Store 部分, 將資料來源從本機陣列改為遠端 PHP 程式輸出的 JSON 檔, 如下列範例 28 所示 :
測試範例 28 : http://tony1966.xyz/test/extjstest/extjs_grid_28.htm [看原始碼]
Ext.QuickTips.init();
Ext.onReady(function() {
//定義表頭欄位
var category=Ext.create('Ext.data.Store', {
fields:['name'],
data:[
{"name":"半導體"},
{"name":"通信"},
{"name":"塑化"},
{"name":"模組"},
{"name":"主機板"},
{"name":"軟體"}
]
});
var columns=[{header:"股票名稱",dataIndex:"name",width:60,
editor:{}
},
{header:"股票代號",dataIndex:"id",width:60,
editor:{}
},
{header:"收盤價 (元)",dataIndex:"close",width:60,
editor:{}
},
{header:"成交量 (張)",dataIndex:"volumn",width:60,
editor:{
xtype:"numberfield",
step:1
}
},
{header:"股東會日期",dataIndex:"meeting",
xtype:"datecolumn",format:"Y-m-d",
editor:{
xtype:"datefield"
}
},
{header:"董監改選",dataIndex:"election",width:50,
xtype:"booleancolumn",trueText:"是",falseText:"否",
editor:{
xtype:"checkboxfield"
}
},
{header:"類股",dataIndex:"category",width:50,
editor:{
xtype:"combo",
displayField:"name",
store:category
}
}
];
//轉成 Store 物件
var store=Ext.create("Ext.data.Store", {
autoLoad:true,
proxy:{
type:"ajax",
url:"get_stocks.php",
reader:{
type:"json",
totalProperty:"totalProperty",
root:"root",
idProperty:"id"
}
},
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"meeting"},
{name:"election",type:"boolean"},
{name:"category"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
title:"台股",
tools:[{id:"refresh",qtip:"重載",
handler:function(){store.load();}
}
],
columns:columns,
store:store,
renderTo:"grid",
width:600,
forceFit:true,
selType:"rowmodel",
plugins:[{ptype:"rowediting",
clicksToEdit:2,
saveBtnText:"儲存",
cancelBtnText:"取消"}
],
tbar:{
xtype:'toolbar',
frame:true,
border:false,
padding:2,
items:[
"->",
"-",
{xtype:"button",text:"新增",handler:addRecord},
"-",
{xtype:"button",text:"刪除",handler:delRecord},
"-"
]
}
});
function addRecord() {
var rec={name:"",id:"",close:"",volumn:"",meeting:"",election:"",
category:""};
store.insert(0,rec);
}
function delRecord() {
var sm=grid.getSelectionModel();
var rec=sm.getSelection()[0];
if (rec==undefined) {Ext.Msg.alert("訊息","請選擇欲刪除之紀錄!");}
else {
Ext.Msg.confirm("確認","確定要刪除?",function(btn){
if (btn=="yes") {
store.remove(rec);
}
});
}
}
}); //end of onReady
注意, Store 中的董監改選欄位 (election) 必須加上 type 屬性, 指定為 boolean, 這樣才會正確將 JSON 輸出的 "0" 與 "1" 字串解析為布林值, 否則會全部顯示 "是".
這裡主要的差別在 Store 中的資料來源是從 proxy 透過 reader 取得與剖析以 Ajax 從後端取得的 JSON 資料, 而非以 data 屬性取自本地二維陣列資料, 但結果與範例 27 是一樣的, 新增或刪除都只是作用在記憶體中的 Store 物件而已, 只要重新整理頁面, 又回復原來的樣子, 資料庫中的 stocks 資料表根本不受影響, 亦即, 我們的增修刪結果並沒有回存到資料庫中, 也就是前後端不同步.
要如何回存增修刪結果呢? 首先需取得有被增修刪的紀錄, 也就是所有的 dirty 紀錄, 然後用 Ajax 方式傳回後端程式進行資料表的增修刪作業才行.
首先在 tbar 上增加一個回存按鈕, 設定其處理函式為 sync() :
items:[
"->",
"-",
{xtype:"button",text:"新增",handler:addRecord},
"-",
{xtype:"button",text:"刪除",handler:delRecord},
"-",
{xtype:"button",text:"回存",handler:sync},
"-",
]
接著撰寫回存處理函式 sync(), 這裡要用到 Ext.data.Store 類別的 getModifiedRecords() 方法, 它會傳回 Ext.data.Model 的陣列, 其中存放著所有修改的資料 (dirty records), 包含新增與更新的紀錄, 但不包括刪除的紀錄, 刪除的紀錄要呼叫 getRemovedRecords().
function sync() {
var modified=store.getModifiedRecords().slice(0); //複製 Model 陣列
var arr=[]; //儲存被修改的資料
Ext.each(modified, function(item){
arr.push(
item.data); //從 Model 物件中取出紀錄, 放入陣列中儲存
});
Ext.Ajax.request({ //起始一個 Ajax 呼叫
method:"post",
url:"
save_stocks_changes.php",
params:"
modified=" +
encodeURIComponent(
Ext.encode(arr)), //傳送參數
success:function(response){ //成功之回應
Ext.Msg.alert("訊息",response.responseText,
function(){store.reload();}
);
},
failure:function(){ //失敗之回應
Ext.Msg.alert("錯誤訊息","無法與伺服端連線!");
}
});
}
如果我們修改了 2330 與 2412 兩家公司的資料後按回存, 那麼 Ajax 會向伺服器送出如下格式的參數 (名稱為 modified) :
可見 Ext.encode() 會將每一筆紀錄以物件實體表示, 多筆紀錄就以物件陣列表示, 傳給 encodeURIComponent() 方法後, 非 ANSI 字元就變成 unicode 編碼了. 所以, 我們的後端 PHP 程式必須解讀這些資訊, 然後更新資料表中的相關紀錄. 參考第二篇的範例 12-6, 這可以在去除左右陣列符號後, 用 PHP 的 json_decode() 函式轉成物件
由於使用 POST 方法送出參數, 因此要用 $_POST["modified"] 取得 modified 參數 (若用 $_GET 會得到空值).
我們將範例 28 加上 sync() 函式後變成下列範例 29 :
以下範例 29 測試尚未成功, 最近要忙評鑑 (我當委員), 所以暫停一下 ....
測試範例 29 : http://tony1966.xyz/test/extjstest/extjs_grid_29.htm [看原始碼]
上面範例 29 須個別處理刪除, 更新與新增這兩種操作, 感覺很麻煩, 其實 Store 中提供了自動同步的功能, 只要將 autoSync 屬性設為 true, 並且將 proxy 屬性中的 url 改為 api, 設定四種後端操作程式 (CRUD, 增讀改刪) 即可在編輯完畢後立即更新後端資料庫, 保持前後端同步 (
不過, 根據我實際測試, CREATE 應該用不到, 因為它實際上是用 UPDATE 的 api 傳送出去).
測試範例 30 : http://tony1966.xyz/test/extjstest/extjs_grid_30.htm [看原始碼]
Ext.QuickTips.init();
Ext.onReady(function() {
//定義表頭欄位
var category=Ext.create('Ext.data.Store', {
fields:['name'],
data:[
{"name":"半導體"},
{"name":"通信"},
{"name":"塑化"},
{"name":"模組"},
{"name":"主機板"},
{"name":"軟體"}
]
});
var columns=[{header:"股票名稱",dataIndex:"name",width:60,
editor:{}
},
{header:"股票代號",dataIndex:"id",width:60,
editor:{}
},
{header:"收盤價 (元)",dataIndex:"close",width:60,
editor:{}
},
{header:"成交量 (張)",dataIndex:"volumn",width:60,
editor:{
xtype:"numberfield",
step:1
}
},
{header:"股東會日期",dataIndex:"meeting",
xtype:"datecolumn",format:"Y-m-d",
editor:{
xtype:"datefield"
}
},
{header:"董監改選",dataIndex:"election",width:50,
xtype:"booleancolumn",trueText:"是",falseText:"否",
editor:{
xtype:"checkboxfield"
}
},
{header:"類股",dataIndex:"category",width:50,
editor:{
xtype:"combo",
displayField:"name",
store:category
}
}
];
//轉成 Store 物件
var store=Ext.create("Ext.data.Store", {
autoLoad:true,
autoSync:true,
proxy:{
type:"ajax",
api:{
read:"get_stocks.php",
create:undefined,
update:"update_stocks.php",
destroy:"delete_stocks.php"
},
reader:{
type:"json",
totalProperty:"totalProperty",
root:"root",
idProperty:"id"
},
writer: {
type:"json",
writeAllFields:true,
allowSingle:false, //一律以陣列送出
root:"data"
}
},
fields:[
{name:"name"},
{name:"id"},
{name:"close"},
{name:"volumn"},
{name:"meeting"},
{name:"election",type:"boolean"},
{name:"category"}
]
});
//建立 GridPanel
var grid=Ext.create("Ext.grid.Panel",{
title:"台股",
tools:[{id:"refresh",qtip:"重載",
handler:function(){store.load();}
}
],
columns:columns,
store:store,
renderTo:"grid",
width:600,
forceFit:true,
selType:"rowmodel",
plugins:[{ptype:"rowediting",
clicksToEdit:2,
saveBtnText:"儲存",
cancelBtnText:"取消"}
],
tbar:{
xtype:'toolbar',
frame:true,
border:false,
padding:2,
items:[
"-",
{xtype:"button",text:"重設",handler:reset},
"->",
"-",
{xtype:"button",text:"新增",handler:addRecord},
"-",
{xtype:"button",text:"刪除",handler:delRecord},
"-"
]
}
});
function addRecord() {
var rec={name:"",id:"",close:"",volumn:"",meeting:"",election:"",
category:""};
store.insert(0,rec);
}
function delRecord() {
var sm=grid.getSelectionModel();
var rec=sm.getSelection()[0];
if (rec==undefined) {Ext.Msg.alert("訊息","請選擇欲刪除之紀錄!");}
else {
Ext.Msg.confirm("確認","確定要刪除?",function(btn){
if (btn=="yes") {
store.remove(rec);
}
});
}
}
function reset() {
Ext.Ajax.request({
method:"post",
url:"reset_stocks.php",
success:function(response){
Ext.Msg.alert("訊息",response.responseText,
function(){store.reload();}
);
},
failure:function(){
Ext.Msg.alert("錯誤訊息","無法與伺服端連線!");
}
});
}
}); //end of onReady
注意,
這裡 Store 的 autoSync 必須設為 true, 才會在每次編輯完畢按儲存後, 自動呼叫 api 指定的後端程式回存異動之紀錄.
當點選一筆資料 (例如台積電), 按下刪除時除了會將 Store 中的資料刪除外, 還會向後端發出 Ajax 要求, 執行 delete_stocks.php 程式, 這時察看 Chrome 的 Javascript 控制台的 Network, 發現 proxy 是將要刪除的資料放在 HTTP 的 Request payload 中, 而不是 Query String 裡 :
這個 payload 資料在 PHP 程式裡不管是用 $_GET[] 還是 $_POST[] 都讀不到, 必須用 file_get_contents("php://input") 去讀取原始資料 :
$input=file_get_contents("php://input");
取得的原始資料為字串, 例如刪除台積電這筆, 上述指令就會得到 $input 如下 :
{"data":[{"name":"\u53f0\u7a4d\u96fb","id":"2330","close":"123","volumn":"4425119","meeting":"2014-06-04","election":false,"category":"\u534a\u5c0e\u9ad4"}
]}
這是因為我們在 Store 的 Writer 中設定了 allowSingle 為 false 以及 root 為 data 的關係 :
writer: {
type:"json",
writeAllFields:true,
allowSingle:false,
root:"data"
}
根據下列關於 allowSingle 說明, 設為 false 就會即使只有一筆資料, 也一律用陣列方式傳回
#
http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.data.writer.Json-cfg-allowSingle
我是在測試時利用下列檔案寫入指令, 從後端檔案管理員中很快就能知道收到甚麼值了 :
$handle=fopen("output.txt", "w");
fwrite($handle, $data);
fclose($handle);
但是這個 $input 不能直接呼叫 json_decode() 函式來轉成物件, 因為 data 屬性的值是一個陣列, 而不是單純的字串. 如果直接將 $input 傳入 json_decode(), 那麼用 data 屬性值將會是空值 (null). 正確的作法是要去頭去尾, 取出裡面的純粹 json 資料.
$data=ltrim($input,'{"data":'); //去除 root 字串
$data=rtrim($data,"}"); //去除右大括號
$data=ltrim($data,"["); //去除左括號
$data=rtrim($data,"]"); //去除右括號
經過這樣處理就得到像下面這樣的 json 字串 $data 了 :
{"name":"\u53f0\u7a4d\u96fb","id":"2330","close":"123","volumn":"4425119","meeting":"2014-06-04","election":false,"category":"\u534a\u5c0e\u9ad4"}
這個 $data 便可以傳給 json_decode() 轉成物件了 :
$obj=json_decode($data);
這樣便可以由 $obj 取得唯一的 id 值, 再用 SQL 指令來刪除紀錄 :
$SQL="DELETE FROM stocks WHERE id=".
$obj->id;
$result=mysql_query($SQL, $conn);
完整的 PHP 程式 delete_stocks.php 如下 :
<?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com";
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$input=file_get_contents("php://input");
$data=ltrim($input,'{"data":'); //去除 root 字串
$data=rtrim($data,"}"); //去除右大括號
$data=ltrim($data,"["); //去除左括號
$data=rtrim($data,"]"); //去除右括號
$obj=json_decode($data);
$SQL="DELETE FROM stocks WHERE id=".
$obj->id;
$result=mysql_query($SQL, $conn);
mysql_close();
?>
接下來看新增紀錄, 按 tbar 上的新增鈕會新增一筆空記錄, 觀察 Javascript 控制台/Network 發現並不會發出任何 Request, 因為那只是在本地的 Store 中增加空白的紀錄而已, 沒有內容要 Request 啥? 要等到點擊該空列, 進入編輯模式輸入資料完畢按儲存後, 這筆新紀錄會被標示為 dirty, 才會觸發回存動作. 這時新增的紀錄會放在 Request payload 中, 但
要注意的是, proxy 是向伺服端發出 UPDATE 要求, 而不是 CREATE 要求, 這就是為什麼我們的 CREATE 屬性設為 undefined 的原因, 因為用不到.
例如我們新增一筆友達 :
如果編輯已有的紀錄, 該筆紀錄會被標示為 dirty, 這時也會發出 UPDATE 要求, 而且將 dirty 的紀錄放在 Request Payload 中送出, 例如我們編輯台積電, 其 payload 如下 :
後端 PHP 程式用 file_get_contents("php://input") 收到的原始資料將是一個 JSON 物件字串 :
{"data":[{"name":"\u53f0\u7a4d\u96fb","id":"2330","close":"123","volumn":4425119,"meeting":"2014-06-04T00:00:00","election":false,"category":"\u534a\u5c0e\u9ad4"}]}
或 :
{"data":[{"name":"\u53f0\u7a4d\u96fb","id":"2330","close":"122","volumn":4425114,"meeting":"2014-06-04T00:00:00","election":true,"category":"\u534a\u5c0e\u9ad4"}]}
如同前面刪除記錄時一樣, 我們必須先把 root 字頭 {"data":[ 以及字尾 ]} 去掉, 才能取出 json 物件字串, 再呼叫 json_decode() 將此字串轉成物件, 以便組成 SQL 指令進行更新或新增紀錄.
由於新增資料與更新資料都是由 UPDATE 的 api 發出, 因此
需要先判別是新增還是更新, 再分別製作 INSERT 與 UPDATE 指令. 判別的方法是利用 key 去搜尋是否已有這筆資料, 若無表示為新增, 若有表示為更新. 此處股票代號為唯一, 因此可搜尋此 id 是否存在 :
$SQL="SELECT * FROM stocks WHERE id=".$obj->id;
$result=mysql_query($SQL, $conn);
$count=mysql_num_rows($result);
若 $count 為 0, 表示要新增紀錄; 否則為更新, 完整的 PHP 程式 update_stocks.php 如下 :
<?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com";
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$input=file_get_contents("php://input");
$data=ltrim($input,'{"data":'); //去除 root 字串
$data=rtrim($data,"}"); //去除右大括號
$data=ltrim($data,"["); //去除左括號
$data=rtrim($data,"]"); //去除右括號
$obj=json_decode($data);
$SQL="SELECT * FROM stocks WHERE id=".$obj->id;
$result=mysql_query($SQL, $conn);
$count=mysql_num_rows($result);
if ($count==0) { //新紀錄
$SQL="INSERT INTO `stocks` (`name`,`id`,`close`,`volumn`,`meeting`,".
"`election`,`category`) VALUES('".$obj->name."','".$obj->id."','".
$obj->close."','".$obj->volumn."','".$obj->meeting."','".
$obj->election."','".$obj->category."')";
}
else {
$SQL="UPDATE `stocks` SET name='".$obj->name."',id='".$obj->id."',close='".
$obj->close."',volumn='".$obj->volumn."',meeting='".$obj->meeting.
"',election='".$obj->election."',category='".$obj->category."' ".
"WHERE id=".$obj->id;
}
$result=mysql_query($SQL, $conn);
$handle=fopen("output.txt", "w");
fwrite($handle, $SQL.$result);
fclose($handle);
mysql_close();
?>
注意, 每編輯一筆資料, 就會馬上對後端進行 UPDATE 作業, 成功後左上角的 dirty 標示會消失. 若沒有消失, 表示後端程式沒有執行成功, 通常這是程式錯誤 (例如在測試時) 或無法連線後端伺服器之故.
此例中我在 tbar 左方也放了一個重設鈕, 如果測試完畢要恢復 stocks 資料表的原始紀錄, 按此按鈕會以 Ajax 執行 reset_stocks.php 這個程式, 它會刪除全部紀錄後重新 INSERT, 完整程式如下 :
<?php
header('Content-Type: text/html;charset=UTF-8');
$host="mysql.1freehosting.com";
$username="u911852767_test";
$password="a5572056";
$database="u911852767_test";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$SQL="DELETE FROM stocks";
$result=mysql_query($SQL, $conn); //刪除全部資料
$SQL="INSERT INTO `stocks` (`name`,`id`,`close`,`volumn`,`meeting`,".
"`election`,`category`) VALUES".
"('台積電', '2330', 123, 4425119, '2014-06-04', 0, '半導體'),".
"('中華電', '2412', 96.4, 5249, '2014-06-15', 0, '通信'),".
"('中碳', '1723', 192.5, 918, '2014-07-05', 0, '塑化'),".
"('創見', '2451', 108, 733, '2014-06-30', 0, '模組'),".
"('華擎', '3515', 118.5, 175, '2014-07-20', 1, '主機板'),".
"('訊連', '5203', 97, 235, '2014-05-31', 0, '軟體')";
$result=mysql_query($SQL, $conn);
mysql_close();
echo "資料表重設完成!";
?>
OK, 終於把 GridPanel 搞定了, 從 6/23 去受訓開始至今 7/14, 總共花了三個星期寫了三篇, 一篇就要花掉我一週時間, 看似簡單的設定而已, 但是要來回測試就花時間了. GridPanel 還有一些零星課題沒時間測試, 例如 feature 與 rowExpander 外掛, 但是我覺得目前用不到, 以後有需要時再說吧. 不過我覺得 CellEditing/RowEditing 功能適合用在全部都是文數字欄位場合, 因為沒辦法放大欄位的 TEXTAREA 或 HTML 編輯欄位. 如果要用在工作日誌改版, 可能還是用傳統的 CRUD 分頁方式較適合.
參考資料 :
#
Problem with Store sync post data
#
Ext.data.Store之資料修改(add/modify/remove)
#
getModifiedRecords()怎么用?
#
Example: autoSync in ExtJS 4 Grid.
#
ExtJs 4 get New, Updated and Deleted Records from a Grid Store
#
ExtJS 4: Proxy Calling ‘Create’ Instead of ‘Update’ When Saving Record
#
Managing Grid CRUD In ExtJS4
#
How to retrieve Request Payload
#
EXTJS 4 PHP GET REQUEST PAYLOAD
#
Extjs 4.1, Making JSON array for Store sync