2014年7月23日 星期三

好大喜功的教改

一免放榜後無校可讀的成績優異學生, 在歷經月餘衝刺後, 終於如願以償考上南一中, 還是全國狀元, 回過頭來批免試入學是個笑話, 教育局官員說不能因為一人改規則云云 ...

# 特招狀元 批免試是個笑話


關於十二年國教免試入學, 相信全國所有有幸成為白老鼠的家長, 早已經 X 到無力再罵, 其實我也不想再批, 但是這位陳同學講出了大家的心聲, 教育官員的回應仍然是搞不清楚自己捅出啥樓子, 所以又讓我不吐不快.
  1. 說是免試, 但又要會考, 又要特招, 這不是笑話, 難道是童話?
  2. 說不能因為一人改規則, 這根本搞不清楚自己為什麼被罵. 
  3. 12 年國教時機不成熟硬上, 根本就是馬英九好大喜功的結果.
  4. 多元學習怪象 : 一大堆國中生為志工時數到市立圖書館排隊
若要就近入學, 又要免試, 當然就要像九年國教國小升國中那樣以戶籍為準, 自己揭櫫的理想根本與事實背道而馳, 還好意思講這麼大聲, 我看這不會笑死人, 而是會讓人得精神分裂症. 其實掛羊頭賣狗肉本來就是我們文化的一環, 我們對造假還振振有辭的事情早就習以為常了, 不是嗎?


台北之行

為了今年的評鑑, 周日早上從鄉下回高雄, 搭 10 點自強號北上, 下午三點到台北後, 先去重慶南路書店街逛, 看到這本蔡律師寫的 "612 包租公", 看到一半不禁掩卷嘆息, 如果十幾年前沒把海山高工附近的那棟小房子賣掉, 我也早就是捷運站附近的包租公了! 都怪當年那個陳進興, 害我對當房東興趣缺缺, 便急著把它賣掉, 真是愚蠢之極. 總之, 我就是沒有橫財運啦! 難怪樂透零星買到現在, 連個最小獎都沒中過!

傍晚想搭捷運到小阿姨在永安市場站附近的火鍋店, 呵呵, 快兩年沒來台北了, 台北轉運站密密麻麻的地下道, 搞得我暈頭轉向, 我是一路問人才搞清楚, 原來要先搭往新店的淡水線到古亭, 再轉搭往南勢角方向即可到永安市場. 但下一次去台北保證又會忘記. 小阿姨說要去板橋店看看, 我騎表弟的機車跟在她後面, 這是離開台北這麼多年, 第一次在原本熟悉的中和板橋地面騎機車亂竄, 但往日的回憶卻已依稀.

晚上回到小阿姨在新店安新路的新家, 見到了 9 個月大可愛的小侄子, 抱著不怕生很愛笑的他, 我跟阿姨說, 時光過得可真快, 似乎這樣抱著菁菁好像不過是昨天的事啊! 一轉眼卻已是個小姑娘了.   

週一週二就去執行公務, 就是評鑑啦! 順便認識一下台北的同事, 平常業務往來只靠電話或視訊, 比較虛擬. 週二麥德姆颱風逼近, 取消午休進行講評結束就去趕高鐵, 萬一停開就回不了高雄了.

2014年7月18日 星期五

垃圾與黃金

最近教育部長因為論文引用問題下台, 造成台灣學術聲譽受損, 今天看了這篇 :

#  科技部:證據讓陳氏兄弟混不下去
#  教授權益案 多半與論文有關
#  論文審查案 學者盼導正風氣

我的感想是, 這不是台灣學術圈普遍的現象嗎? 為何大驚小怪? 老外實在太白目了. 有評論表示, 教育圈重視論文發表數量, 重量不重質, 教育部長不思改善遭到反噬, 能說冤枉嗎?

這讓我想起 1905 年愛因斯坦發表的六篇論文, 其中刊登在物理年鑑的 "論動體的電動力學" 是其狹義相對論的濫觴, 全篇沒有引用任何一篇其他論文, 但卻是開啟人類視野的劃時代著作. 對比現在一堆的垃圾論文, 這才是黃金. 當然, 百年才出一個愛因斯坦, 只是, 現在還有多少人願意像愛因斯坦那樣, 十年磨一劍呢? (還要不要升教授啊)

現在的學術論文有嚴格的審查規定, 引用他人著作務必註明 (citation). 但是學術升等制度卻讓聰明人找到漏洞, 每年製造了為數可觀的論文, 並透過這些漏洞互相引用, 堆高學術聲望的假象, 然後在教授升等或研究計畫申請方面得到好處. 總之, 都是為了 ~~~ 錢.

這真的是可惜了. 聰明人愛錢其實有更好且名正言順的管道, 要嘛做生意 要嘛研究股票投資, 不需要這麼費腦力去搞專業的學術研究. 我深深覺得, 人會出問題, 通常是因為對的人放在錯誤的位置上, 或者, 錯的人放在對的位置上.

「教授要名要錢不要臉,學生看臉看勢不看書」, 唉,  這世道.


2014年7月17日 星期四

ExtJS 4 測試 : Panel 類別與佈局

測完 GridPanel, 本來想前進到 TreePanel, 但想到還沒對這些面板的老祖先 Ext.panel.Panel 類別做個測試, 感覺不太踏實, 因此擱下 TreePanel, 先來看看 Panel 類別以及與其息息相關的版面佈局 (Layout), 其 API 參見 :

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

Panel 類別是一個容器元件 (container), 其 xtype 為 panel, 繼承關係如下 :

Ext.Component
   |__ Ext.container.AbstractContainer
            |__ Ext.container.Container
                     |__ Ext.panel.AbstractPanel
                             |__ Ext.panel.Panel
                                     |__ Ext.panel.Table
                                     |        |__ Ext.grid.Panel
                                     |        |__ Ext.tree.Panel
                                     |__ Ext.tab.Panel
                         

測試網站故障

這兩天無法連線我的測試網站 http://mybidrobot.allalla.com/, 甚至無法以檔案總管管理後端檔案, 已經向 1freehosting 申告. 但其他功能似乎還正常, 因此趕緊備份整個檔案下載.

我看還是用 Google App Engine 比較保險, 它幾乎不會當機, 但原先用 PHP 寫的後端程式全部要改用 Python 寫就有點麻煩而且費時間.

ExtJS 的設定有許多眉眉角角, 昨天本來心生倦意, 想要重回 jQuery UI 懷抱, 複習了一下 "鋒利的 jQuery", 感覺 jQuery 還是比較直覺, 難怪這麼受歡迎.

本周日要上台北參加評鑑, 測試工作要暫停.

[2014-07-18] 今天連線已恢復正常, 改用 GAE 後台以後有空再說.


2014年7月15日 星期二

ExtJS 4 測試 : TabPanel 中的 GridPanel

今天想說提早測試一下未來使用 ExtJS 給工作日誌改版時最重要的應用 : Grid within Tab Panel, 所以草草地根據下面四篇舊作寫了一個測試, 經過一番折騰後, 終於證實可行 :
  1. ExtJS 4 測試 : GridPanel (一)
  2. ExtJS 4 測試 : GridPanel (二)
  3. ExtJS 4 測試 : GridPanel (三)
  4. ExtJS 4 測試 : 頁籤面板 TabPanel
測試範例 1 : http://tony1966.xyz/test/extjstest/grid_in_tab_1.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",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]];
      var store1=Ext.create("Ext.data.ArrayStore", {
          autoLoad:true,
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"},
            {name:"meeting"},
            {name:"election"}
            ]
          });
      var store2=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"}
            ]
          }); 
      var grid1=Ext.create("Ext.grid.Panel",{
        title:"台股 (近端資料)",  //一定要設 title
        columns:columns,
        store:store1,
        width:500,
        forceFit:true
        });
      var grid2=Ext.create("Ext.grid.Panel",{
        title:"台股 (遠端資料)",  //一定要設 title
        columns:columns,
        store:store2,
        width:500,
        forceFit:true
        });
      var tp=Ext.create("Ext.TabPanel",{
        title:"台股",
        width:600,
        height:300,
        style:"margin:10px;",
        frame:true,
        autoScroll:false,
        defaults:{bodyPadding:10},
        items:[
          grid1,
          grid2,
          {title:"管理",html:"管理"}
          ],
        renderTo:Ext.getBody()
        });         
      }); //end of onReady





測試範例 2http://tony1966.xyz/test/extjstest/grid_in_tab_2.htm [看原始碼]  

參考資料 :

# extjs tabpanel 中添加gridpanel如何实现?
# ExtJs TabPanel Scrollbar behaviour if Tab items bigger than tab size




2014年7月9日 星期三

姐姐的新手機

姐姐的手機用了四年多, 雖然還堪用, 為了慶祝她考上美術班, 所以中午跑去林森路的門市, 用爸爸的手機門號 2G 升 3G 的綁約購機優惠 1000 元方案, 買了鴻海入門款的 InFocus M210, 月租 183 型只要 1790, 搭配申請來電答鈴, 每月帳單再扣 100 元為期 1 年 (要記得一年後要停租來電答鈴), 也就是下月起每月月租最低是 183-100=83 元, 還蠻划算的, 相當於又省了 1200, 這樣購機成本就降到 590 元了.

還好昨天姊姊有看到 DM 上有這款, 要不然就會去 InFocus 官網用 3280 價格買了. 據說 2G 將陸續關台, 所以升 3G 是必然的, 趁著鼓勵升 3G 還有優惠就升較划算. 另外也問了客服如意卡升 3G 問題, 摘要如下 :
  1. 一人名下只能有一個 2G, 一個 3G 預付卡限制在 2014/5/13 後已改變, 2G 如意卡可到櫃台免費直接升 3G 如意卡, 但條件是不能再申請退回 2G 卡. 而且年底前還贈送 300 元通話費 (優先扣), 但需半年內用完.
  2. 3G 如意卡若要上網, 不要在櫃台升級時開通 (只開語音即可), 要使用時再撥 928 客服線上申請開通上網. 計量計費方式可用 100M 100 元或 1GB 180 元, 須在開通後 60 天內用完, 用不完可在截止前去電 928 客服展延 60 天.
晚上幫姐姐設定新手機, 還申請了一個新的 GMAIL 帳號. 換手機還真麻煩. 最後接上電腦卻發現沒出現 USB 驅動, 查了一下網路, 才知道 XP 電腦必須安裝 Media Player 11 才會安裝 USB 驅動程式 MTP, Win7 以上則不需要.


關於外勞

為了阿蘭照料問題, 晚上找出佩蓉媽咪的電話, 請教她關於聘用外勞問題, 摘要如下 :
  1. 新申請約需半年作業時間, 承接的則較快, 只要仲介手續辦好即可就任.
  2. 月薪約 21000 (泰籍較高, 約 24000), 手續費新申請 15000, 承接 12000.
  3. 所需證件 : 殘障手冊正本, 戶口名簿, 身分證影本.
計算下來一年約 26 萬元之譜, 是不小的負擔. 但考量尊親年事漸高, 照料阿蘭已力不從心, 而我離退休卻還要十幾年, 緩不濟急. 兩年前在 YAMAHA 聽佩蓉媽咪說她在仲介工作, 就未雨綢繆跟她要了名片. 身為兄長, 這是必須挑起的擔子.


2014年7月8日 星期二

麻婆豆腐

今天去全聯買菜時,特地去架上找上回看到的豆瓣醬,但忘記啥牌子,費了些時間終於找到這瓶海霸王的豆瓣醬,很便宜 (才 56 元而已),再買三盒大漢家常豆腐 (31元),今天要來實驗煮麻婆豆腐。



作法超簡單, 先將兩塊豆腐切塊後放進小燉鍋, 切兩條蔥末灑在上面, 再舀三湯匙豆瓣醬, 蓋上鍋蓋小火慢燉 10 分鐘即可. 當然中途要打開來攪拌一下, 這樣豆瓣醬才會入味到豆腐丁裡去. 我不加絞肉, 因為光是豆瓣醬味道就夠了.

實驗結果, 小狐貍們讚不絕口, 全部吃光光. 唯一的改進空間是, 這瓶豆瓣醬不是超辣那型的, 兩塊豆腐似乎三湯匙味道弱了些, 下回用四湯匙看看.



想做這道菜是上回陪姐姐去補習班試聽時, 在 7-11 吃午餐, 居然有麻婆豆腐便當, 我就好奇買來吃看看, 哇, 還不錯吃咧 (對我而言是辣了些), 我觀察原料超簡單, 只要豆腐, 蔥, 以及豆瓣醬即可. 姐姐說下次再煮這道. 呵呵, 這就是我這原本不近庖廚的男人會愛上煮菜的原因. 


ExtJS 4 測試 : GridPanel (三)

這兩周與 GridPanel 奮戰終於要近尾聲了, 剩下可編輯的表格測完就大功告成. 前面兩篇參見 :
  1. ExtJS 4 測試 : GridPanel (一)
  2. 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 表格可編輯, 必須在兩個地方動手腳 :
  1. 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"
  2. 欄位模式 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 加以指定, 常用有下列四種 :
  1. checkboxfield : 核取方塊
  2. numberfield : 數字調整器, 可使用 step 屬性指定步階值
  3. combo : 下拉式選單
  4. 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 所示 :

測試範例 26http://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 測試尚未成功, 最近要忙評鑑 (我當委員), 所以暫停一下 ....

測試範例 29http://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