初心者のためのExtJS入門

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

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

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

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

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

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

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

データビュー(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'));
        }
    }

});