初心者のためのExtJS入門

ExtJSを使うので、ついでにまとめていきます

チュートリアル:ルーティングを使って、SPA(シングルページアプリケーション)にする(1)

今回は、メモ一覧とメモ入力フォームを別の画面に分けて、ルーティングを使って画面遷移できるようにします。

ルーティングは、URLのパターンで処理を切り替える機能です。これを使ってSPAにします。

最終的には、http://localhost:1841/#listにアクセスした場合は一覧画面、http://localhost:1841/#registにアクセスした場合は登録画面を表示するようにしようと思います。

ビューコントローラのroutesコンフィグでルートを定義する

とりあえず、まずルーティングを試してみます。

ルーティングはどのビューコントローラにでも設定できますが、ここでは全体を構成するビューポートのビューコントローラに定義することにしましょう。

ビューポートとそのビューコントローラクラスだけ用意します。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    cls: 'app-main',

    controller: 'app_main',

    layout: 'card'
});

/**
 * ビューポートのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    routes: {
        'list': 'onList',
        'regist': 'onRegist'
    },

    onList: function () {
        console.log('#list');
    },

    onRegist: function () {
        console.log('#regist');
    }

});

http://localhost:1841/#listにアクセスすると、コンソールログ#listが出力されました。

このように、ビューコントローラのroutesコンフィグにURLのパターンをキーとして設定したメソッドが呼ばれていることが分かります。

画面を表示してみる

次は具体的に画面を表示してみます。

とりあえず、一覧画面と登録画面をそれぞれMemo.view.list.Panel、Memo.view.regist.Panelとして作成してみます。ビューモデルとビューコントローラの部分は一旦外してシンプルな状態にしておきます。sassも変更に合わせて調整が必要です。

/**
 * メモ一覧パネルクラス。
 *
 * @class Memo.view.list.Panel
 * @extend Ext.Panel
 */
Ext.define('Memo.view.list.Panel', {
    extend: 'Ext.Panel',
    xtype: 'list_panel',

    requires: [
        'Memo.view.list.View'
    ],

    layout: 'fit',

    items: {
        xtype: 'list_dataview'
    }
});

/**
 * メモ一覧データビュークラス。
 *
 * @class Memo.view.list.View
 * @extend Ext.view.View
 */
Ext.define('Memo.view.list.View', {
    extend: 'Ext.view.View',
    xtype: 'list_dataview',

    cls: 'list-dataview',

    tpl: [
        '<tpl for=".">',
            '<div class="item">',
                '<h3 class="title">{title}</h3>',
                '<p class="body">{body}</p>',
            '</div>',
        '</tpl>'
    ],

    itemSelector: 'div.item',

    store: 'Memo'
});

/**
 * メモ登録フォームパネルクラス。
 *
 * @class Memo.view.regist.Panel
 * @extend Ext.form.Panel
 */
Ext.define('Memo.view.regist.Panel', {
    extend: 'Ext.form.Panel',
    xtype: 'regist_panel',

    bodyPadding: 15,

    title: 'メモ入力フォーム',

    config: {
        /**
         * @cfg {Memo.model.Memo} メモモデル。
         */
        record: null
    },

    defaults: {
        anchor: '100%'
    },

    items: [
        {
            name: 'title',
            xtype: 'textfield',
            emptyText: 'タイトルを入力してください',
            allowBlank: false
        },
        {
            name: 'body',
            xtype: 'textarea',
            emptyText: '本文を入力してください'
        },
        {
            xtype: 'button',
            text: '保存'
        }
    ],

    /**
     * recordコンフィグ設定時の処理。
     * @param {Memo.model.Memo} record メモモデル
     */
    setRecord: function (record) {
        var me = this;

        if (!record) {
            record = Ext.create('Memo.model.Memo');
        }

        me.loadRecord(record);
    }

});

次に画面は動的に作成し、ビューポートに設置させるようにしてみます。

/**
 * ビューポートのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    requires: [
        'Memo.view.list.Panel',
        'Memo.view.regist.Panel'
    ],

    routes: {
        'list': 'onList',
        'regist': 'onRegist'
    },

    onList: function () {
        this.switchScreen('list_panel');
    },

    onRegist: function () {
        this.switchScreen('regist_panel');
    },

    /**
     * 画面を切り替える。
     * @param {String} screenXType 画面のxtype
     */
    switchScreen: function (screenXType) {
        var me = this,
            view = me.getView(),
            screen;

        // 画面の存在チェック
        screen = view.down(screenXType);

        if (!screen) {
            // 画面を生成
            screen = Ext.widget(screenXType);

            // ビューポートに追加
            view.add(screen);
        }

        view.getLayout().setActiveItem(screen);
    }

});

これでhttp://localhost:1841/#listhttp://localhost:1841/#registにアクセスしたら、一覧画面と登録画面が表示されるようになりました。

f:id:sham-memo:20170117112107p:plain

f:id:sham-memo:20170117112114p:plain

一旦ここまで。次回、もう少し細かい部分の実装をしてみます。

チュートリアル:ビューモデルを使って、フォームパネルを編集に対応させる

今回は、保存しているメモを編集できるようにしたい、という内容です。

編集したいメモを選択すると入力フォームに転記されて保存ボタンを押すと編集内容が反映される、といったかんじにしようと思います。

これだけだと、メモを選択したあとに、新規登録に戻す方法を決めていません。今回は新規登録に切り替えるボタンを別途配置することにします。

データビューを選択したときの処理を実装

まずは、データビューでメモを選択する部分です。

データビュー(Ext.view.View)は、選択状態になるとselectionchangeというイベントが発生します。

このイベントが発生したときに、処理が実行されるようにしてみましょう。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    cls: 'app-main',

    controller: 'app_main',

    bodyPadding: 20,

    layout: 'border',

    items: [
        {
            region: 'west',
            reference: 'form',
            xtype: 'form',
            title: 'メモ入力フォーム',
            bodyPadding: 15,
            width: 300,
            defaults: {
                anchor: '100%'
            },
            items: [
                {
                    name: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください',
                    allowBlank: false
                },
                {
                    name: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            region: 'center',
            reference: 'list',
            xtype: 'dataview',
            cls: 'memo-list-dataview',
            tpl: [
                '<tpl for=".">',
                    '<div class="item">',
                        '<h3 class="title">{title}</h3>',
                        '<p class="body">{body}</p>',
                    '</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: 'Memo',
            listeners: {
                selectionchange: 'onSelectionChangeDataView'
            }
        }
    ]
});


/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            form = me.lookupReference('form'),
            list = me.lookupReference('list'),
            store = list.getStore();

        if (form.isValid()) {
            store.add(form.getValues());
        }
    },

    /**
     * データビューselectionchangeイベント時の処理。
     *
     * @param {Ext.view.View} dataview データビュー
     * @param {Array} records 選択行のモデルリスト
     */
    onSelectionChangeDataView: function (dataview, records) {
        console.log('selectionchangeイベントが発生したよ。');
    }

});

ビューコントローラにonSelectionChangeDataViewメソッドを追加して、ビューのlistenersコンフィグでselectionchange: 'onSelectionChangeDataView'と設定することで、イベント発生時にonSelectionChangeDataViewが呼ばれるようにしました。

API(http://docs.sencha.com/extjs/6.2.1/classic/Ext.view.View.html#event-selectionchange)を見ると、引数について確認できます。

ビューモデルを使ってフォームパネルに転記する

フォームパネルへの転記は、ビューモデルを使うことにします。

ビューモデルを使うことで、ビューモデルとビューの間に双方向データバインディングを実現でき、ビューモデルに値を設定することでビューに反映することができるようになります。

まずはビューモデルを用意します。

/**
 * ビューモデルクラス。
 *
 * @class Memo.view.main.ViewModel
 * @extend Ext.app.ViewModel
 */
Ext.define('Memo.view.main.ViewModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.app_main',

    data: {
        /**
         * @cfg {Memo.model.Memo} 選択中のメモモデル。
         */
        selectedRecord: null
    },

    formulas: {
        /**
         * フォームのモード(新規か編集か)のラベルを返す。
         *
         * @param {Function} get
         * @returns {string} フォームのモード(新規か編集か)のラベル
         */
        labelFormMode: function (get) {
            var selectedRecord = get('selectedRecord');
            return selectedRecord ? '編集' : '新規作成';
        },

        /**
         * 新規作成モードかどうかを返す。
         *
         * @param {Function} get
         * @returns {boolean} 新規作成モードの場合はtrue
         */
        isNew: function (get) {
            return !get('selectedRecord');
        }
    }
});

ビューモデルは、Ext.app.ViewModelを継承し、エイリアス名として「viewmodel.xxx」という形式にします。

dataには、ビューモデルで管理する項目を用意します。ここでは、データビューで選択したモデルを格納するselectedRecordプロパティを定義しました。

formulasには、dataに設定される値を使って別名の値を用意できます。

フォームパネルクラスを作成

次にビューとビューモデルを結びつけます。

そのために、フォームパネルのクラスを別途作成し、その中で結びつけるようにします。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.Form',
        'Memo.view.main.ViewController',
        'Memo.view.main.ViewModel'
    ],

    cls: 'app-main',

    controller: 'app_main',
    viewModel: 'app_main',

    bodyPadding: 20,

    layout: 'border',

    items: [
        {
            region: 'west',
            reference: 'form',
            xtype: 'main_form'
        },
        {
            region: 'center',
            reference: 'list',
            xtype: 'dataview',
            cls: 'memo-list-dataview',
            tpl: [
                '<tpl for=".">',
                    '<div class="item">',
                        '<h3 class="title">{title}</h3>',
                        '<p class="body">{body}</p>',
                    '</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: 'Memo',
            listeners: {
                selectionchange: 'onSelectionChangeDataView'
            }
        }
    ]
});

/**
 * フォームパネルクラス。
 * 
 * @class Memo.view.main.Form
 * @extend Ext.form.Panel
 */
Ext.define('Memo.view.main.Form', {
    extend: 'Ext.form.Panel',
    xtype: 'main_form',

    bodyPadding: 15,

    width: 300,

    title: 'メモ入力フォーム',

    config: {
        /**
         * @cfg {Memo.model.Memo} メモモデル。
         */
        record: null
    },

    tools: [
        {
            xtype: 'button',
            text: '新規作成',
            scale: 'small',
            bind: {
                disabled: '{isNew}'
            },
            handler: 'onClickNewButton'
        }
    ],

    bind: {
        title: 'メモ入力フォーム({labelFormMode})',
        record: '{selectedRecord}'
    },

    defaults: {
        anchor: '100%'
    },

    items: [
        {
            name: 'title',
            xtype: 'textfield',
            emptyText: 'タイトルを入力してください',
            allowBlank: false
        },
        {
            name: 'body',
            xtype: 'textarea',
            emptyText: '本文を入力してください'
        },
        {
            xtype: 'button',
            text: '保存',
            handler: 'onClickSaveButton'
        }
    ],

    /**
     * recordコンフィグ設定時の処理。
     * @param {Memo.model.Memo} record メモモデル
     */
    setRecord: function (record) {
        var me = this;

        me.loadRecord(record);

        me.callParent(arguments);
    }

});

まずはビューポートクラスですが、viewModelコンフィグに、ビューモデルのエイリアス名を指定することでビューモデルを適用できます。

さらにデータバインディングしたい項目にbindコンフィグを設定していきます。

bind: {
  title: 'メモ入力フォーム({labelFormMode})',
  record: '{selectedRecord}'
}

上記のように{}の中にビューモデルのdataやformulasで定義した項目を設定すると、そこに値がバインディングされます。

bindにオブジェクトリテラルを指定すると、キーの項目に値が設定されます。上記の例だとtitleとrecordコンフィグにバインディングされることになります。文字列を指定した場合はdefaultBindPropertyの値にバインディングされるようになっています。defaultBindPropertyの値はコンポーネントごとに異なります。

データバインディングしたい項目が存在しない場合は、configコンフィグを定義することで追加することができます。追加した項目はsetメソッドとupdateメソッドが自動的に用意されます。今回の場合はrecordを追加したので、setRecordとupdateRecordです。recordの値が設定されたり変更されたりした場合に呼び出されます。

updateRecordメソッド内で、loadRecordメソッドを呼び出しています。loadRecordを使うと、モデルのフィールド名と入力フィールドのnameコンフィグの値が一致する場合に、その値がフィールドに設定されます(内部的には入力フィールドのsetValueが呼ばれる)。

最後に、ビューコントローラでビューモデルに値を設定するようにします。

/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            form = me.lookupReference('form'),
            list = me.lookupReference('list'),
            store = list.getStore();

        if (form.isValid()) {
            store.add(form.getValues());
        }
    },

    /**
     * 新規作成ボタンクリック時の処理。
     */
    onClickNewButton: function () {
        var me = this,
            viewModel = me.getViewModel(),
            form = me.lookupReference('form'),
            dataview = me.lookupReference('list');

        // 選択を解除
        dataview.deselect(dataview.getSelection());

        viewModel.set('selectedRecord', null);
    },

    /**
     * データビューselectionchangeイベント時の処理。
     *
     * @param {Ext.view.View} dataview データビュー
     * @param {Array} records 選択行のモデルリスト
     */
    onSelectionChangeDataView: function (dataview, records) {
        var me = this,
            viewModel = me.getViewModel();

        if (records.length > 0) {
            viewModel.set('selectedRecord', records[0]);
        } else {
            viewModel.set('selectedRecord', Ext.create('Memo.model.Memo'));
        }
    }

});

ビューモデルは、ビューコントローラのgetViewModelメソッドで取得できます(内部的にはビューのgetViewModelが呼ばれます)。

で、ビューモデルのsetメソッドで値を設定しています。

f:id:sham-memo:20170115145633p:plain

f:id:sham-memo:20170115145641p:plain

保存処理を更新に対応

モデルのphantomの値を使って新規か更新かを判断しています。

新規の場合はストアに追加、更新の場合はモデルに値を設定し、最後にcommitします。

/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            form = me.lookupReference('form'),
            list = me.lookupReference('list'),
            store = list.getStore();

        if (form.isValid()) {
            var record = form.getRecord();

            if (record.phantom) {
                // 新規
                var records = store.add(form.getValues());
                records[0].commit();
            } else {
                // 更新
                record.set(form.getValues());
            }
        }
    },

    /**
     * 新規作成ボタンクリック時の処理。
     */
    onClickNewButton: function () {
        var me = this,
            viewModel = me.getViewModel(),
            form = me.lookupReference('form'),
            dataview = me.lookupReference('list');

        // 選択を解除
        dataview.deselect(dataview.getSelection());

        viewModel.set('selectedRecord', null);
    },

    /**
     * データビューselectionchangeイベント時の処理。
     *
     * @param {Ext.view.View} dataview データビュー
     * @param {Array} records 選択行のモデルリスト
     */
    onSelectionChangeDataView: function (dataview, records) {
        var me = this,
            viewModel = me.getViewModel();

        if (records.length > 0) {
            viewModel.set('selectedRecord', records[0]);
        } else {
            viewModel.set('selectedRecord', Ext.create('Memo.model.Memo'));
        }
    }

});

チュートリアル:モデル、ストアを作成する

今回はモデルとストアを作成します。

これまではデータビューのstoreコンフィグでストアを簡易な形式で設定していましたが、これを別ファイルとして作成することにします。

事前準備

作成する前に、ファイルを整理します。

まずはapp/Application.jsをclassic直下にコピーします。

その後、appディレクトリに配置されているディレクトリ、ファイルをまるごと削除します。

あと、classic/view/main/List.jsも使っていないので削除します。

(なぜこんなことしてるのかについては、本記事最後の「補足」を参照ください)

モデル

モデルはclassic/src/modelディレクトリに作成していきます。

Ext.data.Modelを継承し、モデルに必要なデータの定義はfieldsコンフィグに設定していきます。

/**
 * メモモデルクラス。
 *
 * @class Memo.model.Memo
 * @extend Ext.data.Model
 */
Ext.define('Memo.model.Memo', {
    extend: 'Ext.data.Model',

    fields: [
        {
            name: 'title',
            type: 'string'
        },
        {
            name: 'body',
            type: 'string'
        }
    ]
});

上記のような、メモを表す「メモモデル」を作成してみました。

ストア

ストアはclassic/src/storeディレクトリに作成していきます。

Ext.data.Storeを継承し、使用するモデルとプロキシを設定します。

/**
 * メモストアクラス。
 *
 * @class Memo.store.Memo
 * @extend Ext.data.Store
 */
Ext.define('Memo.store.Memo', {
    extend: 'Ext.data.Store',

    requires: [
        'Memo.model.Memo'
    ],

    model: 'Memo.model.Memo',

    proxy: 'memory',

    data: [
        { title: 'タイトル1', body: '本文1' },
        { title: 'タイトル2', body: '本文2' },
        { title: 'タイトル3', body: '本文3' }
    ]
});

プロキシについて、ざっくり説明します。

ストアはデータを管理するための機能で、データを取得、保存、削除することができます。

取得、保存、削除のような操作をどのような形式で行うかというのがプロキシで決まります。

プロキシの種類としては、Ext.data.proxy.Ajax、Ext.data.proxy.JsonP、Ext.data.proxy.Memory、Ext.data.proxy.LocalStorageなど色々な種類があります(標準で提供されているのはExt.data.proxyパッケージにあるクラスです)。

単純な非同期通信でサーバとデータのやり取りをしたいのであれば、Ext.data.proxy.Ajaxを使います。

上記のコードでは、Ext.data.proxy.Memoryを使っています。これは、特に通信などを行わずメモリ上に残しておくだけになります(そのうち、Ext.data.proxy.LocalStorageに変えるつもりです)。

データビューでストアを使うようにする

まずはアプリケーションクラスのstoreコンフィグに作成したストアを設定します。

/**
 * アプリケーションクラス。
 *
 * @class Memo.Application
 * @extend Ext.app.Application
 */
Ext.define('Memo.Application', {
    extend: 'Ext.app.Application',
    
    name: 'Memo',

    stores: [
        'Memo'
    ]
});

ちなみにストアには「ストアID」という一意となるIDが付与されるようになっています。

自動的に付与されるのはクラス名と同じ文字列です。

最後にデータビューのstoreコンフィグを変更します。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    cls: 'app-main',

    controller: 'app_main',

    bodyPadding: 20,

    layout: 'border',

    items: [
        {
            region: 'west',
            reference: 'form',
            xtype: 'form',
            title: 'メモ入力フォーム',
            bodyPadding: 15,
            width: 300,
            defaults: {
                anchor: '100%'
            },
            items: [
                {
                    name: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください',
                    allowBlank: false
                },
                {
                    name: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            region: 'center',
            reference: 'list',
            xtype: 'dataview',
            cls: 'memo-list-dataview',
            tpl: [
                '<tpl for=".">',
                    '<div class="item">',
                        '<h3 class="title">{title}</h3>',
                        '<p class="body">{body}</p>',
                    '</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: 'Memo'
        }
    ]
});

これで、ストアを別ファイルに分けることができました。

補足

今回、appディレクトリ以下のファイルを削除しました。これは私の場合ですが、appディレクトリは使用しません。

最初、appにはclassicとmodernの共通処理のコードを作成すると説明しました。自動生成した時点では、ストアとモデルもappに作成するようにディレクトリが用意されています。

しかし、実際の案件で使っていると、classicとmodernでは画面設計が違ったりすることが多く、全く同じストアとモデルを使えないことが多々あるため、最初から分けてしまうことにしています。

共通処理が必要な場合は「パッケージ」を作って、そこに作成するようにしています。

チュートリアル:スタイルを整える

現在の画面は↓なかんじになっているわけですが。。。

f:id:sham-memo:20170111204647p:plain

これを見て気付くことがあります。。。そうダサいですね。こんなものを使うわけありません。

ということで、今回はスタイルを整えてみます。

layoutを指定する

まずはlayoutコンフィグを指定して、画面の左側に入力フォーム、右側に一覧を配置します。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    controller: 'app_main',

    bodyPadding: 20,

    layout: 'border',

    items: [
        {
            region: 'west',
            reference: 'form',
            xtype: 'form',
            title: 'メモ入力フォーム',
            bodyPadding: 15,
            width: 300,
            defaults: {
                anchor: '100%'
            },
            items: [
                {
                    name: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください',
                    allowBlank: false
                },
                {
                    name: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            region: 'center',
            reference: 'list',
            xtype: 'dataview',
            tpl: [
                '<tpl for=".">',
                '<div class="item">{title}</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: {
                proxy: 'memory',
                data: [
                    { title: 'タイトル1', body: '本文1' },
                    { title: 'タイトル2', body: '本文2' },
                    { title: 'タイトル3', body: '本文3' }
                ]
            }
        }
    ]
});

まずはビューポートにlayout: 'border'を追記しました。

これはレイアウトをボーダーレイアウト(Ext.layout.container.Border)にする、ということを意味します。

ボーダーレイアウトは、アイテムコンポーネントにregionを指定することで、東・西・南・北・中央の5カ所にコンポーネントを配置することができます。

大きな制約はなく、「西に2つ、中央は何も無し」のような指定もできたりします。

今回はフォームパネルにregion: 'west'、データビューにregion: 'center'を指定しました。

あとは、フォームパネルにタイトルを付けたり(titleコンフィグ追加)、入力フィールドを目一杯横に伸ばしたりしてます(anchor: '100%'を設定)。

f:id:sham-memo:20170112183945p:plain

スタイルシートを適用する

データビューにはスタイルシートを適用させます。

ExtJSではsassを使ってスタイルシートを作成します。

ディレクトリ構成のルールとしては、classic/sassディレクトリにsrc以下のディレクトリ構成と同じにする必要があります。

例えば、Memo.view.main.Mainのスタイルシートを作成するのであれば、classic/sass/src/view/main/Main.scssをいうファイルにスタイルを定義することになります。

このようにルール通りにsassファイルを配置しておくと、sencha app watchでsassコンパイルが実行されます。

※ExtJS6.5からはビューのjsファイルと同じディレクトリにscssファイルを配置しても良くなりました。(上記の方法も使えます)

 

まずはclsコンフィグでコンポーネントセレクタ名を指定します。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    cls: 'app-main',

    controller: 'app_main',

    bodyPadding: 20,

    layout: 'border',

    items: [
        {
            region: 'west',
            reference: 'form',
            xtype: 'form',
            title: 'メモ入力フォーム',
            bodyPadding: 15,
            width: 300,
            defaults: {
                anchor: '100%'
            },
            items: [
                {
                    name: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください',
                    allowBlank: false
                },
                {
                    name: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            region: 'center',
            reference: 'list',
            xtype: 'dataview',
            cls: 'memo-list-dataview',
            tpl: [
                '<tpl for=".">',
                '<div class="item">{title}</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: {
                proxy: 'memory',
                data: [
                    { title: 'タイトル1', body: '本文1' },
                    { title: 'タイトル2', body: '本文2' },
                    { title: 'タイトル3', body: '本文3' }
                ]
            }
        }
    ]
});

次にMain.scssを用意します。アプリケーションを作成した時点で、自動的にMain.scssファイルも作成されていたので、内容を変更してみます。

@charset "UTF-8";

.app-main {

    .memo-list-dataview {
        background-color: #fff;
    }

}

とりあえず背景色を#fffにしてみます。

f:id:sham-memo:20170112203013p:plain

ちゃんと適用されているのがわかります。

もう少しスタイルを変更してみましょう。

@charset "UTF-8";

.app-main {
    .memo-list-dataview {
        background-color: #fff;
        margin-left: 5px;

        .item {
            cursor: pointer;
            outline: none !important;

            .title {
                background-color: #5fa2dd;
                color: #fff;
                padding: 5px;
                margin: 0;
            }

            .body {
                padding: 5px;
                margin: 0;
            }

            &.x-item-selected {
                .title {
                    background-color: darken(#5fa2dd, 10%);
                }
            }
        }
    }
}

f:id:sham-memo:20170112213523p:plain

x-item-selectedセレクタなどはdeveloper toolsで確認しながら設定しました。

まだまだダサさは残ってますが、ある程度スタイルを調整できましたね。

チュートリアル:フォームパネルの機能を使う

フォームパネルの機能を使ってリファクタリング

まずはフォームパネル(Ext.form.Panel)の機能を使って、さらにリファクタリングします。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    controller: 'app_main',

    bodyPadding: 20,

    items: [
        {
            reference: 'form',
            xtype: 'form',
            items: [
                {
                    name: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください'
                },
                {
                    name: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            reference: 'list',
            xtype: 'dataview',
            tpl: [
                '<tpl for=".">',
                '<div class="item">{title}</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: {
                proxy: 'memory',
                data: [
                    { title: 'タイトル1', body: '本文1' },
                    { title: 'タイトル2', body: '本文2' },
                    { title: 'タイトル3', body: '本文3' }
                ]
            }
        }
    ]
});

入力フィールドにnameコンフィグを指定すると、フォームパネルのgetValuesメソッドの戻り値に指定したnameコンフィグをキーとして値が含まれるようになっています。

タイトルと本文のnameコンフィグに、それぞれtitle、bodyと設定したので、getValuesの戻り値は下記のような値となります。

{
  "title": "タイトルとして入力したテキスト",
  "body": "本文として入力したテキスト"
}

これでいちいち入力フィールドをひとつずつ参照しなくて済むようになりました。

ビューコントローラは次のようになります。

/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            form = me.lookupReference('form'),
            list = me.lookupReference('list'),
            store = list.getStore();

        store.add(form.getValues());
    }
});

入力チェック

タイトルが入力されていなければ保存ボタンが機能しないようにしたいです。

その場合、テキストフィールド(Ext.form.field.Text)のallowBlankコンフィグにfalseを指定します。

そうすると、対象フィールドのisValidメソッドを実行することで、入力の検証を行い、エラーと判断した場合にはエラーメッセージを表示してくれます。

また、フォームパネルのisValidメソッドを実行すると、パネル内に配置されているフィールド全てのisValidを自動的に呼び出してくれるので、こっちを使ったほうが便利です。

入力チェックを追加したコードは次のようになりました。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    controller: 'app_main',

    bodyPadding: 20,

    items: [
        {
            reference: 'form',
            xtype: 'form',
            items: [
                {
                    name: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください',
                    allowBlank: false
                },
                {
                    name: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            reference: 'list',
            xtype: 'dataview',
            tpl: [
                '<tpl for=".">',
                '<div class="item">{title}</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: {
                proxy: 'memory',
                data: [
                    { title: 'タイトル1', body: '本文1' },
                    { title: 'タイトル2', body: '本文2' },
                    { title: 'タイトル3', body: '本文3' }
                ]
            }
        }
    ]
});

/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            form = me.lookupReference('form'),
            list = me.lookupReference('list'),
            store = list.getStore();

        if (form.isValid()) {
            store.add(form.getValues());
        }
    }
});

タイトルを入力せずに保存ボタンをクリックすると、タイトルのテキストフィールドが赤枠になります。

f:id:sham-memo:20170112181606p:plain

チュートリアル:referenceを使って参照する

フォームの入力欄をカスタマイズすることにしました。

入力項目は「タイトル」「本文」の2つです(だんだんメモ登録っぽくしていってます)。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    controller: 'app_main',

    bodyPadding: 20,

    items: [
        {
            xtype: 'form',
            items: [
                {
                    reference: 'title',
                    xtype: 'textfield',
                    emptyText: 'タイトルを入力してください'
                },
                {
                    reference: 'body',
                    xtype: 'textarea',
                    emptyText: '本文を入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            reference: 'list',
            xtype: 'dataview',
            tpl: [
                '<tpl for=".">',
                '<div class="item">{title}</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: {
                proxy: 'memory',
                data: [
                    { title: 'タイトル1', body: '本文1' },
                    { title: 'タイトル2', body: '本文2' },
                    { title: 'タイトル3', body: '本文3' }
                ]
            }
        }
    ]
});

そして、ビューコントローラから入力フィールドを参照するのにgetComponentを使っていましたが、入力フィールドの順番を入れ替えるとその度に修正しないといけないので、referenceを使うように変更しました。

/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            titleField = me.lookupReference('title'),
            bodyField = me.lookupReference('body'),
            list = me.lookupReference('list'),
            store = list.getStore();

        store.add({
            title: titleField.getValue(),
            body: bodyField.getValue()
        });
    }
});

このように、referenceを指定しておくと、lookupReferenceメソッドを使って参照できます。

ちなみに、ビューコントローラのlookupReferenceは、内部処理としてはビューのlookupReferenceを実行してます。

f:id:sham-memo:20170111204647p:plain

今回で、こんな感じの画面になりました。 次回はフォームパネルの機能を使ってみます。

チュートリアル:ビューコントローラを使う

ExtJSには、ビューコントローラという機能があります。これによってボタン押したり行を選択したりした場合のイベント発火時の処理をビューから切り離すことができます。

前に定義した保存ボタンクリック時の処理を、ビューコントローラに移してみました。

まずはビューコントローラです。

/**
 * Memo.view.main.Mainのビューコントローラクラス。
 *
 * @class Memo.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.app_main',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            view = me.getView(),
            textfield = view.down('form').getComponent(0),
            value = textfield.getValue(),
            dataview = view.down('dataview'),
            store = dataview.getStore();

        store.add({ text: value });
    }
});

Memo.view.main.ViewControllerがビューコントローラです。ビューコントローラは、Ext.app.ViewControllerを継承する必要があります。

aliasコンフィグを指定することで、ビューコントローラのエイリアス名を定義できます。ここではapp_mainとしています。ビューコントローラの場合は「controller.xxx」のように、controllerという文字列を先頭に付与しておきます。

あとは保存ボタンクリック時に実行される処理として、onClickSaveButtonという関数を定義しています。

関数内でgetViewを使うことで、ビューを参照することができます。getViewで参照できるビューは、ビューコントローラを使っているビューとなります。

/**
 * ビューポートクラス。
 *
 * @class Memo.view.main.Main
 * @extend Ext.Panel
 */
Ext.define('Memo.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app_main',

    requires: [
        'Memo.view.main.ViewController'
    ],

    controller: 'app_main',

    bodyPadding: 20,

    items: [
        {
            xtype: 'form',
            items: [
                {
                    xtype: 'textfield',
                    emptyText: 'テキストを入力してください'
                },
                {
                    xtype: 'button',
                    text: '保存',
                    handler: 'onClickSaveButton'
                }
            ]
        },
        {
            xtype: 'dataview',
            tpl: [
                '<tpl for=".">',
                    '<div class="item">{text}</div>',
                '</tpl>'
            ],
            itemSelector: 'div.item',
            store: {
                proxy: 'memory',
                data: [
                    { text: 'アイテム1' },
                    { text: 'アイテム2' },
                    { text: 'アイテム3' }
                ]
            }
        }
    ]
});

controllerコンフィグに、ビューコントローラのエイリアス名を指定することで、このビューがビューコントローラを使いますよー、ということを設定します(requiresに参照するクラス名を定義することをお忘れなく)。

あとは保存ボタンのhandlerコンフィグにonClickSaveButtonを文字列で指定することで、ビューコントローラに定義した関数と結び付けることができます。

これでビュークラスのコードはすっきりしました。