読者です 読者をやめる 読者になる 読者になる

初心者のためのExtJS入門

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

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

ExtJS

今回は、SPA対応のmodern版です。

まあ、classicとほとんど同じなのですが。。。

ルーティングの設定

メインパネルにビューコントローラを設定し、そのビューコントローラにルーティングを定義します。

/**
 * メインパネルクラス。
 *
 * @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',

    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',

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

    routes: {
        'list': 'showList',
        'regist': 'showRegist',
        'regist/:id': 'showRegist'
    },

    /**
     * 一覧画面を表示する。
     */
    showList: function () {
        this.switchScreen('list_panel');
    },

    /**
     * 登録画面を表示する。
     * @param {Number} [id] メモID
     */
    showRegist: function (id) {
        var me = this,
            params = {};

        if (Ext.isDefined(id)) {
            params.id = +id;
        }

        me.switchScreen('regist_panel', params);
    },

    /**
     * 画面を切り替える。
     *
     * @param {String} screenXType 画面のxtype
     * @param {Object} [params] パラメータ
     */
    switchScreen: function (screenXType, params) {
        var me = this,
            view = me.getView(),
            screen;

        params = params || {};

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

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

            view.add(screen);
        }

        view.setActiveItem(screen);

        screen.fireEvent('showscreen', params);
    }
});

説明を忘れていましたが、Memo.view.main.Mainは「メインパネル」としています。ビューポートじゃないのか?というと、ビューポートではありません。modernでは、自動的にビューポートが作成されており、Ext.Viewportでグローバルに参照できるようになっています。Memo.view.main.Mainは、ビューポートに自動的に配置されるビューで、大枠のビューとして使うことにしています。ちなみに、この自動生成はapp.jsというファイルに定義されています。

さて、ルーティングについてですが、これはclassicの時とほぼ同じです。異なる箇所ですが、classicではview.getLayout().setActiveItem(screen)だったコードが、view.setActiveItem(screen)となっています。こういうのがclassicとmodernでのAPIの微妙な違いによるものです。

プロキシを変更

localstorageに変更しておきます。

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

    fields: [
        {
            name: 'id',
            type: 'int'
        },
        {
            name: 'title',
            type: 'string'
        },
        {
            name: 'body',
            type: 'string'
        }
    ],

    validators: {
        title: 'presence'
    }
});

/**
 * メモストアクラス。
 *
 * @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: {
        type: 'localstorage',
        id: 'memo'
    }
});

一覧画面のビューコントローラ作成

ビューコントローラを追加し、一覧画面の処理を実装します。

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

    requires: [
        'Memo.view.list.List',
        'Memo.view.list.ViewController'
    ],

    controller: 'list',

    title: 'メモ一覧',

    tools: [
        {
            xtype: 'button',
            iconCls: 'x-fa fa-plus',
            ui: 'action',
            handler: 'onTapCreateButton'
        }
    ],

    items: {
        xtype: 'list_list'
    },

    listeners: {
        showscreen: 'onShowScreen'
    }
});

/**
 * メモ一覧リストクラス。
 *
 * @class Memo.view.list.List
 * @extend Ext.List
 */
Ext.define('Memo.view.list.List', {
    extend: 'Ext.List',
    xtype: 'list_list',

    cls: 'list-list',

    itemTpl: [
        '<h2 class="title">{title}</h2>',
        '<p class="body">{body}</p>'
    ],

    store: 'Memo',

    listeners: {
        itemtap: 'onItemTap'
    }
});

/**
 * メモ一覧ビューコントローラクラス。
 *
 * @class Memo.view.list.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.list.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.list',

    /**
     * showscreenイベント時の処理。
     */
    onShowScreen: function () {
        Ext.getStore('Memo').load();
    },

    /**
     * 新規作成ボタンタップ時の処理。
     */
    onTapCreateButton: function () {
        this.redirectTo('regist', true);
    },

    /**
     * リストitemtapイベント時の処理。
     *
     * @param {Memo.view.list.List} list リスト
     * @param {Number} index インデックス番号
     * @param {Ext.dom.Element} target タップ要素
     * @param {Memo.model.Memo} record メモモデル
     */
    onItemTap: function (list, index, target, record) {
        this.redirectTo('regist/' + record.getId(), true);
    }
});

Memo.view.list.Listのxtypeが残念なことになっていますが、まあ、これで進めましょうw

classicで「click」と名の付いたイベントは、modernでは「tap」となります。Ext.Listで行をタップしたときは、itemtapイベントが発生するので、イベントハンドラの割り当てもitemtapに行います。

ボタンの場合は、classicと同じようにhandlerコンフィグにイベントハンドラを割り当てられますが、内部的にはtapイベント発生時に処理されています。

登録画面のビューコントローラ、ビューモデル作成

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

    requires: [
        'Memo.view.regist.ViewController',
        'Memo.view.regist.ViewModel'
    ],

    controller: 'regist',
    viewModel: 'regist',

    title: 'メモ登録',

    bind: {
        title: 'メモ登録({labelFormMode})',
        record: '{record}'
    },

    listeners: {
        showscreen: 'onShowScreen'
    },

    tools: [
        {
            xtype: 'button',
            ui: 'action',
            iconCls: 'x-fa fa-check',
            handler: 'onTapSaveButton'
        }
    ],

    bodyPadding: 10,

    items: [
        {
            xtype: 'textfield',
            name: 'title',
            placeHolder: 'タイトルを入力してください'
        },
        {
            xtype: 'textareafield',
            name: 'body',
            placeHolder: '本文を入力してください'
        }
    ]

});

/**
 * メモ登録のビューコントローラクラス。
 *
 * @class Memo.view.regist.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Memo.view.regist.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.regist',

    /**
     * showscreenイベント時の処理。
     * @param {Object} params パラメータ
     */
    onShowScreen: function (params) {
        var me = this,
            viewModel = me.getViewModel(),
            store = Ext.getStore('Memo'),
            record = null;

        if (Ext.isNumber(params.id)) {
            record = store.getById(params.id);
        }

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

        viewModel.set('record', record);
    },

    /**
     * 保存ボタンタップ時の処理。
     */
    onTapSaveButton: function () {
        var me = this,
            store = Ext.getStore('Memo'),
            form = me.getView(),
            record = form.getRecord(),
            validation;

        record.set(form.getValues());

        validation = record.getValidation();

        if (!validation.dirty) {
            if (record.phantom) {
                // 新規
                store.add(record);
            }

            store.sync();

            Ext.Msg.alert('完了', '保存しました', function () {
                me.redirectTo('list', true);
            });
        } else {
            var errorMsg = [];
            Ext.Object.each(validation.data, function (key, value) {
                if (value !== true) {
                    errorMsg.push(key + ':' + value);
                }
            });

            if (errorMsg.length > 0) {
                Ext.Msg.alert('エラー', errorMsg.join('<br>'));
            }
        }
    }

});

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

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

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

大きく異なる部分は、入力チェックの部分です。

classicでは入力フィールドにバリデーションチェックの定義を設定しますが、modernではモデルのvalidatorsにバリデーションチェックの定義を設定します。

具体的には、モデルのgetValidationの戻り値であるExt.data.Validationのdirtyでエラーがあるかどうかを判定します。

そして、エラーがあった場合の表示方法が乏しいのがmodernの特徴です(ノД`)

classicの場合、エラーのあった入力フィールドにエラーメッセージを表示する機能が標準で備わっていますが、modernでは無いためユーザ自身で実装する必要があります。

先のコードでは、メッセージボックスで簡単にエラー表示しています。

defaultTokenを設定しておく

ここはclassicと同じです。

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

    name: 'Memo',

    stores: [
        'Memo'
    ],

    defaultToken : 'list'
});

最終的に、↓のようになりました。

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

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

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

エラーメッセージがひどい状態ですが、今回はここまで。

次回は削除できるようにします。