2014年7月2日 星期三

ExtJS 4 測試 : GridPanel (二)

ExtJS 4 的 GridPanel 還真龐大, 要一一測試實在很花時間, 只能挑常用的來測. 本篇是 GridPanel 的續集, 前篇詳見 :

# ExtJS 4 測試 : GridPanel (一)

4.2.2 版的 API :

http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.grid.Panel

在前一篇的測試範例 11, 我們使用 PHP 程式從後端送出資料表的全部紀錄, 結果表格可能會非常的長, 但如果我們在 GridPanel 中設定 height 屬性的話, 由於 GridPanel 是一個容器, 當顯示內容超過表格高度時, 會自動出現垂直捲軸, 如果資料量不大, 用捲軸會比分頁好用, 如下列範例 13 所示 :

測試範例 13 : http://mybidrobot.allalla.com/extjstest/extjs_grid_13.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          autoLoad:true,
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_all.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        height:400,
        forceFit:true
        });
      }); //end of onReady


可見設定 height 屬性後就出現捲軸了. 注意, 這裡必須使用的後端程式是輸出 stocks_list 資料表全部記錄的 get_stocks_list_all.php. 移動捲軸時反應還算 OK, 可能是總共才 865 筆資料的關係. 若有一萬筆資料, 可能反應會變卡卡的.

針對此問題, ExtJS 4 有所謂 BufferedRenderer 設計, 以緩衝繪製方式提高反應速度. 參考 ExtJS 4.2 版解壓縮目錄 examples/grid/infinite-scroll.js 檔的寫法, 將範例 13 改寫為範例 13-1 :

測試範例 13-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_13_1.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          autoLoad:true,
          buffered:true,
          leadingBufferZone:10,
          trailingBufferZone:10,
          scrollToLoadBuffer:1000,
          pageSize:100,
          proxy:{
            type:"ajax",
            url:codebase + "get_stocks_list_all.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel 
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:500,
        height:400,
        forceFit:true,
        loadMask:true
        });
      }); //end of onReady

此例在 GridPanel 要設定 loadMask:true 屬性, 這樣當大幅捲動時才會出現等待 "讀取中" 動畫. 其次, pageSize 與 buffered 一定要設, 而且 pageSize 不要設太小, 否則最前面的紀錄編號會從最後一筆紀錄續編, 很奇怪. 另外, 此例在捲動時會出現水平捲軸, 也很奇怪 (改為 4.2.2 版後就不會).

接著要來測試 Store 的搜尋功能. jQuery UI 的 DataTables 套件就有 Store 的搜尋功能. 在下列範例 14 中, 我們在 GridPanel 的 tbar (上工具列) 放置一個輸入欄位以及搜尋按鈕讓使用者搜尋 Store 中的紀錄 :

測試範例 14 : http://mybidrobot.allalla.com/extjstest/extjs_grid_14.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          autoLoad:true,
          proxy:{
            type:"ajax",
            url:codebase + "get_stocks_list_all.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          });
      var queryStore=function(){
        var target=Ext.getCmp("queryText").getValue();
        if (target=="") {return;}
        store.filterBy(function(record, id) {
          var found=record.get('stock_id').indexOf(target)!=-1 ||
                    record.get('stock_name').indexOf(target)!=-1;
          if (found) {return true;}
          else {return false;}
          });
        }
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        height:400,
        forceFit:true,
        tbar:{
          xtype:'toolbar',
          frame:true,
          border:false,
          padding:2,
          items:[
            {xtype:"textfield",emptyText:"",width:220,id:"queryText"},
            {xtype:"button",text:"搜尋",handler:queryStore},
            {xtype:"button",text:"重整",handler:function(){store.load();}}
            ]
          }
        });
      }); //end of onReady


此處我們是在 tbar 屬性中加入一個文字欄位 (id=queryText), 以及一個搜尋按鈕與一個重整鈕, 當按下搜尋時, 事件處理器會呼叫 queryStore() 方法, 此方法會呼叫 Store 的 filterBy() 方法檢查要搜尋的關鍵字. 它會在 callback 方法中傳入每一個紀錄物件 record 以及在 Store 中之 id. 利用 get() 取得各欄位值後, 再用 indexOf() 檢查是否含有所搜尋之關鍵字, 有找到的話傳回 true 給 filterBy() 方法讓 Store 搜尋出所要的紀錄.

搜尋過後若要回復原狀, 按 "重整" 鈕會呼叫 store.load() 重新載入到 View 中顯示.

使用分頁工具列時也是可以使用這個 Store 搜尋方法, 如下列範例 15 所示 :

測試範例 15http://mybidrobot.allalla.com/extjstest/extjs_grid_15.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},
          pageSize:20,
          proxy:{
            type:"ajax",
            url:codebase + "get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          remoteSort:true
          });
          var queryStore=function(){
                var target=Ext.getCmp("queryText").getValue();
                if (target=="") {return;}
                store.filterBy(function(record, id) {
                    var found=record.get('stock_id').indexOf(target)!=-1 ||
                    record.get('stock_name').indexOf(target)!=-1;
                    if (found) {return true;}
                    else {return false;}
                  });
              }
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        tbar:{
          xtype:'toolbar',
          frame:true,
          border:false,
          padding:2,
          items:[
            {xtype:"textfield",emptyText:"",width:220,id:"queryText"},
            {xtype:"button",text:"搜尋",handler:queryStore},
            {xtype:"tbfill"},   //填充使重整鈕靠右
            {xtype:"button",text:"重整",handler:function(){store.load();}}
            ]
          },
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      }); //end of onReady


注意, 此處使用的是後台分頁程式 get_stocks_list_page.php, 每次按下一頁時, 後端僅傳送該頁紀錄到 Store 中, 因此搜尋時也僅以該頁內容為限, 不會搜尋其他頁之內容. 上圖是在第五頁中搜尋 "豐" 的結果, 共有兩筆, 按 "重整" 鈕或分頁工具列中間的 icon (兩者效果相同), 會重新載入該頁內容. 注意, 這裡我們用 tbfill 填充工具列, 使得 "重整" 鈕被擠到最右邊.

# How to align elements in a toolbar to left, middle, right

事實上, GridPanel 類別有一個 tools 屬性可用來製作重載按鈕, 此屬性產生 Ext.panel.Tool 物件陣列, 並在表格的標題列的右邊產生按鈕. 即使沒有設定 title 屬性, tools 屬性也會產生標題列, 如範例 16 所示 :

測試範例 16http://mybidrobot.allalla.com/extjstest/extjs_grid_16.htm [看原始碼]

    Ext.QuickTips.init();
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},
          pageSize:20,
          proxy:{
            type:"ajax",
            url:codebase + "get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          remoteSort:true
          });
            var queryStore=function(){
                var target=Ext.getCmp("queryText").getValue();
                if (target=="") {return;}
                store.filterBy(function(record, id) {
                    var found=record.get('stock_id').indexOf(target)!=-1 ||
                    record.get('stock_name').indexOf(target)!=-1;
                    if (found) {return true;}
                    else {return false;}
                  });
              }
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        title:"台股",
        tools:[{id:"refresh",qtip:"重載",handler:function(){store.load();}}],

        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        tbar:{
          xtype:'toolbar',
          frame:true,
          border:false,
          padding:2,
          items:[
            {xtype:"textfield",emptyText:"",width:220,id:"queryText"},
            {xtype:"button",text:"搜尋",handler:queryStore}
            ]

          },
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      }); //end of onReady


tools 屬性的 id="refresh" 設定會在表格標題列右邊產生一個更新表格顯示的按鈕, 但必須設定其事件處理選項 handler 為 store.load() 才會有作用. 而 qtip 屬性為按鈕提示語, 跟分頁工具列的提示語一樣, 必須先呼叫 Ext.QuickTips.init() 與以起始化才會有效果.

除了 refresh 這個標題上的按鈕外, tools 屬性還有其他標題列小按鈕可用 :
  1. close : 關閉
  2. minimize : 最小化
  3. maximize : 最大化
  4. restore : 復原
這四個小按鈕在表格中沒啥用處, 只有 refresh 較常用. 除此之外 GridPanel 還有一個 collapsible 屬性也會在表格標題列上放置一個小按鈕, 用來縮合或還原表格, 如下列範例 17 所示 :

測試範例 17 : http://mybidrobot.allalla.com/extjstest/extjs_grid_17.htm [看原始碼]

    Ext.QuickTips.init();
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},
          pageSize:20,
          proxy:{
            type:"ajax",
            url:codebase + "get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          remoteSort:true
          });
     var queryStore=function(){
        var target=Ext.getCmp("queryText").getValue();
       if (target=="") {return;}
       store.filterBy(function(record, id) {
           var found=record.get('stock_id').indexOf(target)!=-1 ||
                          record.get('stock_name').indexOf(target)!=-1;
           if (found) {return true;}
          else {return false;}
          });
        }
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        title:"台股",
        tools:[{id:"refresh",qtip:"重載",handler:function(){store.load();}},
               {id:"close",qtip:"關閉"},
               {id:"minimize",qtip:"最小化"},
               {id:"maximize",qtip:"最大化"},
               {id:"restore",qtip:"復原"},
               ],
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        tbar:{
          xtype:'toolbar',
          frame:true,
          border:false,
          padding:2,
          items:[
            {xtype:"textfield",emptyText:"",width:220,id:"queryText"},
            {xtype:"button",text:"搜尋",handler:queryStore}
            ]
          },
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          }),
        collapsible:true
        });
      }); //end of onReady


上圖是按了縮合鍵的結果, 表格整個被縮起來了, 但再按一下就會恢復. tools 的其他四個按鈕因為沒有設定 handler 方法, 因此怎麼按也不會有作用.

下面接著要來測試欄位模型的渲染器或繪製器 (renderer). GridPanel 的 Data Model 將 data 轉成 Store 物件時, 預設是將 data 的各欄位資料當作字串繪製到 GridView 上, 如下列範例 18 所示 :

測試範例 18http://mybidrobot.allalla.com/extjstest/extjs_grid_18.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name",width:60},
                   {header:"股票代號",dataIndex:"id",width:60},
                   {header:"收盤價 (元)",dataIndex:"close",width:60},
                   {header:"成交量 (張)",dataIndex:"volumn",width:60},
                   {header:"股東會日期",dataIndex:"meeting"},
                   {header:"董監改選",dataIndex:"election",width:50},
                   ];
      //定義原始資料
      var data=[["台積電","2330",123,4425119,"2014-06-04 10:00:00",false],
                ["中華電","2412",96.4,5249,"2014-06-15 14:00:00",false],
                ["中碳","1723",192.5,918,"2014-07-05 09:00:00",true],
                ["創見","2451",108,733,"2014-06-30 14:00:00",false],
                ["華擎","3515",118.5,175,"2014-07-20 08:00:00",true],
                ["訊連","5203",97,235,"2014-05-31 10:00:00",false]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"},
            {name:"meeting"},
            {name:"election"}
            ]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:550,
        forceFit:true
        });
      }); //end of onReady



可見預設是把 data 中的原始資料如實呈現而已. 如果股東會日期只要呈現日期即可, 不需要時間, 或者成交量張數要每一千打個逗點, 該怎麼做呢? 這就要用到自訂的欄位繪製器 (column renderer), 以便將特定欄位的資料繪製成所需要的格式. ExtJS 有兩個常用的內建繪製器函式可以完成這兩個需求 :
  1. Ext.util.Format.dateRenderer() : 日期繪製器, 傳入日期繪製格式字串
  2. Ext.util.Format.numberRenderer() : 數值繪製器, 以 0 作為 placeholder
繪製器 renderer 跟欄位有關, 因此是定義在 columns 的 renderer 屬性中, 其值是一個有傳回值的函式, 它必須傳回要繪製的內容, 除了在欄位 columns 中定義繪製器函式外, 在 Store 的 fields 屬性中, 也要定義此欄位的類型 (int, float, date) 與格式 (主要是日期才需要). 如下列範例 19 所示 :

測試範例 19http://mybidrobot.allalla.com/extjstest/extjs_grid_19.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name",width:60},
                   {header:"股票代號",dataIndex:"id",width:60},
                   {header:"收盤價 (元)",dataIndex:"close",width:60,
                   renderer:Ext.util.Format.numberRenderer("NT$ 0.0")},
                   {header:"成交量 (張)",dataIndex:"volumn",width:60,
                   renderer:Ext.util.Format.numberRenderer("0,000")},
                   {header:"股東會日期",dataIndex:"meeting",
                   renderer:Ext.util.Format.dateRenderer("Y-m-d")},
                   {header:"董監改選",dataIndex:"election",width:50},
                   ];
      //定義原始資料
      var data=[["台積電","2330",123,4425119,"2014-06-04 10:00:00",false],
                ["中華電","2412",96.4,5249,"2014-06-15 14:00:00",false],
                ["中碳","1723",192.5,918,"2014-07-05 09:00:00",true],
                ["創見","2451",108,733,"2014-06-30 14:00:00",false],
                ["華擎","3515",118.5,175,"2014-07-20 08:00:00",true],
                ["訊連","5203",97,235,"2014-05-31 10:00:00",false]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close",type:"float"},
            {name:"volumn",type:"int"},
            {name:"meeting",type:"date",dateFormat:"Y-m-d H:i:s"},
            {name:"election"}
            ]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:550,
        forceFit:true
        });
      }); //end of onReady



其中, 台積電與訊連的收盤價原始資料為整數 123 與 97, 但因欄位類型為 float, 格式為 0.0 (小數一位), 因此繪製結果為 123.0 與 97.0. 格式中的 0 稱為 placeholder, 代表一個位數. 同樣地, 成交量欄位的類型為整數, 格式為 0,000 表示每三位數加一個逗號. 而股東會日期的原始資料經過指定 "Y-m-d" 格式繪製後, 就去除時間了.

更多的內建 renderer 函式參見 Ext.util.Format 類別 :

# http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.util.Format

其中較常用的有 :

  1. Ext.util.Format.uppercase : 大寫
  2. Ext.util.Format.lowercase : 小寫
  3. Ext.util.Format.captalize : 首字母大寫
  4. Ext.util.Format.round : 四捨五入
  5. Ext.util.Format.ellipsis : 刪節
  6. Ext.util.Format.trim : 去除左右兩邊的空白

除了數值與日期格式轉換可以使用上述兩個內建函式來做外, 也可以自建 renderer 函式, 這時 Ext.column.Model 物件會傳入 7 個參數 (見 Packt : Learning ExtJS 4 P216) :
  1. value : 儲存格之值 
  2. rowIndex : 儲存格之列索引
  3. colIndex : 儲存格之欄索引
  4. row : 目前之列物件
  5. view : GridView 物件 (視圖)
  6. store : 儲存物件
  7. metaData : 目前儲存格之後設資料 (元資料, 詮釋資料), 用來調整儲存格樣式

http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.grid.column.Column-cfg-renderer

這 7 個參數只要傳入用得到的即可, 不需全部傳入. 首先來看看 URL 的繪製器, 只要傳入儲存格的值 value 這個參數就夠了, 如下列範例 20 所示 :

測試範例 20http://mybidrobot.allalla.com/extjstest/extjs_grid_20.htm [看原始碼]

    Ext.onReady(function() {
      var add_yahoo_link=function(value){
        var link="<a href='http://tw.stock.yahoo.com/q/q?s=" + value + 
                 "' target='_blank'>" + value + "</a>";
        return link;
        }
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id",
                    renderer:add_yahoo_link},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      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", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}
            ]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady


此例中的 renderer 屬性值為一個自訂方法, 利用傳入之儲存格值製作超連結字串後傳回, 這樣欄位繪製器就會在儲存格中描繪此超連結矣. 這樣就不需要依賴後端程式傳回超連結, 只要傳回原始的字串, 再由前端的 renderer 來描繪成超連結, 如此可以大大地降低後端傳回來的 JSON 檔案之大小, 提升效率.

繪製函式的 callback 傳入參數 metaData 是欄位的後設資料物件, 主要用來設定該欄所有儲存格之 HTML 屬性 (例如 title) 與樣式 (class, style), 此物件有三個屬性 :
  1. style : 儲存格 (td) 的樣式
  2. tdCls : 儲存格 (td) 的樣式類別
  3. tdAttr : 儲存格 (td) 的屬性
下面範例 20-1 演示其用法 :

測試範例 20-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_20_1.htm [看原始碼]

    Ext.onReady(function() {
      var add_yahoo_link=function(value,metaData){
          var link="<a href='http://tw.stock.yahoo.com/q/q?s=" + value + 
                 "' target='_blank'>" + value + "</a>";
        metaData.style="background-color:yellow;";
        metaData.tdAttr="title='前往 Yahoo 股市 (" + value + ")'";
        return link;
        }
      var above_100=function(value,metaData) {
        if (value > 100) {metaData.tdCls="above_100";}
        return value;
        }
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id",
                    renderer:add_yahoo_link},
                   {header:"收盤價 (元)",dataIndex:"close",
                    renderer:above_100},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      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"}
            ]
          });
      //建立 GridPanel 
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady


此例我們利用 metaData 的 style 屬性為股票代號欄位的每一個儲存格設定了黃色的背景, 同時也為表格的 td 元素加上 title 提示語. 另外, 收盤價欄位的描繪器則會在收盤價超過 100 元時套用 一個是先定義好的樣式類別 above_100 :

  <style>
    .above_100 {color:red;font-weight:bold;}
  </style>

注意, renderer 的函式一定要有傳回值, 否則無法描繪. 此處收盤價欄位因為內容不變, 因此仍傳回 value 本身. 但很奇怪的是, font-weight 在 4.2.0 版似乎無效 :

測試範例 20-1GPL : http://mybidrobot.allalla.com/extjstest/extjs_grid_20_1GPL.htm [看原始碼]

2014-07-10 註 : 改為 4.2.2 版後即正常 (即範例 20-1 所示).

除了加上超連結外, renderer 方法也可傳回各種自訂的輸出, 例如將原始資料為數值改成以圖形來描繪, 如下列範例 21 將 0~5 等級的投資評等改用星星圖形來表示, 讓人一目瞭然 :

測試範例 21 : http://mybidrobot.allalla.com/extjstest/extjs_grid_21.htm [看原始碼]

    Ext.onReady(function() {
      var add_yahoo_link=function(value){
        var link="<a href='http://tw.stock.yahoo.com/q/q?s=" + value +
                 "' target='_blank'>" + value + "</a>";
        return link;
        }
      var add_stars=function(value){
        var stars=[];
        for (var i=0; i<value; i++){
          stars.push("<img src='star.gif' style='width:16px;height:16px;'>");
          }
        return stars.join("");;
        }
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id",
                    renderer:add_yahoo_link},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"},
                   {header:"投資評等",dataIndex:"stars",
                    renderer:add_stars}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123,4425119,5],
                ["中華電","2412",96.4,5249,4],
                ["中碳","1723",192.5,918,5],
                ["創見","2451",108,733,3],
                ["華擎","3515",118.5,175,4],
                ["訊連","5203",97,235,3]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"},
            {name:"stars"}
            ]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady


這裡我準備了一個圖檔 star.gif, 依據 stars 欄位的數值, 以迴圈製作代表該數值的 img 元素, 然後傳回給 renderer 去描繪.

另外一種常用的描繪是動作按鈕或超連結, 例如刪除列或編輯列資料, 如下面範例 22 所示 (但我發現行不通) :

測試範例 22 : http://mybidrobot.allalla.com/extjstest/extjs_grid_22.htm [看原始碼]

    Ext.onReady(function() {
      function edit(){
        Ext.Msg.alert("訊息","編輯");
        }
      function del(){
        Ext.MessageBox.confirm(
          "確認訊息",
          "確定要刪除這筆留言?",
          function(btn) {
            if (btn=="yes") {
              //delete data ...
              Ext.MessageBox.alert("訊息", "資料已刪除!");
              }
            }
          );
        }
      var add_yahoo_link=function(value){
        var link="<a href='http://tw.stock.yahoo.com/q/q?s=" + value + 
                 "' target='_blank'>" + value + "</a>";
        return link;
        }
      var add_actions=function(){
        var style="style='width:16px;height:16px;'";
        var actions="<img src='edit.gif' " + style + " onclick='edit()'> " +
                    "<img src='delete.gif' " + style + " onclick='del()'>";
        return actions;
        }
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id",
                    renderer:add_yahoo_link},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"},
                   {header:"動作",dataIndex:"actions",
                    renderer:add_actions}
                   ];
      //定義原始資料
      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", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"},
            {name:"actions"}
            ]
          });
      store.load();
      //建立 GridPanel 
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady



明明都有定義 edit() 與 del() 函式了, 為何點 icon 後卻出現 "not defined" 錯誤? (在 Chrome 按 Ctrl+Shift+J 或 Firefox 按 F12). 看來用 renderer 屬性來製作動作按鈕是沒辦法了.

其實, 除了使用 renderer 屬性來客製化欄位的繪製外, 也可以使用 items 與 xtype 屬性來繪製, 其中 items 用來擺放我們要描繪的元件, 而 xtype 則是設定這些欄位元件的類型. xtype 定義於 Ext.enums.Widget 類別中 :

http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.enums.Widget

其中 actioncolumn 這個 xtype 就是專門用來製作動作按鈕的, 如下列範例 23 所示 :

測試範例 23 : http://mybidrobot.allalla.com/extjstest/extjs_grid_23.htm [看原始碼]

    Ext.QuickTips.init(); //必須! 否則動作按鈕提示 altText 無效
    Ext.onReady(function() {
      var add_yahoo_link=function(value){
        var link="<a href='http://tw.stock.yahoo.com/q/q?s=" + value +
                 "' target='_blank'>" + value + "</a>";
        return link;
        }
      var edit=function(){
        Ext.Msg.alert("訊息","編輯");
        };
      var del=function(grid,rowIndex,colIndex){
        Ext.MessageBox.confirm(
          "確認訊息",
          "確定要刪除這筆留言?",
          function(btn) {
            if (btn=="yes") {
              var store=grid.getStore();  //取得 Store 物件
              var rec=store.getAt(rowIndex); //取得紀錄物件
              var name=rec.get("name");  //取得 name 欄位值
              store.remove(rec);
              Ext.Msg.alert("訊息",name + " 已刪除!");
              }
            }
          );
        };
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id",
                    renderer:add_yahoo_link},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"},
                   {header:"動作",width:60,
                    xtype:'actioncolumn',
                    items:[{icon:"edit.gif",tooltip:"編輯",iconCls:"icon",
                            altText:"編輯",handler:edit},
                           {}, //空一格
                           {icon:"delete.gif",tooltip:"刪除",iconCls:"icon",
                            altText:"刪除",handler:del}]
                    }
                   ];
      //定義原始資料
      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", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"},
            {name:"action"}
            ]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady


此處我們必須準備兩個圖檔 :edit.gif 與 delete.gif, 由於大小都是 30x30, 必須用 iconCls 屬性指定一個樣式, 限制其顯示大小為 16x16, 以免 icon 太大 :

  <style>
    .icon {width:16px;height:16px;}
  </style>

參考 : # http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.grid.column.Action

每一個 icon 是一個 item, 中間我們用了一個空物件來隔開兩個 icon. 兩個 icon 的 handler 屬性用來指定點擊 icon 圖檔時要呼叫的函式, 其中 edit() 部份只是一個 alert 的虛動作, 實際應用時可能是載入一個編輯頁面; 而 del() 則是真正從 Store 中刪除該列紀錄. 利用 callback 方法傳回的 grid 與 rowIndex 取得目前這筆記錄 rec, 再呼叫 Store 的 remove() 方法刪除之. 不過這裡只是刪除記憶體中 Store 的紀錄而已, 並非刪除遠端資料庫的資料. 若要同時刪除遠端資料, 須用 Ajax 驅動後端程式才行.

注意, icon 的提示語屬性 altText 是透過 QuickTip 達成的, 因此必須先呼叫 Ext.QuickTips.init() 起始才會有效果.

除了 actioncolumn 外, 還有六個常用的內建 xtype 欄位 :
  1. numbercolumn : 數值欄位, 包含整數與浮點數 (Ext.grid.column.Number 類別)
  2. datecolumn : 日期欄位 (Ext.grid.column.Date 類別)
  3. booleancolumn : 布林值欄位, 提供 trueText/FalseText 設定真值顯示字串 (Ext.grid.column.Boolean 類別)
  4. rownumberer : 列編號欄位 (Ext.grid.RowNumberer 類別)
  5. checkcolumn : 核取方塊 (checkbox) 欄位 (Ext.grid.column.CheckColumn 類別)
  6. templatecolumn : XTemplate 模板欄位 (Ext.grid.column.TemplateColumn 類別)
使用 xtype 可以簡化設定, 提升效率, 還能取代上面範例 19 中 renderer 所提供的格式化欄位功能, 首先來測試一下 numbercolumn, datecolumn, 以及 booleancolumn 這三種 xtype 用法, 我們把範例 19 改寫為如下範例 24 :

測試範例 24http://mybidrobot.allalla.com/extjstest/extjs_grid_24.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name",width:60},
                   {header:"股票代號",dataIndex:"id",width:60},
                   {header:"收盤價 (元)",dataIndex:"close",width:60,
                    xtype:"numbercolumn",format:"NT$ 0.0"},
                   {header:"成交量 (張)",dataIndex:"volumn",width:60,
                    xtype:"numbercolumn",format:"0,000"},
                   {header:"股東會日期",dataIndex:"meeting",
                    xtype:"datecolumn",format:"Y-m-d"},
                   {header:"董監改選",dataIndex:"election",width:50,
                    xtype:"booleancolumn",trueText:"是",falseText:"否"},
                   ];
      //定義原始資料
      var data=[["台積電","2330",123,4425119,"2014-06-04 10:00:00",false],
                ["中華電","2412",96.4,5249,"2014-06-15 14:00:00",false],
                ["中碳","1723",192.5,918,"2014-07-05 09:00:00",true],
                ["創見","2451",108,733,"2014-06-30 14:00:00",false],
                ["華擎","3515",118.5,175,"2014-07-20 08:00:00",true],
                ["訊連","5203",97,235,"2014-05-31 10:00:00",false]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"},
            {name:"meeting"},
            {name:"election"}
            ]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:550,
        forceFit:true
        });
      }); //end of onReady


可見 renderer 做出的效果, 用 xtype 也行, 而且布林值的真值也可以客製化. 注意, xtype 中 format 屬性設定的方式跟 renderer 的設法是一樣的, 數值部分使用 0 當 placeholder, 日期部份採用 PHP 的格式.

不過, 在 Firefox 30.0 上, 股東會日期用 xtype 卻無法正確繪製 (NaN), 可見有 Bug :


下面範例 24-1 則是加上 rownumberer 欄位, 以及將董監改選欄位之 xtype 由 booleancolumn 改為 checkcolumn 的結果 :

測試範例 24-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_24_1.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"編號",xtype:"rownumberer",width:50},
                   {header:"股票名稱",dataIndex:"name",width:60},
                   {header:"股票代號",dataIndex:"id",width:60,
                    xtype:"templatecolumn",
                    tpl:"<a href='http://tw.stock.yahoo.com/q/q?s={id}' " + 
                        "target='_blank'>{id}</a>"},
                   {header:"收盤價 (元)",dataIndex:"close",width:60,
                    xtype:"numbercolumn",format:"NT$ 0.0"},
                   {header:"成交量 (張)",dataIndex:"volumn",width:60,
                    xtype:"numbercolumn",format:"0,000"},
                   {header:"股東會日期",dataIndex:"meeting",
                    xtype:"datecolumn",format:"Y-m-d"},
                   {header:"董監改選",dataIndex:"election",width:50,
                    xtype:"checkcolumn"},
                   ];
      //定義原始資料
      var data=[["台積電","2330",123,4425119,"2014-06-04 10:00:00",false],
                ["中華電","2412",96.4,5249,"2014-06-15 14:00:00",false],
                ["中碳","1723",192.5,918,"2014-07-05 09:00:00",true],
                ["創見","2451",108,733,"2014-06-30 14:00:00",false],
                ["華擎","3515",118.5,175,"2014-07-20 08:00:00",true],
                ["訊連","5203",97,235,"2014-05-31 10:00:00",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"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:550,
        forceFit:true
        });
      }); //end of onReady


可見列編號欄位與使用 Ext.create("Ext.grid.RowNumberer") 效果是一樣的, 而股票代號欄位也透過模板加上了超連結 (此處我們是在 tpl 屬性中, 於字串值中以大括號套入欄位名稱), 最後的董監改選欄位則變成用核取方塊呈現了, 使用 xtype 真是乾淨俐落! 總之, 在 ExtJS 4 中, xtype 是王道 !

注意, 之前使用 4.1.1 版, 結果發現 checkbox 沒有繪製出來, 如下圖所示 :


但改換 4.2.2 版後即正常了. 使用 Sencha CDN 的 GPL 4.2.0 版也 OK, 如下列範例 24-2 所示 :

測試範例 24-2 : http://mybidrobot.allalla.com/extjstest/extjs_grid_24_2.htm [看原始碼]


沒有留言 :