初心者のためのExtJS入門

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

レスポンシブ対応

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

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

(ExtJS6.5)パネル・ダイアログ[modern]

ExtJS6.5におけるパネルの変更点とダイアログ追加についてです。

パネル

collapsibleコンフィグとresizableコンフィグが追加されました。classicではどちらも実装済みの機能です。

classicの場合はboolean型の値しか指定できませんが、modernではオブジェクトリテラルで↓のように指定します。

{
    docked: 'left',
    title: 'left panel',
    width: 200,
    collapsible: {
        collapsed: true,
        direction: 'left'
    }
}

また、resizableはオブジェクトリテラルで↓のように指定します。

{
    docked: 'top',
    title: 'top panel',
    minHeight: 100,
    resizable: {
        split: true,
        edges: 'south'
    }
}

edgesでリサイズ操作でタップするエッジ部分を指定します。複数方向を指定する場合はedges: ['west', 'south']のように配列指定するようです。

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

ダイアログ

Ext.Dialogというダイアログ用のクラスが追加されました。ウィンドウ

var dialog = Ext.create({
    xtype: 'dialog',
    title: 'Dialog',

    maximizable: true,
    html: 'Content<br>goes<br>here',

    buttons: {
        ok: function () {  // standard button (see below)
            dialog.destroy();
        }
    }
});

dialog.show();

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

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

Ext.Panelのサブクラスなのでアイテムコンポーネントも指定できます。Ext.MessageBoxよりも柔軟なことができそうですね。

ボタンはbuttonsコンフィグで↓のようにokやcancelをキーとしたオブジェクトリテラルで指定します。

buttons: {
    ok: 'onOK',
    cancel: 'onCancel'
}

buttonsコンフィグの値はstandardButtonsコンフィグの値とマージされて、↓がデフォルト値となっているbuttonToolbarコンフィグに渡されます。

buttonToolbar: {
    xtype: 'toolbar',
    itemId: 'buttonToolbar',
    docked: 'bottom',
    defaultType: 'button',
    weighted: true,
    ui: 'footer',
    defaultButtonUI: 'action',

    layout: {
        type: 'box',
        vertical: false,
        pack: 'center'
    }
}

weighted: trueとなっているので、ボタンの並び順はweightで変更できますね。

(ExtJS6.5)フローティングメニューの追加・weightedコンフィグ[modern]

今回は小ネタ2つです。

メニュー

これまでmodernのメニューといえば、ActionSheetを画面外からスライドイン(http://examples.sencha.com/extjs/6.5.0/examples/kitchensink/?modern#actionsheets)する形式がほとんどでしたが、今回classicのようなフローティング形式のメニューが使えるようになりました。これもmodernをPCブラウザでも使えるようにという点から追加されたものでしょう。

classicにとってはあまり目新しい機能があるわけではありませんが、↓のような感じで使います。

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

    items: [
        {
            xtype: 'button',
            text: 'menu',
            menu: {
                floated: false,
                items: [
                    {
                        text: 'item1'
                    },
                    {
                        text: 'item2(disabled)',
                        disabled: true
                    },
                    {
                        text: 'item3(uncheck)',
                        checked: false,
                        separator: true
                    },
                    {
                        text: 'item4(checked)',
                        checked: true
                    },
                    {
                        text: 'item5',
                        menu: {
                            items: [
                                {
                                    text: 'subitem1'
                                },
                                {
                                    text: 'subitem2'
                                }
                            ]
                        }
                    },
                    {
                        group: 'mygroup',
                        text: 'item6(group)',
                        value: 'item6',
                        checked: true,
                        separator: true
                    },
                    {
                        group: 'mygroup',
                        text: 'item7(group)',
                        value: 'item7'
                    },
                    {
                        group: 'mygroup',
                        text: 'item8(group)',
                        value: 'item8'
                    },
                    {
                        text: 'Open Google',
                        href: 'http://www.google.com',
                        separator: true
                    }
                ]
            }
        }
    ]
});

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

Exampleでは、Ext.menu.Itemのtargetコンフィグを使うようなコードが紹介されていてtarget: '_blank'としてありましたが、効きませんでした。そりゃまあバグは残ってますよね。

Ext.Containerのweightedコンフィグ

Ext.Containerのweightedコンフィグをtrueにすると、アイテムコンポーネントに指定されたweightの値で、順番を制御できるようになります。これもclassicでは既に存在する設定で、modernでも使えるようにしたようですね。

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

    layout: 'fit',

    items: [
        {
            xtype: 'container',
            layout: 'vbox',
            weighted: true,
            items: [
                {
                    xtype: 'button',
                    text: 'ボタン1',
                    weight: 20
                },
                {
                    xtype: 'button',
                    text: 'ボタン2',
                    weight: 10
                }
            ]
        }
    ]
});

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

weightが小さいほうがより先頭位置になります。

(ExtJS6.5)フォームの新機能[modern] (2)

今回までフォーム関連の新機能についてです。

バイスに応じて日付フィールドの入力方法が切り替わる

これまでmodernでの日付入力といえば、画面下部からスライドインするパネルでドラムをクルクル回して日付を選択するというものでした。

ExtJS6.5になっても、モバイルのブラウザで操作する場合は基本的には変わらないのですが、PCブラウザで操作する場合には、classicと同じ入力形式に切り替わるようになりました。

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

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

ドラムでの入力ってもう古いと思うんですが、どうなんでしょうね?個人的には、画面中央に日付選択用のダイアログが表示されたほうが入力しやすいんですが・・。

あと、時間入力用のフィールドを作ってほしいな・・。あのアナログ時計形式のやつ。

ExtJS7に期待します。

あと、コンボボックスも同じようにデバイスに応じて切り替わるみたいです。

classic形式の日付入力では、Ext.panel.Dateという新しいクラスが使用されています。

datefieldのfloatedPickerコンフィグで指定されており、設定を変えたい場合はここに直接指定すれば良さそうです。

floatedPicker: {
    xtype: 'datepanel',
    autoConfirm: true,
    floated: true,
    panes: 3,
    listeners: {
        tabout: 'onTabOut',
        scope: 'owner'
    },
    keyMap: {
        ESC: 'onTabOut',
        scope: 'owner'
    }
}

↑のようにpanes: 3を指定すると、範囲が3か月分に広がりました。

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

lookupNameメソッド

入力フィールドにはほぼnameコンフィグを指定しますが、nameコンフィグの値でフィールドを参照できるlookupNameメソッドが追加されました。

中身を見た感じ、一度参照を取得したらnameRefsプロパティにキャッシュしているようです。

たしかにあると便利かもしれないですね。

ちなみに、ラジオボタンのように同じnameの値が指定される場合は、配列で戻ってくるみたいですよ。

フォームレイアウト

この変更については、いまいちピンとこなかったんですが、フィールドのラベルの幅が自動的にいい感じのサイズに調整されますよってことだと思ってます。

該当箇所: http://docs.sencha.com/extjs/6.5.0/guides/whats_new/whats_new.html#whats_new--whats_new-_form_layout