初心者のためのExtJS入門

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

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

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

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

今回からExtJS6.5のフォームの変更点を挙げていきます。

バリデーション

バリデーションは大きく変わり、classicに寄せられました。

以前のバリデーション機能はモデル側で実装されており、フォームの各フィールドで設定できるのはごくわずかでした。

しかし、今回の変更でフィールドに直接バリデーションの内容を指定できるようになり、また、エラーメッセージも表示されるようになっています(ようやく)。

/**
 * ユーザ登録フォームクラス。
 *
 * @class Sample.view.user.regist.Form
 * @extend Ext.form.Panel
 */
Ext.define('Sample.view.user.regist.Form', {
    extend: 'Ext.form.Panel',
    xtype: 'user_regist_form',

    requires: [
        'Ext.field.Text',
        'Ext.field.Date',
        'Ext.field.Radio',
        'Ext.field.Container',
        'Ext.data.validator.Email',
        'Sample.view.user.regist.FormViewController'
    ],

    controller: 'user_regist_form',

    cls: 'user-regist-form',

    title: 'ユーザ登録',

    items: [
        {
            xtype: 'emailfield',
            label: 'メールアドレス',
            name: 'mailAddress',
            required: true,
            maxLength: 255,
            validators: 'email'
        },
        {
            xtype: 'textfield',
            label: '氏名',
            name: 'name',
            required: true
        },
        {
            xtype: 'textfield',
            label: 'フリガナ',
            name: 'kana',
            required: true
        },
        {
            xtype: 'datefield',
            label: '生年月日',
            name: 'birthday'
        },
        {
            xtype: 'containerfield',
            layout: 'vbox',
            label: '性別',
            margin: '20 0 0',
            items: [
                {
                    xtype: 'radiofield',
                    name: 'gender',
                    label: '男性',
                    value: 1
                },
                {
                    xtype: 'radiofield',
                    name: 'gender',
                    label: '女性',
                    value: 2
                }
            ]
        }
    ],

    buttons: {
        save: {
            text: '登録',
            ui: 'action',
            handler: 'onClickSaveButton'
        }
    }
});

/**
 * ユーザ登録フォームのビューコントローラクラス。
 *
 * @class Sample.view.user.regist.FormViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Sample.view.user.regist.FormViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.user_regist_form',

    /**
     * 保存ボタンクリック時の処理。
     */
    onClickSaveButton: function () {
        var me = this,
            view = me.getView();

        if (view.validate()) {
            // TODO: エラーが無い場合の処理をここに記述
        }
    }
});

f:id:sham-memo:20170610153813p:plain f:id:sham-memo:20170610153827p:plain

複数のチェックがある場合はvalidatorsに配列を指定できます。

validators: [
    'url',
    { type: 'length', max: 140 }
]

モデルで定義していたバリデーション定義を、そのままフィールドに移した感じです。

バリデーション用のクラスは、Ext.data.validatorパッケージに存在します。

また、エラーメッセージの表示はフィールドのerrorTargetコンフィグで変更できます。

たとえば、errorTarget: 'under'とすれば↓のように表示されます。

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

いやー、ようやくclassicに追いつきましたね!

Ext.field.InputMask

入力形式のアシスト機能として、Ext.field.InputMaskが実装されました。

どういうことができるかというと、↓のカード番号入力のようなことです。(こういう入力って一般的な名称があるんでしょうか・・)

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

時々、サポートしているサイトがありますが、入力しやすくて便利ですよね。これがサポートされたのはうれしい限りです。

もう少しUIとかスタイルをキレイにできるのかな?