初心者のためのExtJS入門

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

ツリーパネル[modern]

今回はmodernのツリーパネルです。

できることはclassicのツリーパネルとほぼ同じです。

基本的な使い方

モデルとストアはclassicのときと同じものを使っています。

ビューにはExt.grid.TreeとExt.list.Treeを使えます。

ビュー

まずはExt.grid.Treeを使った場合です。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.grid.Tree',
    xtype: 'sample_panel',

    store: 'Earnings',

    rootVisible: false
});

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

次にExt.list.Treeを使った場合です。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.list.Tree',
    xtype: 'sample_panel',

    store: 'Earnings',

    ui: 'nav',

    expanderFirst: false
});

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

Ext.list.Treeはuiが少し用意されてるみたいです。expanderFirstコンフィグはExt.grid.Treeでも使えます。

Ext.list.TreeはExt.Gadgetを継承しているみたいで、Ext.grid.Treeとは根本的に違うようです。

編集などの機能はExt.grid.Treeでしか実装できません。

スマフォサイズで単純なツリーパネルを使う場合、expanderFirst: falseのほうが個人的に好きなUIです。

ツリーグリッド

最後に、ツリーグリッドにしてさらに編集可能にしてみました。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.grid.Tree',
    xtype: 'sample_panel',

    requires: [
        'Ext.grid.column.Tree',
        'Ext.grid.column.Number',
        'Ext.grid.plugin.CellEditing'
    ],

    store: 'Earnings',

    rootVisible: false,

    rowLines: true,
    columnLines: true,

    columns: [
        {
            text: 'グループ名',
            xtype: 'treecolumn',
            width: 300,
            flex: 1,
            dataIndex: 'text',
            editable: true
        },
        {
            text: '第1四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter1',
            align: 'right',
            editable: true
        },
        {
            text: '第2四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter2',
            align: 'right',
            editable: true
        },
        {
            text: '第3四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter3',
            align: 'right',
            editable: true
        },
        {
            text: '第4四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter4',
            align: 'right',
            editable: true
        }
    ],

    plugins: {
        gridcellediting: true
    }
});

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

ドラッグ&ドロップで入れ替えてみようと思いましたが、API見ても分かりませんでした。

知ってる人がいたらぜひ教えてください( )

ツリーパネル[classic] (2)

今回はツリーグリッドを取り上げます。

基本的な使い方

Ext.tree.Panelクラスを使うと、ツリーパネルとグリッドパネルを一緒にしたようなツリーグリッドUIを利用できます。

基本的な使い方はツリーパネルと同じで、それに加えてExt.tree.Panelクラスのcolumnsコンフィグを指定するだけです。

columnsコンフィグは、ほぼExt.grid.Panelのcolumnsコンフィグと同じです。

ストアとモデルは前回と同じものを使用しました

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.tree.Panel',
    xtype: 'sample_panel',

    requires: [
        'Ext.tree.Column',
        'Ext.grid.column.Number'
    ],

    title: 'ツリーグリッド サンプル',

    store: 'Earnings',

    columnLines: true,
    rowLines: true,
    rootVisible: false,

    columns: [
        {
            xtype: 'treecolumn',
            text: 'グループ',
            dataIndex: 'text',
            flex: 1
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter1',
            text: '第1四半期',
            width: 120,
            align: 'right'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter2',
            text: '第2四半期',
            width: 120,
            align: 'right'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter3',
            text: '第3四半期',
            width: 120,
            align: 'right'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter4',
            text: '第4四半期',
            width: 120,
            align: 'right'
        }
    ]
});

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

編集

編集には、グリッドパネルと同じプラグインを使えます。

例えば、セル編集であれば下記のようになります。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.tree.Panel',
    xtype: 'sample_panel',

    requires: [
        'Ext.tree.Column',
        'Ext.grid.column.Number',
        'Ext.grid.plugin.CellEditing'
    ],

    title: 'ツリーグリッド サンプル',

    store: 'Earnings',

    columnLines: true,
    rowLines: true,
    rootVisible: false,

    columns: [
        {
            xtype: 'treecolumn',
            text: 'グループ',
            dataIndex: 'text',
            flex: 1,
            editor: 'textfield'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter1',
            text: '第1四半期',
            width: 120,
            align: 'right',
            editor: 'numberfield'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter2',
            text: '第2四半期',
            width: 120,
            align: 'right',
            editor: 'numberfield'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter3',
            text: '第3四半期',
            width: 120,
            align: 'right',
            editor: 'numberfield'
        },
        {
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter4',
            text: '第4四半期',
            width: 120,
            align: 'right',
            editor: 'numberfield'
        }
    ],

    plugins: {
        cellediting: true
    }
});

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

 

 

ツリーパネルはちょっとクセがあって使いづらいですが、ツリーグリッドのUIは表現力が高いので、いろいろな場面で使いたくなりますね。

ツリーパネル[classic] (1)

今回はclassicのツリーパネルを取り上げます。

ツリーパネルでは、エクスプローラディレクトリやファイルの位置関係を階層構造で表現するUIを提供できるビューです。

modernにも同じようなビューがありますが、classicとはクラス名が異なるため今回はclassicのみ対象とします(次はmodernを取り上げる予定)。

基本的な使い方

モデル、ストア

モデルにはExt.data.TreeModelクラス、ストアにはExt.data.TreeStoreクラスを使います。

Ext.define('Sample.model.Earning', {
    extend: 'Ext.data.TreeModel',

    fields: [
        {
            name: 'text',
            type: 'string'
        },
        {
            name: 'quarter1',
            type: 'int'
        },
        {
            name: 'quarter2',
            type: 'int'
        },
        {
            name: 'quarter3',
            type: 'int'
        },
        {
            name: 'quarter4',
            type: 'int'
        }
    ]
});

Ext.define('Sample.store.Earnings', {
    extend: 'Ext.data.TreeStore',

    requires: [
        'Ext.data.proxy.Memory'
    ],

    model: 'Sample.model.Earning',

    proxy: {
        type: 'memory'
    },

    root: {
        text: '',
        children: [
            {
                id: 1,
                text: '全体',
                quarter1: 20000,
                quarter2: 11100,
                quarter3: 5200,
                quarter4: 13600,
                expanded: true,
                iconCls: 'x-fa fa-building',
                children: [
                    {
                        id: 2,
                        text: 'グループ1',
                        quarter1: 1000,
                        quarter2: 800,
                        quarter3: 500,
                        quarter4: 1200,
                        iconCls: 'x-fa fa-users',
                        leaf: true
                    },
                    {
                        id: 3,
                        text: 'グループ2',
                        quarter1: 5000,
                        quarter2: 2300,
                        quarter3: 1200,
                        quarter4: 3000,
                        iconCls: 'x-fa fa-users',
                        children: [
                            {
                                id: 4,
                                text: 'グループ2-1',
                                quarter1: 3000,
                                quarter2: 1500,
                                quarter3: 700,
                                quarter4: 1100,
                                iconCls: 'x-fa fa-users',
                                leaf: true
                            },
                            {
                                id: 5,
                                text: 'グループ2-2',
                                quarter1: 2000,
                                quarter2: 800,
                                quarter3: 500,
                                quarter4: 1900,
                                iconCls: 'x-fa fa-users',
                                leaf: true
                            }
                        ]
                    },
                    {
                        id: 6,
                        text: 'グループ3',
                        quarter1: 14000,
                        quarter2: 8000,
                        quarter3: 3500,
                        quarter4: 9400,
                        expanded: true,
                        iconCls: 'x-fa fa-users',
                        children: [
                            {
                                id: 71,
                                text: 'グループ3-1',
                                quarter1: 1000,
                                quarter2: 2000,
                                quarter3: 500,
                                quarter4: 1100,
                                iconCls: 'x-fa fa-users',
                                leaf: true
                            },
                            {
                                id: 8,
                                text: 'グループ3-2',
                                quarter1: 5000,
                                quarter2: 2000,
                                quarter3: 2300,
                                quarter4: 500,
                                iconCls: 'x-fa fa-users',
                                leaf: true
                            },
                            {
                                id: 9,
                                text: 'グループ3-3',
                                quarter1: 8000,
                                quarter2: 4000,
                                quarter3: 700,
                                quarter4: 7800,
                                iconCls: 'x-fa fa-users',
                                leaf: true
                            }
                        ]
                    }
                ]
            }
        ]
    }
});

上記ではストアのデータをインメモリにしていますが、ツリーストア用のデータ構造にする必要がある点に注意してください。

まず、最上位の位置にルートノードが必要です。

さらにその直下に所属すべきノードをchildrenプロパティで並べていきます。末端のノードにはleaf: trueを指定しましょう。

ノードのtextプロパティは、ビューに表示されるテキストです。(これtextから変更できるのかな?捜したけど見つからなかったです)

idプロパティは内部的に使っているみたいで、モデルのフィールドに定義したらエラーとなりました。何かIDとして付けておきたい場合は、別のフィールド名で用意しておくようにしましょう。

他にも使えるプロパティがありますが、詳細は下記リンクからドキュメントを参照してください。Ext.data.NodeInterfaceのコンフィグがほとんどそのまま指定できます。

http://docs.sencha.com/extjs/6.5.1/classic/Ext.data.NodeInterface.html

http://docs.sencha.com/extjs/6.5.1/classic/Ext.data.TreeStore.html#cfg-root

ビュー

あとはExt.tree.Panelでビューを作成し、storeに先ほど作成したストアを指定します。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.tree.Panel',
    xtype: 'sample_panel',

    title: 'ツリーパネル サンプル',

    store: 'Earnings',

    rootVisible: false

});

ルートノードは必ず1つなので、表現したいデータの最上位ノートが複数の場合は、rootVisible: falseとすることでルートノードを表示しないように制御できます。

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

mtype, childTypeでノードごとにモデルを切り替えられる

ツリーストアの場合、ノードごとにモデルを指定できます(使うべき場面を思いつかなかった。。mtype, childTypeを知らなかったのでとりあえず書いときました)

http://docs.sencha.com/extjs/6.5.1/classic/Ext.tree.Panel.html

ロード

サーバにリクエストを投げる場合、まずは通常のストアのようにプロキシを設定します。

Ext.define('Sample.store.Earnings', {
    extend: 'Ext.data.TreeStore',

    model: 'Sample.model.Earning',

    proxy: {
        type: 'ajax',
        url: 'dummy/earnings.json'
    }
});

これでloadメソッドが呼ばれると、下記のようにnodeというリクエストパラメータ付きでリクエストされます。

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

これはTreeStoreのnodeParamとdefaultRootIdで変更できます。

Ext.define('Sample.store.Earnings', {
    extend: 'Ext.data.TreeStore',

    model: 'Sample.model.Earning',

    proxy: {
        type: 'ajax',
        url: 'dummy/earnings.json'
    },

    nodeParam: 'id',

    defaultRootId: 1
});

こうすると、id: 1でリクエストされます。(ちなみに0はダメみたいです。なら'root'のままでサーバ側で判定しても良いのかもね)

ルートノードより下位のノードでは、各ノードのidプロパティの値を使って展開時にリクエストされるようになっています。

一度展開したノードは、一旦閉じてから再度展開してもロードしないようです。

その場合もロードさせたい場合は、下記のように無理やりloadedをfalseにすればロードしてくれました。もっと良い方法を知っていれば誰か教えてください。

Ext.define('Sample.store.Earnings', {
    extend: 'Ext.data.TreeStore',

    model: 'Sample.model.Earning',

    proxy: {
        type: 'ajax',
        url: 'dummy/earnings.json'
    },

    nodeParam: 'id',

    defaultRootId: 1,

    listeners: {
        nodecollapse: function (node) {
            node.set('loaded', false);
        }
    }
});

ちなみに、サーバからは下記のようなレスポンスを返します。

{
  "text": "Root",
  "children": [
    {
      "text": "全体",
      "quarter1": 20000,
      "quarter2": 11100,
      "quarter3": 5200,
      "quarter4": 13600,
      "expanded": false,
      "iconCls": "x-fa fa-building"
    }
  ]
}

追加、削除

ノードの追加、削除はモデルに対して行います。

Ext.data.TreeModelはExt.data.NodeInterfaceの機能を使えるようになっているので、appendChild、removeメソッドを呼ぶことができます。

listeners: {
    selectionchange: function (view, records) {
        var record = records[0];

        if (record) {
            record.appendChild({
                id: 100,
                text: 'hogehoge',
                leaf: true
            });
        }
    }
}

listeners: {
    selectionchange: function (view, records) {
        var record = records[0];

        if (record) {
            record.remove(true);
        }
    }
}

selectionchangeのタイミングで追加、削除というかなり雑なコードになっていますが、モデルに対してこのように記述すると、対象ノードの子ノードとして追加したり削除したりできます。

ドラッグ&ドロップ

Ext.tree.plugin.TreeViewDragDropプラグインを使います。

 Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.tree.Panel',
    xtype: 'sample_panel',

    requires: [
        'Ext.tree.plugin.TreeViewDragDrop'
    ],

    title: 'ツリーパネル サンプル',

    viewConfig: {
        plugins: {
            treeviewdragdrop: true
        }
    },

    store: 'Earnings',

    rootVisible: false
});

そのうえで、ノードのallowDrag、allowDropでドラッグ、ドロップ可否を制御できます。

ドロップ時にbeforedrop、dropイベントが発生するので、処理はそのタイミングで実行します。

レスポンシブ対応

今回はレスポンシブ対応を試してみます。

CSSではメディアクエリを使ってレスポンシブ対応しますが、ExtJSではExt.plugin.Responsiveクラスを使いjavascript上で処理します。

ExtJSのコードをざっと見た感じでは、resizeイベント発火を起点にしているようです。

基本的な実装方法

Ext.plugin.Responsiveプラグインを使用するようにして、responsiveConfigコンフィグで状態に応じた設定を指定します。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    requires: [
        'Ext.plugin.Responsive'
    ],

    cls: 'sample-panel',

    plugins: 'responsive',

    responsiveConfig: {
        'width < 800': {
            html: '幅が800px未満の場合のテキストを表示しています'
        },

        'width >= 800': {
            html: '幅が800px以上の場合のテキストを表示しています'
        }
    }
});

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

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

それぞれブラウザの幅が800px未満、800px以上の場合の表示です。

responsiveConfigコンフィグのキーとして、width < 800、width >= 800のように指定することで、対象コンポーネントのwidthが800px未満と800px以上という条件を指定できます。

さらにそれぞれの場合にどのような値を設定するかを、条件の値としてオブジェクトリテラルで指定できます。

responsiveConfigコンフィグの条件としては、下記の値が指定できます(http://docs.sencha.com/extjs/6.5.0/classic/Ext.mixin.Responsive.html#cfg-responsiveConfig より引用)。

landscape - True if the device orientation is landscape (always true on desktop devices).
portrait - True if the device orientation is portrait (always false on desktop devices).
tall - True if width < height regardless of device type.
wide - True if width > height regardless of device type.
width - The width of the viewport in pixels.
height - The height of the viewport in pixels.
platform - An object containing various booleans describing the platform (see Ext.platformTags). The properties of this object are also available implicitly (without "platform." prefix) but this sub-object may be useful to resolve ambiguity (for example, if one of the responsiveFormulas overlaps and hides any of these properties). Previous to Ext JS 5.1, the platformTags were only available using this prefix.

少し実用的な使い方

少し実用的な使い方だとこんな風にもできます。

ナビゲーションメニューの位置を、幅によって切り替えることを想定しています。

Ext.define('Sample.view.main.Main', {
    extend: 'Ext.panel.Panel',
    xtype: 'app-main',

    requires: [
        'Ext.plugin.Viewport',
        'Ext.plugin.Responsive',
        'Ext.layout.container.Border'
    ],

    layout: 'border',

    defaults: {
        xtype: 'panel'
    },

    items: [
        {
            xtype: 'panel',

            cls: 'menu-panel',

            plugins: 'responsive',

            responsiveConfig: {
                'width < 800': {
                    region: 'north',
                    width: '100%',
                    html: 'region: northのパネル'
                },
                'width >= 800': {
                    region: 'west',
                    width: 250,
                    html: 'region: westのパネル'
                }
            }
        },
        {
            region: 'center',
            cls: 'center-panel',
            html: 'region: centerのパネル'
        }
    ]
});

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

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

注意点

setメソッドがない場合は自分で定義する必要がある

responsiveConfigコンフィグに指定する値として、気を付けなければならない点があります。

レスポンシブの内部処理は、指定された設定値のキーに対してsetメソッドを呼び出すようになっています。

そのためsetメソッドが実装されていないコンフィグを指定した場合は、値が切り替わりません。

例えばclsコンフィグを切り替えようと、下記のようにしても反映されません。(以前はエラーになっていましたが無視されて処理継続されるようです)

responsiveConfig: {
    'width < 800': {
        region: 'north',
        cls: 'north-menu-panel',
        width: '100%'
    },
    'width >= 800': {
        region: 'west',
        cls: 'west-menu-panel',
        width: 250
    }
}

このような場合は、自分でsetメソッドを用意してあげる必要があります。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    config: {
        /**
         * @cfg {string} 現在のレスポンシブによるセレクタ名。
         */
        currentResponsiveCls: null
    },

    /**
     * clsコンフィグのsetメソッド(レスポンシブプラグインで呼び出される)。
     * @param {string} cls セレクタ名
     */
    setCls: function (cls) {
        var me = this,
            currentCls = me.getCurrentResponsiveCls();

        if (currentCls) {
            me.removeCls(currentCls);
        }

        if (cls) {
            me.setCurrentResponsiveCls(cls);
            me.addCls(cls);
        }
    }
});

Ext.define('Sample.view.main.Main', {
    extend: 'Ext.panel.Panel',
    xtype: 'app-main',

    requires: [
        'Ext.plugin.Viewport',
        'Ext.plugin.Responsive',
        'Ext.layout.container.Border'
    ],

    layout: 'border',

    defaults: {
        xtype: 'panel'
    },

    items: [
        {
            xtype: 'sample_panel',

            plugins: 'responsive',

            html: 'ナビゲーションメニューパネル',

            responsiveConfig: {
                'width < 800': {
                    region: 'north',
                    cls: 'north-menu-panel',
                    width: '100%'
                },
                'width >= 800': {
                    region: 'west',
                    cls: 'west-menu-panel',
                    width: 250
                }
            }
        },
        {
            region: 'center',
            cls: 'content-panel',
            html: 'コンテンツパネル'
        }
    ]
});
@charset "UTF-8";

.west-menu-panel {
    .x-panel-body-default {
        background-color: #90caf9;
    }
}

.north-menu-panel {
    .x-panel-body-default {
        background-color: #80cbc4;
    }
}

.content-panel {
    .x-panel-body-default {
        background-color: #e3f2fd;
    }
}

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

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

あまりスマートな記述ではありませんが、一応期待通りになりました。

aこういうsetメソッドはプラグインとして作成したほうが良いでしょうね。

modernのプラグインの動作があやしい

バグかもしれませんが、modernのExt.plugin.Responsiveの動作が若干怪しいです。

メインビューで直接レスポンシブを指定する場合は動作しましたが、ビューをクラス定義してその中でレスポンシブ指定するとなぜか動作しませんでした。

ExtJS6.5.1に期待します。

ワークスペース、パッケージの作成

Senchaコマンドを使って「ワークスペース」という構成を作成できます。

バックエンド用とフロントエンド用のプロジェクトが必要というような、ExtJSアプリケーションを複数作成する場合は特に便利です。

複数アプリケーションで共通のExtJS SDKやパッケージを参照できるようになるため、コードの再利用性を高めることができます。

ワークスペースの作り方

ワークスペースは、下記コマンドで作成します。

sencha -sd (ExtJSのSDKのパス) generate workspace (作成するワークスペースのパス)

-sdオプションは必須ではないですが、付けておくと共通で使用するSDKのコピーもやってくれます(ワークスペース直下に作成されるextディレクトリ)。

実行すると.gitignoreファイル、workspace.jsonファイル、そしてextディレクトリが作成されたはずです。

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

extディレクトリが共通で利用するExtJS SDKになります。

ワークスペース内にアプリケーションを作成

作成したワークスペースにアプリケーションを作成してみます。

ワークスペースに移動して、下記を実行します。

sencha -sd ./ext generate app Front ./apps/Front

apps/FrontにExtJSアプリケーションが作成されました(フロントエンド用のという意味で「Front」としてみました)。

同じようにBackendも作成してみます。

sencha -sd ./ext generate app Backend ./apps/Backend

apps/BackendにExtJSアプリケーションが作成されました(バックエンド用という意味で「Backend」としてみました)。

これでワークスペース内に2つのExtJSアプリケーションを作成できました。

パッケージの作り方

次にパッケージを作ってみます。

ワークスペースに移動して、下記を実行します。

sencha generate package Common

すると、packages/local/Commonにパッケージが作成されたはずです。

作成した時点ではclassicとmodern用に分かれていないので、分ける場合はapp.jsonclasspath等を修正します(↓の箇所ぐらいかな?)。

"classpath": [
    "${package.dir}/src",
    "${package.dir}/${toolkit.name}/src"
],

"overrides": [
    "${package.dir}/overrides",
    "${package.dir}/${toolkit.name}/overrides"
],

"sass": [
    "etc": [
        "${package.dir}/sass/etc/all.scss",
        "${package.dir}/${toolkit.name}/sass/etc/all.scss"
    ],

    "var": [
        "${package.dir}/sass/var",
        "${package.dir}/${toolkit.name}/sass/var"
    ],

    "src": [
        "${package.dir}/sass/src",
        "${package.dir}/${toolkit.name}/sass/src"
    ]
]

あとは対応するディレクトリを適宜作成すれば、1パッケージにclassicとmodern用のモジュールを作成できます。

ちなみに初めから2つパッケージを作って、それぞれclassic用とmodern用にするのもアリだと思います。

あとは、ExtJSアプリケーション側のapp.jsonでrequiresに追記すれば使えるようになります。

"requires": [
    "font-awesome",
    "Common"
]

(ExtJS6.5)細かい変更点

ようやく http://docs.sencha.com/extjs/6.5.0/guides/whats_new/whats_new.html は試し終えました。

d3やpivotパッケージはプレミアム機能は試せてないですが、そこは必要になったときに調べよう。

そういえば、ドキュメントのページも用意されているようなのでそろそろExtJS6.5.1もリリースされそうですよね。不具合修正されてるはずなので早くリリースしてほしい。

associatedコンフィグ

モデルで保存のリクエストを送信するときに、writerコンフィグを以下のように定義しておくとアソシエーションモデルのデータもリクエストパラメータに含めることができました。

Ext.define('Sample.model.Book', {
    extend: 'Ext.data.Model',

    fields: [
        {
            name: 'id',
            type: 'int'
        },
        {
            name: 'title',
            type: 'string'
        },
        {
            name: 'price',
            type: 'int'
        },
        {
            name: 'authorId',
            reference: {
                type: 'Sample.model.Author',
                inverse: 'books'
            }
        }
    ]
});

Ext.define('Sample.model.Author', {
    extend: 'Ext.data.Model',

    proxy: {
        type: 'ajax',
        url: '/dummy/author.json',
        writer: {
            allDataOptions: {
                associated: true
            }
        }
    },

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

    hasMany: [
        {
            name: 'books',
            associatedName: 'books',
            model: 'Sample.model.Book'
        }
    ]
});

この場合、リクエストパラメータにbooksプロパティが含まれるわけですが、アソシエーションが複数あるときは全てリクエストパラメータに含まれていました。

それを特定のものだけに指定できるようになっています。

associated: {
  books: true
}

ViewController

ビューモデルの値が変更されたときにビューコントローラの特定の処理を実行できるようになりました。

Ext.define('Sample.view.main.MainModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.main',

    data: {
        x: 100
    }
});

Ext.define('Sample.view.main.MainController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.main',

    bindings: {
        onChangeX: '{x}'
    },

    /**
     * ビューモデルのx値が変更された場合の処理。
     * @param {number} x x値
     */
    onChangeX: function (x) {
        console.log(x);
    }
});

どんな場面に使えるのかな。。

Google Mapのマーカー(modernだけっぽい)

Google Mapのマーカーのクラスが追加されたみたいです。(え、今まで無かったの?)

その他

アプリケーションのひな型を作成したら、app.jsがrequiresで"Sample.*“のようにアスタリスクを使ってクラスをインポートしてるのに気づきました。知らなかった。。

調べてみたら結構前からワイルドカード形式でクラスをインポートできるようになってたみたいです(https://plugins.jetbrains.com/plugin/7740-sencha-ext-js/update/20820)。

自分が作ったクラスなんて絶対全部使うんだから、ワイルドカード使ってインポートしたほうが楽ですよね。便利!!

(ExtJS6.5)チャート

今回はチャートの改善点です。

チャートはclassic、modernどちらも共通のパッケージを使用しています。そのため、classicでも恩恵に預かれます。

キャプション

captionsコンフィグを使って、チャートのタイトル・サブタイトル・クレジットを簡単に設定できるようになりました。

Ext.define('Sample.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app-main',

    requires: [
        'Ext.chart.CartesianChart',
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Time',
        'Ext.chart.series.Line'
    ],

    layout: 'fit',

    bodyPadding: 20,

    items: {
        xtype: 'cartesian',
        insetPadding: 20,
        innerPadding: '0 20 0 0',

        store: 'USDJPY',

        captions: {
            title: 'ドル・円推移',
            subtitle: {
                text: '2012~2013年あたりの終値の推移',
                style: {
                    color: '#0000aa'
                }
            },
            credits: 'データ配信元: http://www.central-tanshifx.com/market/finder/popn-csv-download.html'
        },

        axes: [
            {
                type: 'numeric',
                position: 'left',
                fields: ['end']
            },
            {
                type: 'time',
                position: 'bottom',
                fields: ['date'],
                dateFormat: 'Y/m/d'
            }
        ],

        series: [
            {
                type: 'line',
                xField: 'date',
                yField: 'end'
            }
        ]
    }
});

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

ナビゲータ

チャート全体を表すミニマップを設置し、描画する範囲や位置を切り替えることができるようになりました。

Ext.define('Sample.view.main.Main', {
    extend: 'Ext.Panel',
    xtype: 'app-main',

    requires: [
        'Ext.chart.CartesianChart',
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Time',
        'Ext.chart.series.Line',
        'Ext.chart.navigator.Container'
    ],

    layout: 'fit',

    bodyPadding: 20,

    items: {
        xtype: 'chartnavigator',
        reference: 'chartnavigator',

        navigator: {
            axis: 'bottom'
        },

        chart: {
            xtype: 'cartesian',
            reference: 'chart',
            insetPadding: 20,
            innerPadding: '0 20 0 0',

            store: 'USDJPY',

            captions: {
                title: 'ドル・円推移'
            },

            axes: [
                {
                    type: 'numeric',
                    position: 'left',
                    fields: ['end']
                },
                {
                    id: 'bottom',
                    type: 'time',
                    position: 'bottom',
                    fields: ['date'],
                    dateFormat: 'Y/m/d'
                }
            ],

            series: [
                {
                    type: 'line',
                    xField: 'date',
                    yField: 'end'
                }
            ]
        }
    }
});

f:id:sham-memo:20170625205230p:plain f:id:sham-memo:20170625205014p:plain

画面下のほうに表示されているのがナビゲータ部分です。背景が白色の四角で囲われた部分を移動したり、拡大・縮小したりして操作できます。

x軸に、id: ‘bottom'を設定していますが、これはnavigatorコンフィグのaxis: 'bottom'と一致させないといけません。Ext.chart.navigator.Containerの処理で、軸の参照を取得できなくなりエラーが発生してしまいます。

Ext.chart.series.BoxPlot

新しいチャートが追加されました。複数のデータの分布を表現できるみたいです。

http://examples.sencha.com/extjs/6.5.0/examples/kitchensink/?modern#boxplot-nobel

見た目はローソク足に似ていますが、時系列のデータではなく、国ごとの比較や年齢ごとの比較といったカテゴリ別のデータの場合に使用します。

こんなチャートがあるということを頭の片隅に入れておいて、使えそうな場面で適宜引っ張り出せるようにしましょう。

Ext.chart.series.Lineのcurveコンフィグ

以前からスプライン曲線はありましたが、curveコンフィグを指定して点と点をどう結ぶかをもう少し細かく指定できるようになりました。

APIには、↓の種類が挙げられています。

curve: {
    type: 'linear'
}

curve: {
    type: 'cardinal,
    tension: 0.5
}

curve: {
    type: 'natural'
}

curve: {
    type: 'step-after'
}

step-afterを指定すると↓のように描画されます。

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