初心者のためのExtJS入門

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

(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とかスタイルをキレイにできるのかな?

(ExtJS6.5)associatedDataコンフィグ[modern]

ExtJS6.5で、Ext.dataview.AbstractクラスにassociatedDataコンフィグが追加されました。

Ext.dataview.Abstractは、リストやグリッドの基底クラスです。

リストやグリッドのようなデータビューのレンダリング処理では、ストアのモデルのデータをitemTplコンフィグで指定されたテンプレート内の変数に埋め込みます。

その際、アソシエーション付けしたフィールドがあると、レンダリング処理の中でそのデータも全て取得しようとします。

はじめから表示する項目にアソシエーションのデータが含まれないのであれば、それを除外できたほうがパフォーマンス上有利です。

associatedDataコンフィグを使うとその設定を実現できます。

モデルとストア

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

    fields: [
        {
            name: 'state',
            type: 'string'
        },
        {
            name: 'cities',
            type: 'string'
        }
    ]
});

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

    requires: [
        'Sample.model.Address'
    ],

    fields: [
        {
            name: 'id',
            type: 'int'
        },
        {
            name: 'name',
            type: 'string'
        },
        {
            name: 'tel',
            type: 'string'
        },
        {
            name: 'age',
            type: 'int'
        },
        {
            name: 'address',
            reference: 'Sample.model.Address'
        }
    ]
});

Ext.define('Sample.store.User', {
    extend: 'Ext.data.Store',

    requires: [
        'Sample.model.User'
    ],

    model: 'Sample.model.User',

    proxy: 'memory',

    data: [
        {id: 1, name: 'yamada1', tel: '09011111111', age: 35, address: {state: '鹿児島県', cities: '鹿児島市1'}},
        {id: 2, name: 'yamada2', tel: '09022222222', age: 28, address: {state: '鹿児島県', cities: '鹿児島市2'}},
        {id: 3, name: 'yamada3', tel: '09033333333', age: 24, address: {state: '鹿児島県', cities: '鹿児島市3'}},
        {id: 4, name: 'yamada4', tel: '09044444444', age: 20, address: {state: '鹿児島県', cities: '鹿児島市4'}},
        {id: 5, name: 'yamada5', tel: '09055555555', age: 62, address: {state: '熊本県', cities: '熊本市1'}},
        {id: 6, name: 'yamada6', tel: '09066666666', age: 61, address: {state: '熊本県', cities: '熊本市2'}},
        {id: 7, name: 'yamada7', tel: '09077777777', age: 40, address: {state: '熊本県', cities: '熊本市3'}},
        {id: 8, name: 'yamada8', tel: '09088888888', age: 38, address: {state: '熊本県', cities: '熊本市4'}},
        {id: 9, name: 'yamada9', tel: '09099999999', age: 32, address: {state: '熊本県', cities: '熊本市5'}}
    ]
});

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

    requires: [
        'Ext.layout.Fit',
        'Ext.dataview.List',
        'Sample.store.User'
    ],

    controller: 'main',

    layout: 'fit',

    title: 'ユーザ一覧',

    items: {
        xtype: 'list',

        store: 'User',

        itemTpl: [
            '{name}<br>{address.state}{address.cities}'
        ]
    }
});

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

特に何も設定していない場合は、↑となります。addressがアソシエーションの項目です。

associatedData: falseを指定すると、全てのアソシエーションを除外します。

items: {
    xtype: 'list',

    store: 'User',

    itemTpl: [
        '{name}<br>{address.state}{address.cities}'
    ],

    associatedData: false
}

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

XTemplate evaluation exception: Cannot read property ‘state’ of undefined となり、addressがundefinedになっていることがわかります。

特定の項目だけ取得したいときは↓のようにするようです。

associatedData: {
  address: true
}

ちなみに、address: falseでも同じ結果になりました。バグっぽいね。

(ExtJS6.5)リストの新機能[modern]

今回はリストの追加機能を取り上げます。

行のスワイプ操作

Ext.dataview.listswiper.ListSwiperというプラグインクラスを使って、リストの行をスワイプするアクションが簡単に実装できるようになりました。

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

    requires: [
        'Ext.layout.Fit',
        'Ext.dataview.List',
        'Ext.dataview.listswiper.ListSwiper'
    ],

    controller: 'main',

    layout: 'fit',

    title: 'ユーザ一覧',

    items: {
        xtype: 'list',

        store: {
            proxy: 'memory',

            fields: [
                'id',
                'state',
                'name',
                'tel',
                'age'
            ],

            data: [
                { id: 1, state: '鹿児島県', name: 'yamada1', tel: '09011111111', age: 35 },
                { id: 2, state: '鹿児島県', name: 'yamada2', tel: '09022222222', age: 28 },
                { id: 3, state: '鹿児島県', name: 'yamada3', tel: '09033333333', age: 24 },
                { id: 4, state: '鹿児島県', name: 'yamada4', tel: '09044444444', age: 20 },
                { id: 5, state: '熊本県', name: 'yamada5', tel: '09055555555', age: 62 },
                { id: 6, state: '熊本県', name: 'yamada6', tel: '09066666666', age: 61 },
                { id: 7, state: '熊本県', name: 'yamada7', tel: '09077777777', age: 40 },
                { id: 8, state: '熊本県', name: 'yamada8', tel: '09088888888', age: 38 },
                { id: 9, state: '熊本県', name: 'yamada9', tel: '09099999999', age: 32 }
            ]
        },

        itemTpl: [
            '{name}'
        ],

        plugins: {
            listswiper: {
                defaults: {
                    width: 48
                },

                right: [{
                    iconCls: 'x-fa fa-envelope',
                    ui: 'alt confirm',
                    commit: 'onMessage'
                }, {
                    iconCls: 'x-fa fa-phone',
                    ui: 'alt action',
                    commit: 'onCall'
                }, {
                    iconCls: 'x-fa fa-trash',
                    ui: 'alt decline',
                    commit: 'onDeleteItem',
                    undoable: true
                }]
            }
        }
    }
});

f:id:sham-memo:20170605231956p:plain f:id:sham-memo:20170605232009p:plain

行をスワイプすると、右側からボタンが現れます。いいですね。こういうのを見ると、すぐ導入したくなっちゃいます。こういう機能追加は大歓迎です。

さらにExt.dataview.listswiper.Stepperを使って、もう少し違う形式にもなります。

plugins: {
    listswiper: {
        widget: {
            xtype: 'listswiperstepper'
        },

        defaults: {
            width: 48
        },

        right: [{
            iconCls: 'x-fa fa-envelope',
            ui: 'alt confirm',
            commit: 'onMessage'
        }, {
            iconCls: 'x-fa fa-phone',
            ui: 'alt action',
            commit: 'onCall'
        }, {
            iconCls: 'x-fa fa-trash',
            ui: 'alt decline',
            commit: 'onDeleteItem',
            undoable: true
        }]
    }
}

重複するのでpluginsコンフィグだけ抜粋です。

f:id:sham-memo:20170605231956p:plain f:id:sham-memo:20170605234107p:plain f:id:sham-memo:20170605234051p:plain f:id:sham-memo:20170605234115p:plain

これは、スワイプ途中で離すとアクションが発生するようです。画像2枚目の状態で指を離すと、onMessageメソッドが実行されるようになっています。

なんかすごいですね。AndroidiOSAPIにはあまり詳しくないですが、こういう機能が標準搭載されているのでしょうか?

一応グリッドでも使えるみたいですね。横スクロールがある場合、あんまりよろしくないようですが。

Pull Refresh

リストが一番上にある状態で、さらにスクロールさせることで最新データをリロードさせる操作ですね。

Ext.dataview.pullrefresh.PullRefreshプラグインクラスを使います。

ExtJS6.2だと、ブラウザの標準機能に影響を受けて正しく機能しなかったため全然使ってませんでしたが、今回は大丈夫そうです。スマフォで試してみても動作しているようでした。

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

    requires: [
        'Ext.layout.Fit',
        'Ext.dataview.List',
        'Ext.dataview.pullrefresh.PullRefresh'
    ],

    controller: 'main',

    layout: 'fit',

    title: 'ユーザ一覧',

    items: {
        xtype: 'list',

        store: {
            proxy: 'memory',

            fields: [
                'id',
                'state',
                'name',
                'tel',
                'age'
            ],

            data: [
                {id: 1, state: '鹿児島県', name: 'yamada1', tel: '09011111111', age: 35},
                {id: 2, state: '鹿児島県', name: 'yamada2', tel: '09022222222', age: 28},
                {id: 3, state: '鹿児島県', name: 'yamada3', tel: '09033333333', age: 24},
                {id: 4, state: '鹿児島県', name: 'yamada4', tel: '09044444444', age: 20},
                {id: 5, state: '熊本県', name: 'yamada5', tel: '09055555555', age: 62},
                {id: 6, state: '熊本県', name: 'yamada6', tel: '09066666666', age: 61},
                {id: 7, state: '熊本県', name: 'yamada7', tel: '09077777777', age: 40},
                {id: 8, state: '熊本県', name: 'yamada8', tel: '09088888888', age: 38},
                {id: 9, state: '熊本県', name: 'yamada9', tel: '09099999999', age: 32}
            ]
        },

        itemTpl: [
            '{name}'
        ],

        plugins: {
            pullrefresh: true
        }
    }
});

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

pullrefresh: trueを指定すると、↓のようにpullrefreshspinnerを使うようです。

plugins: {
    pullrefresh: {
        widget: 'pullrefreshspinner',
        overlay: true
    }
}

ほかにもpullrefreshbarを使う方法もあるようです。

plugins: {
    pullrefresh: {
        widget: 'pullrefreshbar',
        overlay: false
    }
}

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

たしか元々のUIはこれでしたね。スピナーのほうは無かったと思います。