顯示具有 ExtJS 標籤的文章。 顯示所有文章
顯示具有 ExtJS 標籤的文章。 顯示所有文章

2019年11月4日 星期一

Python 學習筆記 : Django 2 測試 (四) : 本機前端框架環境配置

由於在前一篇前端網頁框架模板測試中發現 EasyUI 最新版的 CDN 檔案似乎有 Bug, 資料表格 datagrid 的表頭與表身寬度不齊會錯開, 反而以前下載的舊版不會, 可見得 CDN 雖然方便, 但可能會因為版本差異而使網頁外觀不符需求, 因此準備一份本機端的框架環境有其必要性.

本系列之前的測試筆記參考 :

# Python 學習筆記 : Django 2 測試 (一) : 請求與回應處理
# Python 學習筆記 : Django 2 測試 (二) : 模板基礎與靜態檔案
# Python 學習筆記 : Django 2 測試 (三) : 模板的匯入與繼承

本篇主要是延續上面的第三篇測試, 差別主要是將框架函式庫來源從 CDN 改為本機環境而已.

首先說明目錄結構, 我在 static 下分別建立 bootstrap, jqueryui, easyui, 以及 extjs 這四個框架子目錄以放置本地框架函式庫, 結構如下 :

mysite (上層專案目錄)
     |____ manage.py
     |____ mysite  (下層專案目錄)
     |              |____ __init__.py
     |              |____ settings.py
     |              |____ urls.py
     |              |____ views.py  (自行添加)
     |              |____ wsgi.py
     |____ templates (網頁模板目錄)
     |              |____ bootstrap.htm
     |              |____ jqueryui.htm
     |              |____ easyui.htm
     |              |____ extjs.htm
     |____ static (靜態檔案目錄)
                    |____ bootstrap
                    |____ jqueryui
                    |____ easyui
                    |____ extjs




靜態目錄 static 底下的結構如下 :







除了 ExtJS 外, 其他框架均以 jQuery 為基礎, 所以它們每一個框架都有相同的一份 jquery.min.js, 這樣方便開發單一框架專案時可順利分割. 各框架的版本如下 :
  1. jQuery v3.4.1
  2. jQuery UI v1.12.1
  3. jQuery DataTables v1.10.20
  4. Bootstrap v3.3.7
  5. EasyUI 1.4.1
  6. ExtJS v4.2.2
本篇測試比上一篇多加了 ExtJS 框架的兩個測試, 其實要改的部份主要是 template 底下各框架的模板網頁, 即 bootstrap.htm, jqueryui.htm, easyui.htm, 以及新加的 extjs.htm. 另外就是主題佈景測試需更改 Link 元素的 href 屬性, 套用本地檔案時也需要修改. 其餘的測試網頁均不變, 此處不再列出, 參考上一篇 "模板的匯入與繼承".


一. 修改 /template 下的四個框架模板網頁 : 

HTML 基礎模板網頁 base.htm 不變, 內容如下 :

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>{% block title %}{% endblock %}</title>
  {% block link %}{% endblock %}
  {% block script %}{% endblock %}
  <style>
  {% block style %}{% endblock %}
  </style>
</head>
<body>
  {% block body %}{% endblock %}
</body>
</html>


四個框架模板都繼承自 base.htm, 由於是本地供檔, 以此需使用 load static 指令匯入靜態目錄位置. 注意, 由於 extends 指令必須放在首位, 所以 load static 放在第二位 :


1. jQueryui 模板網頁 :

<--! jqueryui.htm -->
{% extends "base.htm" %}
{% load static %}
{% block link %}
  <link id="theme" rel="stylesheet"  href='{% static "jqueryui/themes/hot-sneaks/jquery-ui.css" %}'>
  <link rel="stylesheet" href='{% static "jqueryui/jquery.dataTables.min.css" %}'>
{% endblock %}
{% block script %}
  <script src='{% static "jqueryui/jquery.min.js" %}'></script>
  <script src='{% static "jqueryui/jquery-ui.min.js" %}'></script>
  <script src='{% static "jqueryui/jquery.dataTables.min.js" %}'></script>
{% endblock %}


2. EasyUI 模板網頁 : 

<--! easyui.htm -->
{% extends "base.htm" %}
{% load static %}
{% block link %}
  <link id="theme" rel="stylesheet" href='{% static "easyui/themes/default/easyui.css" %}'>
  <link rel="stylesheet" href='{% static "easyui/themes/icon.css" %}'>
{% endblock %}
{% block script %}
  <script src='{% static "easyui/jquery.min.js" %}'></script>
  <script src='{% static "easyui/jquery.easyui.min.js" %}'></script>
  <script src='{% static "easyui/locale/easyui-lang-zh_TW.js" %}'></script>
{% endblock %}


3. Bootstrap 模板網頁 : 

<--! bootstrap.htm -->
{% extends "base.htm" %}
{% load static %}
{% block link %}
  <link rel="stylesheet" href='{% static "bootstrap/bootstrap.min.css" %}'>
  <link rel="stylesheet" href='{% static "bootstrap/bootstrap-theme.min.css" %}'>
  <link rel="stylesheet" href='{% static "bootstrap/bootstrap-table.min.css" %}'>
{% endblock %}
{% block script %}
  <script src='{% static "bootstrap/jquery.min.js" %}'></script>
  <script src='{% static "bootstrap/bootstrap.min.js" %}'></script>
  <script src='{% static "bootstrap/bootstrap-table.min.js" %}'></script>
{% endblock %}


4. ExtJS 模板網頁 :

使用 ExtJS 需載入 ext-all.css 與 ext-all.js 以及語言檔 ext-lang-zh_TW.js, 我使用的是五年前的舊版本 v4.2.2 GPL 版 (之前買的書也是 v4.1 版), 參考 :

# ExtJs 環境部署

<--! extjs.htm -->
{% extends "base.htm" %}
{% load static %}
{% block link %}
<link id="theme" rel="stylesheet" href='{% static "extjs/resources/css/ext-all.css" %}'>
{% endblock%}
{% block script %}
<script type="text/javascript" src='{% static "extjs/ext-all.js" %}'></script>
<script type="text/javascript" src='{% static "extjs/locale/ext-lang-zh_TW.js" %}'></script>
{% endblock%}

注意, 遷就之前的主題佈景測試範例使用 Javascript 切換, 因此這裡 id="theme" 在下面的佈景切換測試中還用不到.


二. 修改測試首頁 home.htm 

為了新增 ExtJS 的兩項測試, 需修改首頁 /templates/home.htm :

<--! home.htm -->
<!DOCTYPE html>
<html>
<head>
  <title>前端框架模板模板測試</title>
  <meta charset="utf-8">
</head>
<body>
  <h3>前端框架模板測試</h3>
  <ul>
    <li><a href="/jqui_test_tabs">jQueryUI 頁嵌面板 Tabs</a></li>
    <li><a href="/jqui_test_themes">jQueryUI 主題布景切換 Themes</a></li>
    <li><a href="/jqui_test_datatables_array">jQueryUI 資料表格 DataTables (陣列)</a></li>
    <li><a href="/jqui_test_datatables_ajax">jQueryUI 資料表格 DataTables (Ajax)</a></li>
    <li><a href="/ezui_test_tabs">EasyUI 頁嵌面板 Tabs</a></li>
    <li><a href="/ezui_test_themes">EasyUI 主題布景切換 themes</a></li>
    <li><a href="/ezui_test_datagrid_ajax">EasyUI 資料表格 Datagrid (Ajax)</a></li>
    <li><a href="/bootstrap_test_table_html">Bootstrap 表格 (HTML)</a></li>
    <li><a href="/bootstrap_test_table_ajax">Bootstrap 表格 (Ajax)</a></li>
    <li><a href="/bootstrap_test_jumbotron">Bootstrap 主題 (jumbotro)</a></li>
    <li><a href="/extjs_test_themes">ExtJS 主題布景切換 themes</a></li>
    <li><a href="/extjs_test_grid_panel">ExtJS 資料表格 GridPanel</a></li>
  </ul>
</body>
</html>

藍色部分即所新增的兩個 ExtJS 測試.


三. 修改主題佈景測試網頁 : 

因 jQuery UI 與 EasyUI 提供主題佈景, 在上一篇測試中直接取用 CDN 供檔, 但若改為本地供檔時, 網頁中的 Javascript 程式需進行修改, 原先 jqui_test_themes.htm 在 CDN 版本是這樣寫的 :

    $(document).ready(function(){
      $("#datepicker").datepicker();
      $("#themes").selectmenu();
      $("#themes").val("hot-sneaks");
      $("#themes").selectmenu("refresh");
      $('#themes').on('selectmenuchange', function() {
        var theme=$(this).val();
        var href="https://code.jquery.com/ui/1.12.0/themes/" + theme +
                 "/jquery-ui.min.css";
        $("#theme").attr("href", href);
        });

此處由本地供檔要改為如下 :

    $(document).ready(function(){
      $("#datepicker").datepicker();
      $("#themes").selectmenu({width:180});
      $("#themes").val("hot-sneaks");
      $("#themes").selectmenu("refresh");
      $('#themes').on('selectmenuchange', function() {
        var theme=$(this).val();
        var href='{% static "jqueryui/themes/" %}' + theme + "/jquery-ui.min.css";
        $("#theme").attr("href", href);
        });
      });

此處用到 static 指令, 因此在網頁最前面需先用 load static 指令取得靜態目錄位置, 修改後的測試網頁如下 :

{% extends "jqueryui.htm" %}
{% load static %}
{% block style %}
    body {
      font-family: Arial, Helvetica, sans-serif;
      font-size:10px;
      }
{% endblock%}
{% block body %}
  <select id="themes">
    <option value="base">base</option>
    <option value="black-tie">black-tie</option>
    <option value="blitzer">blitzer</option>
    <option value="cupertino">cupertino</option>
    <option value="dark-hive">dark-hive</option>
    <option value="dot-luv">dot-luv</option>
    <option value="eggplant">eggplant</option>
    <option value="excite-bike">excite-bike</option>
    <option value="flick">flick</option>
    <option value="hot-sneaks" selected>hot-sneaks</option>
    <option value="humanity">humanity</option>
    <option value="le-frog">le-frog</option>
    <option value="mint-choc">mint-choc</option>
    <option value="overcast">overcast</option>
    <option value="pepper-grinder">pepper-grinder</option>
    <option value="redmond">redmond</option>
    <option value="smoothness">smoothness</option>
    <option value="south-street">south-street</option>
    <option value="start">start</option>
    <option value="sunny">sunny</option>
    <option value="swanky-purse">swanky-purse</option>
    <option value="trontastic">trontastic</option>
    <option value="ui-darkness">ui-darkness</option>
    <option value="ui-lightness">ui-lightness</option>
    <option value="vader">vader</option>
  </select><br><br>
  <script>
    $(document).ready(function(){
      $("#datepicker").datepicker();
      $("#themes").selectmenu({width:180});
      $("#themes").val("hot-sneaks");
      $("#themes").selectmenu("refresh");
      $('#themes').on('selectmenuchange', function() {
        var theme=$(this).val();
        var href='{% static "jqueryui/themes/" %}' + theme + "/jquery-ui.min.css";
        $("#theme").attr("href", href);
        });
      });
  </script>
{% endblock%}

我覺得這 selectmenu 似乎怪怪的, 選項像清單一樣全部打開不會收闔, 之前測試不會, 難道是與 datepicker 犯沖嗎? 參考 :

# jQuery UI 學習筆記 (十) : 下拉式選單 (Selectmenu)
# jQuery UI 學習筆記 (一) : 主題布景 (Themes)

其實 jQuery UI 的主題佈景中我最喜歡的只有 hot-sneaks, 用在公司的專案都是使用此佈景.

原先 ezui_test_themes.htm 在 CDN 版本是這樣寫的 :

    $(document).ready(function() {
      $("#theme_sel").combobox({
        onSelect:function(rec){
          var css="http://www.jeasyui.com/easyui/themes/" +
                  rec.value + "/easyui.css";
          $("#theme").attr("href", css);
          }
        });
      });

此處由本地供檔要改為如下 :

    $(document).ready(function() {
      $("#theme_sel").combobox({
        onSelect:function(rec){
          var css='{% static "easyui/themes/" %}' + rec.value + "/easyui.css";
          $("#theme").attr("href", css);
          }
        });
      });

這裡同樣在網頁最前面需先用 load static 指令取得靜態目錄位置, 修改後的測試網頁如下 :

<!--ezui_test_themes.htm-->
{% extends "easyui.htm" %}
{% load static %}
{% block style %}
    body {
      font-family: Arial, Helvetica, sans-serif;
      font-size:10px;
      }
{% endblock%}
{% block body %}
  <select id="theme_sel" class="easyui-combobox" style="width:120px" panelHeight="320">
    <option value="default" selected>default</option>
    <option value="gray">gray</option>
    <option value="black">black</option>
    <option value="bootstrap">bootstrap</option>
    <option value="metro">metro</option>
    <option value="metro-blue">metro-blue</option>
    <option value="metro-gray">metro-gray</option>
    <option value="metro-green">metro-green</option>
    <option value="metro-orange">metro-orange</option>
    <option value="metro-red">metro-red</option>
    <option value="ui-cupertino">ui-cupertino</option>
    <option value="ui-dark-hive">ui-dark-hive</option>
    <option value="ui-pepper-grinder">ui-pepper-grinder</option>
    <option value="ui-sunny">ui-sunny</option>
  </select><br><br>
  <input id="datebox1" type="text" class="easyui-datebox">
  <script>
    $(document).ready(function() {
      $("#theme_sel").combobox({
        onSelect:function(rec){
          var css='{% static "easyui/themes/" %}' + rec.value + "/easyui.css";
          $("#theme").attr("href", css);
          }
        });
      });
  </script>
{% endblock%}

藍色部分為修改或增加的部分, 其餘不變.


四. 新增兩個 ExtJS 測試網頁 :

在 templates 目錄下新增 extjs_test_themes.htm (主題佈景測試) 與 extjs_test_grid_panel. htm (資料表格) 兩個測試網頁, 其中主題佈景測試 取自之前的 ExtJS 測試, 參考 :

# ExtJS 4 測試 (二) : 主題佈景切換 (Look and Feel)

<--! extjs_test_themes.htm -->
{% extends "extjs.htm" %}
{% load static %}
{% block style %}
   body {padding:20px;}
{% endblock%}
{% block body %}
  <script type="text/javascript">
    Ext.onReady(function() {
      var btn_alert=new Ext.Button({
          text : "訊息框",
          width : 100,
          renderTo : Ext.getBody(),
          handler : function(btn) {
            Ext.MessageBox.alert("通知訊息", "您好! Hello World!");
            }
          });
      var neptune=new Ext.Button({
          text : "Neptune",
          width : 100,
          renderTo : Ext.getBody(),
          handler : function() {
            var url='{% static "extjs/resources/css/ext-all-neptune.css" %}';
            Ext.util.CSS.swapStyleSheet("theme",url);
            }
          });
      var gray=new Ext.Button({
          text : "Gray",
          width : 100,
          renderTo : Ext.getBody(),
          handler : function() {
            var url='{% static "extjs/resources/css/ext-all-gray.css" %}';
            Ext.util.CSS.swapStyleSheet("theme",url);
            }
          });
      var access=new Ext.Button({
          text : "Access",
          width : 100,
          renderTo : Ext.getBody(),
          handler : function() {
            var url='{% static "extjs/resources/css/ext-all-access.css" %}';
            Ext.util.CSS.swapStyleSheet("theme",url);
            }
          });
      var Default=new Ext.Button({
          text : "Default",
          width : 100,
          renderTo : Ext.getBody(),
          handler : function() {
            var url='{% static "extjs/resources/css/ext-all.css" %}';
            Ext.util.CSS.swapStyleSheet("theme",url);
            }
          });
      }); //end of onReady
  </script>
{% endblock%}


而資料表格測試 extjs_test_grid_panel. htm 則參考下列的範例 1 :

# ExtJS 4 測試 : GridPanel (一)

<--! extjs_test_grid_panel. htm -->
{% extends "extjs.htm" %}
{% block style %}
   body {padding:20px;}
{% endblock%}
{% block body %}
  <div id="grid"></div>
  <script type="text/javascript">
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,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",{
        title:"台股",
        columns:columns,
        store:store,
        renderTo:"grid",
        autoHeight:true,
        width:450
        });
      }); //end of onReady
  </script>
{% endblock%}


五. 修改 urls.py 與 views.py :

路由程式 urls.py 新增兩個 URL (藍色部分) : 

#urls.py
from django.contrib import admin
from django.urls import path
import mysite.views as views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('jqui_test_tabs/', views.jqui_test_tabs),
    path('jqui_test_tab2/', views.jqui_test_tab2),
    path('jqui_test_tab3/', views.jqui_test_tab3),
    path('jqui_test_themes/', views.jqui_test_themes),
    path('jqui_test_datatables_array/', views.jqui_test_datatables_array),
    path('jqui_test_datatables_ajax/', views.jqui_test_datatables_ajax),
    path('get_stocks_list/', views.get_stocks_list),
    path('ezui_test_tabs/', views.ezui_test_tabs),
    path('ezui_test_tab2/', views.ezui_test_tab2),
    path('ezui_test_tab3/', views.ezui_test_tab3),
    path('ezui_test_themes/', views.ezui_test_themes),
    path('ezui_test_datagrid_ajax/', views.ezui_test_datagrid_ajax),
    path('get_stocks_list_ezui/', views.get_stocks_list_ezui),
    path('bootstrap_test_table_html/', views.bootstrap_test_table_html),
    path('bootstrap_test_table_ajax/', views.bootstrap_test_table_ajax),
    path('bootstrap_test_jumbotron/', views.bootstrap_test_jumbotron),
    path('extjs_test_themes/', views.extjs_test_themes),
    path('extjs_test_grid_panel/', views.extjs_test_grid_panel),
    path('', views.home),
]

視圖處理程式 views.py 新增兩個函數 (藍色部分) :

#views.py
from django.shortcuts import render
from django.http import HttpResponse
from django.http import JsonResponse
from datetime import datetime

def jqui_test_tabs(request):
    return render(request, 'jqui_test_tabs.htm', {})

def jqui_test_tab2(request):
    tab=2
    return render(request, 'jqui_test_tab2.htm', locals())

def jqui_test_tab3(request):
    return HttpResponse("<h3>這是頁籤 3 (直接回應)</h3>")

def jqui_test_themes(request):
    return render(request, 'jqui_test_themes.htm', {})

def jqui_test_datatables_array(request):
    return render(request, 'jqui_test_datatables_array.htm', {})

def jqui_test_datatables_ajax(request):
    return render(request, 'jqui_test_datatables_ajax.htm', {})

def get_stocks_list(request):
    data={"aaData":[{"stock_name":"台積電",
                    "stock_id":"2330",
                    "close":111.5,
                    "volumn":19268},
                   {"stock_name":"中華電",
                    "stock_id":"2412",
                    "close":95.1,
                    "volumn":7096},
                   {"stock_name":"中碳",
                    "stock_id":"1723",
                    "close":145.0,
                    "volumn":317},
                   {"stock_name":"創見",
                    "stock_id":"2451",
                    "close":104.0,
                    "volumn":459},
                   {"stock_name":"華擎",
                    "stock_id":"3515",
                    "close":104.0,
                    "volumn":95},
                   {"stock_name":"訊連",
                    "stock_id":"5203",
                    "close":98.5,
                    "volumn":326}]}
    return JsonResponse(data)

def ezui_test_tabs(request):
    return render(request, 'ezui_test_tabs.htm', {})

def ezui_test_tab2(request):
    tab=2
    return render(request, 'ezui_test_tab2.htm', locals())

def ezui_test_tab3(request):
    return HttpResponse("<h3>這是頁籤 3 (直接回應)</h3>")

def ezui_test_themes(request):
    return render(request, 'ezui_test_themes.htm', {})

def ezui_test_datagrid_ajax(request):
    return render(request, 'ezui_test_datagrid_ajax.htm', {})

def get_stocks_list_ezui(request):
    data={"totals":"6",
          "rows":[{"stock_name":"台積電",
                    "stock_id":"2330",
                    "close":111.5,
                    "volumn":19268},
                   {"stock_name":"中華電",
                    "stock_id":"2412",
                    "close":95.1,
                    "volumn":7096},
                   {"stock_name":"中碳",
                    "stock_id":"1723",
                    "close":145.0,
                    "volumn":317},
                   {"stock_name":"創見",
                    "stock_id":"2451",
                    "close":104.0,
                    "volumn":459},
                   {"stock_name":"華擎",
                    "stock_id":"3515",
                    "close":104.0,
                    "volumn":95},
                   {"stock_name":"訊連",
                    "stock_id":"5203",
                    "close":98.5,
                    "volumn":326}]}
    return JsonResponse(data)

def bootstrap_test_table_html(request):
    return render(request, 'bootstrap_test_table_html.htm', {})

def bootstrap_test_table_ajax(request):
    return render(request, 'bootstrap_test_table_ajax.htm', {})

def bootstrap_test_jumbotron(request):
    return render(request, 'bootstrap_test_jumbotron.htm', {})

def extjs_test_themes(request):
    return render(request, 'extjs_test_themes.htm', {})

def extjs_test_grid_panel(request):
    return render(request, 'extjs_test_grid_panel.htm', {})

def home(request):
    return render(request, 'home.htm', {})


以上就是此次測試的全紀錄, 終於順利將四個前端 UI 框架納入 Django 架構中, 四個框架檔案大小合計 18.6MB, 但實際應用時不需要全上, 只要挑選要用的框架放在 static 目錄下即可. 下面僅紀錄 ExtJS 的兩個測試結果, 其它三個框架的測試參考上一篇 "模板的匯入與繼承" :






Bingo! 繼續往下一站前進!

參考 :

# https://www.jeasyui.com/extension/themes_older.php

2019-11-04 補充 :

上面的 ExtJS 主題佈景測試可以跟 jQueryUI 與 EasyUI 一樣改用 ComboBox 來做主題設定, extjs_test_themes.htm 網頁更改如下 : 

<--! extjs_test_themes.htm -->
{% extends "extjs.htm" %}
{% load static %}
{% block style %}
   body {padding:20px;}
{% endblock%}
{% block body %}
  <script type="text/javascript">
    Ext.onReady(function() {
      var themes=Ext.create('Ext.data.Store', {
          fields : ['theme', 'value'],     //theme=顯示用, value=值
          data : [
            {"theme":"Default", "value":"default"},
            {"theme":"Neptune", "value":"neptune"},
            {"theme":"Gray", "value":"gray"},
            {"theme":"Access", "value":"access"}
            ]
        });
      Ext.create('Ext.form.ComboBox', {
          fieldLabel: '選擇主題',
          store: themes,
          queryMode: 'local',
          displayField: 'theme',
          valueField: 'value',
          renderTo: Ext.getBody(),
          listeners:{
            change: function(combo, nv, ov){  //nv=新選項, ov=舊選項
              if (nv=="default") {
                var href='{% static "extjs/resources/css/ext-all.css" %}';
                }
              else {         
                var href='{% static "extjs/resources/css/ext-all-" %}' + nv + ".css";
                }
              var theme_link=Ext.get("theme");  //取得 Link 元件物件
              theme_link.set({'href' : href});  //設定新佈景
              }
            }
          });
      var btn_alert=new Ext.Button({
          text : "訊息框",
          width : 100,
          renderTo : Ext.getBody(),
          handler : function(btn) {
            Ext.MessageBox.alert("通知訊息", "您好! Hello World!");
            }
          });
      }); //end of onReady
  </script>
{% endblock%}

此程式首先為下拉式選單建立資料來源 (data store), 然後在 listeners 屬性中設定監聽 change 事件, 只要偵測到選項改變就呼叫 Link 元件的 set() 方法更改要套用的主題佈景之 css 檔. 預設佈景是 ext-all.css, 而其它佈景為 ext-all-xxx.css, 因此需用 if else 來判斷. 結果如下 :




參考 : 

# https://docs.sencha.com/extjs/6.2.0/classic/Ext.form.field.ComboBox.html
# DOM的查詢&遍歷
# ExtJS4.2 下拉列表Combobox级联选择
# https://www.extjs-tutorial.com/extjs/working-with-dom
# Ext.form.field.ComboBoxView source

2019-11-05 補充 :

關於上面 jQuery UI 主題佈景切換測試中, 發現下拉式選單 SelectMenu 元件整個清單會展開亂跳的現象, 今天將 jquery.htm 改換回 CDN 供檔方式就正常, 其次更改底下切換主題 CSS 的來源為本地主題又不行 :

var href="https://code.jquery.com/ui/1.12.0/themes/" + theme +
               "/jquery-ui.min.css";     //OK
var href="/static/jqueryui/themes/" + theme + "/jquery-ui.min.css";  //NG

本地使用最新的 jQuery UI 1.12.1 版, 我以為是版本問題, 就去下載 jQuery UI 1.12.0 版, 結果還是一樣會亂跳, 但只要底下切換主題的 href 改回 CDN 來源就正常, 很奇怪, 明明都是從 jQuery UI 官網下載的 1.12.0 版, 為何會有這種差別?

# https://jqueryui.com/download/all/

我又改為使用 CDN 供檔的 1.12.1 版也 OK 啊! 可見不是版本問題, 難道是開發伺服器問題? 目前無解, 總之, 若使用 jQuery UI 開發, 我想還是使用 CDN 供檔方式較妥當, 不然就固定用漂亮的 hot-sneaks 主題就好, 別切換了.

基於以上測試心得, 結論是 EasyUI 與 ExtJS 最好使用本機供檔方式以固定版本保證網站外觀; 而 jQuery UI 與 Bootstrap 就直接使用 CDN 供檔即可, 因為 Bootstrp 與 jQuery UI 都已很成熟.

2019年3月16日 星期六

ExtJS 學習筆記索引

這周因為一時興起重新回味前端技術, 複習了 jQuery UI 用法, 連帶對 2013~2014 年間熱中學習的 ExtJS 也有點懷念, 所以今天抽空整理了一下筆記索引如下 :

# ExtJS 我回來了
# ExtJs 環境部署
# ExtJS 4 測試 (一) : 對話框與進度條
# ExtJS 4 測試 (二) : 主題佈景切換 (Look and Feel)
# ExtJS 4 測試 : 頁籤面板 TabPanel
# ExtJS 4 測試 : each 與 forEach 迴圈測試
# ExtJS4 類別別名表 (Alias)
# ExtJS 4 測試 : 類別與物件
# ExtJS 4.2.2 版
# ExtJS 5 GPL 授權版下載
# ExtJS 4 測試 : GridPanel (一)
# ExtJS 4 測試 : GridPanel (二)
# ExtJS 4 測試 : GridPanel (三)
# ExtJS 4 測試 : TabPanel 中的 GridPanel
# ExtJS 4 測試 : Panel 類別與佈局
# ExtJS 5 GPL 授權版下載

我的 ExtJS 學習在 2014/07/17 畫下終點, 那真是一段令我傷感的日子啊. 雖然以後使用 ExtJS 開發專案的機率趨近於 0, 但那近一年的專心投入畢竟也蠻有收穫的, 是我自學網頁技術的一個里程碑. 不再使用 ExtJS 的原因主要是 :
  1. 體積較龐大反應較慢
  2. 物件導向設計不易學習
  3. 商業版區授權使用, GPL授權限制多
  4. jQuery UI 或 Easy UI 簡單免費又好用
關於商業授權參考 :

# https://www.sencha.com/store/

2015年5月8日 星期五

ExtJS 5 GPL 授權版下載

與 ExtJS 一別已近一年, 沒想到 Sencha 已經推出第五版了, 與我原先學的 4.2.1 版不知變化有多大? 但我已經回頭投入 jQuery 陣營了, 其實也沒時間再研究 ExtJS. 只是看到 ExtJS 商用版貴鬆鬆的報價 (5 人開發版 $3225, 台幣 9 萬 6 千多), 我想大概只有專業設計公司會花錢買吧 !

#  https://www.sencha.com/store/


在 Sencha 首頁到處找都看不到以前 Community GPL 免費版的下載頁, Google 了一下, 在論壇這篇找到了 :

# Ext JS Community Forums 5.x : ExtJS 5 Release and Opensource download

原來是在首頁上方的 "Store/Licencing" 頁面中間, 有一個 "Sencha GPL Download Page", 點下去就會出現下載頁 :

# http://www.sencha.com/legal/GPL/ 


輸入 Email 後按 "Download SDK", 再去收信, 點信裡的超連結就可以下載了. 我下載到的是 5.1.0 版, 超恐怖, zip 檔高達 92MB! 而 Sencha Touch 是 2.4.1 版, 也高達 55MB!


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





測試範例 2 : http://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月8日 星期二

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 所示 :

測試範例 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