(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: エラーが無い場合の処理をここに記述 } } });
複数のチェックがある場合はvalidatorsに配列を指定できます。
validators: [ 'url', { type: 'length', max: 140 } ]
モデルで定義していたバリデーション定義を、そのままフィールドに移した感じです。
バリデーション用のクラスは、Ext.data.validatorパッケージに存在します。
また、エラーメッセージの表示はフィールドのerrorTargetコンフィグで変更できます。
たとえば、errorTarget: 'under'とすれば↓のように表示されます。
いやー、ようやくclassicに追いつきましたね!
Ext.field.InputMask
入力形式のアシスト機能として、Ext.field.InputMaskが実装されました。
どういうことができるかというと、↓のカード番号入力のようなことです。(こういう入力って一般的な名称があるんでしょうか・・)
時々、サポートしているサイトがありますが、入力しやすくて便利ですよね。これがサポートされたのはうれしい限りです。
もう少し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}' ] } });
特に何も設定していない場合は、↑となります。addressがアソシエーションの項目です。
associatedData: falseを指定すると、全てのアソシエーションを除外します。
items: { xtype: 'list', store: 'User', itemTpl: [ '{name}<br>{address.state}{address.cities}' ], associatedData: false }
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 }] } } } });
行をスワイプすると、右側からボタンが現れます。いいですね。こういうのを見ると、すぐ導入したくなっちゃいます。こういう機能追加は大歓迎です。
さらに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コンフィグだけ抜粋です。
これは、スワイプ途中で離すとアクションが発生するようです。画像2枚目の状態で指を離すと、onMessageメソッドが実行されるようになっています。
なんかすごいですね。AndroidやiOSのAPIにはあまり詳しくないですが、こういう機能が標準搭載されているのでしょうか?
一応グリッドでも使えるみたいですね。横スクロールがある場合、あんまりよろしくないようですが。
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 } } });
pullrefresh: trueを指定すると、↓のようにpullrefreshspinnerを使うようです。
plugins: { pullrefresh: { widget: 'pullrefreshspinner', overlay: true } }
ほかにもpullrefreshbarを使う方法もあるようです。
plugins: { pullrefresh: { widget: 'pullrefreshbar', overlay: false } }
たしか元々のUIはこれでしたね。スピナーのほうは無かったと思います。
(ExtJS6.5)グリッドの新機能[modern] (3)
今回もグリッドについてです。細かいところばかりなので、前回書いとけば良かったです。
Virtual Stores
Ext.data.virtual.Storeというクラスが追加されたようです。
BufferedStoreと同じようなクラスであるとの記述があります。
管理するデータセットのうち必要なレコードおよびその前後のレコードをロード・保持できるようです。
たしかにBufferedStoreと同じような気がしますが、残念ながら私自身は具体的な利用箇所をイメージできていません。
いくつか違いがあるようですが、そのうち「複数のビューで共有可能」という点があるので、VirtualStoreのほうが応用効くのかな。(そもそもBufferedStoreが共有できないことを知らなかったですが)
もう少し情報を仕入れてから判断したいところです。
選択用クラス名が変わってる
名前変わっただけじゃなくて、いくつか機能も追加されているようです。
Ext.grid.plugin.MultiSelection -> Ext.grid.plugin.RowOperations
グルーピング
グリッドのgroupHeaderコンフィグでテンプレートなどを指定するようになりました。
前回しれっとグルーピングしているので、具体的なコードはそちらで確認できます。
http://sham-memo.hatenablog.com/entry/2017/06/02/200247
グリッドのサマリー
Ext.grid.SummaryRowを使いますが、groupFooterかpinnedFooterコンフィグに指定するようになりました。
年齢を追加して、平均年齢を表示させてみました。
Ext.define('Sample.view.main.Main', { extend: 'Ext.Panel', xtype: 'app-main', requires: [ 'Ext.layout.Fit' ], layout: 'fit', title: 'ユーザ一覧', items: { xtype: 'grid', store: { proxy: 'memory', fields: [ 'id', 'state', 'name', 'tel', { name: 'age', summary: 'average' } ], 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 } ], grouper: { property: 'state' } }, grouped: true, groupHeader: { tpl: 'Group: {name} ({count})' }, groupFooter: { xtype: 'gridsummaryrow', cls: 'user-summaryrow' }, columns: [ { dataIndex: 'id', text: 'ID', align: 'right' }, { dataIndex: 'name', text: '氏名', minWidth: 150, flex: 1 }, { dataIndex: 'tel', text: '電話番号', width: 200 }, { dataIndex: 'age', text: '年齢', width: 150, align: 'right', summaryCell: 'numbercell' } ] } });
pinnedFooterコンフィグを使う場合は、pinFooters: trueも併せて指定します。pinnedFooterを使うと、フッター部分をピン留めするようにグリッドのスクロール時にサマリー行が下部に留まってくれるようです。
あと、試しにサマリー行の背景色を変えようとして気づいたことがあります。
scssファイルの配置場所がjsファイルと同じところになってるようです。Main.jsと同じディレクトリにMain.scssを作成したら確かに反映されました。ExtJS7でもこれでいくのかな。
レーティング
Ext.ux.rating.Pickerで5段階のレーティングを表現できます。標準装備に加わりました。
他にもあるようですが、グリッドは以上で。次はリストを見てみるつもりです。
(ExtJS6.5)グリッドの新機能[modern] (2)
今回もExtJS6.5のmodernグリッド改善点です。
グリッドの各所にアイコンボタンを設置できるようになった!
スマフォやタブレットでお馴染みのアイコンのボタンを、グリッドの色々な箇所に設置できるようになりました。
Ext.define('Sample.view.main.Main', { extend: 'Ext.Panel', xtype: 'app-main', requires: [ 'Ext.layout.Fit' ], controller: 'main', viewModel: 'main', layout: 'fit', items: { xtype: 'grid', store: { proxy: 'memory', data: [ { id: 1, state: '鹿児島県', name: 'yamada1', tel: '09011111111' }, { id: 2, state: '鹿児島県', name: 'yamada2', tel: '09022222222' }, { id: 3, state: '鹿児島県', name: 'yamada3', tel: '09033333333' }, { id: 4, state: '鹿児島県', name: 'yamada4', tel: '09044444444' }, { id: 5, state: '鹿児島県', name: 'yamada5', tel: '09055555555' }, { id: 6, state: '鹿児島県', name: 'yamada6', tel: '09066666666' }, { id: 7, state: '鹿児島県', name: 'yamada7', tel: '09077777777' }, { id: 8, state: '鹿児島県', name: 'yamada8', tel: '09088888888' }, { id: 9, state: '鹿児島県', name: 'yamada9', tel: '09099999999' } ], grouper: { property: 'state' } }, platformConfig: { desktop: { plugins: { gridcellediting: true } }, '!desktop': { plugins: { grideditable: true } } }, grouped: true, groupHeader: { tpl: 'Group: {name}', tools: { print: { tooltip: 'Print group', zone: 'tail' }, save: { }, refresh: { } } }, columns: [ { dataIndex: 'id', text: 'ID', align: 'right' }, { dataIndex: 'name', text: '氏名', minWidth: 150, flex: 1, editable: true, tools: { gear: { zone: 'end', handler: function (column, tool, e) { // columnは列への参照。 // toolはアイコンボタンへの参照(Ext.Toolクラスのインスタンス) // eはイベントオブジェクト。 } } }, cell: { tools: { gear: { zone: 'end', handler: function (grid, config) { // gridはグリッドへの参照。 // configには、cell・column・event・grid・record・toolのプロパティが存在 } } } } }, { dataIndex: 'tel', text: '電話番号', width: 200, editor: 'textfield' } ] } });
上記のように、toolsプロパティを指定すると、これまで設置が大変だったカラムやグループの部分にもボタンを設置できます。
gearやprintのプロパティ名がアイコンの種類になるようです。Developer Toolで直接参照したところ、該当アイコンのclass属性は「x-icon-el x-font-icon x-tool-type-print」となっていたので(printの場合)、おそらくMaterial Iconsに存在するアイコンが指定できるのだと思われます。ext/modern/theme-neptune/sass/src/Tool.scssにアイコンの定義があったので、そこに存在する分は指定できそうです。無ければ同じように追加できる気がします。
あと、ツリー形式のグリッドでも対応してるみたいです(本投稿では省略します)。
カラムメニューが使える!
classicのようなカラムのメニューが実装されました。
ソート順やカラムの表示・非表示を切り替える、classicでは定番のUIですね。
さらに、メニューの項目も簡単に追加カスタマイズできるようです。
columns: [ { dataIndex: 'id', text: 'ID', align: 'right', menu: { custom: { text: 'Custom Item', separator: true, handler: 'onCustom' } } }, ・・・
(ExtJS6.5)グリッドの新機能[modern] (1)
ExtJS6.5でmodernのグリッドにいくつか変更箇所があります。
今回は選択と編集を試してみました。
選択
Ext.grid.Gridのselectableコンフィグを使って、どういう選択を可能にするかを設定できるようになりました。
items: [{ xtype: 'grid', selectable: { checkbox: true, cells: true } }]
プラグインで指定していた以前より、可読性が良くなりました。
ドキュメントを見る限り8つの項目を指定できるようです。
- mode `'single'`, `'multi'` Allow selection of only a single or multiple *records*. This is only valid when selecting rows. - deselectable Configure as false to disallow deselecting down to zero selected *records*. This is only valid when selecting rows. - drag `true` or `false` to allow drag gestures to swipt a rage of cells or rows. - columns `true` to enable column selection by clicking on headers. Defaults to `false` - cells `true` to enable cell selection by clicking or dragging on cells. Defaults to `false` - rows Set to `false` to disable selecting rows. Defaults to `true` - checkbox `true` to add a checkbox column to display selected state. `'only'` to indicate that only clicks on the checkbox affect row selected state. - extensible `true` to enable the selection to be extended either in the `X` or `Y` axis or `'x'` or `'y'` to configure
グリッドの場合は、Ext.grid.selection.Model(http://docs.sencha.com/extjs/6.5.0/modern/Ext.grid.selection.Model.html)というセレクションモデルのクラスを使っているようで、このクラスのコンフィグとして渡してくれるみたいです。
編集
Ext.grid.plugin.CellEditingプラグインクラスを使って、セル単位で編集できるようになりました。classicでは当たり前にできていましたが、modernでは新規です。
これまでの編集は、行を選択すると、編集フォームが画面外からスライドイン表示され、そこで編集作業を行うという流れでした。
しかし、PCで扱う場合は直接編集できたほうが操作感が良いため、PC向けに作成されたようです。
このことから、最終的にはPC・モバイルともにmodernにまとめたいという意思を感じます。
PC向けの場合は新しく追加されたプラグインを、モバイル向けの場合はこれまでのUIを使いたい場合は、platformConfigで設定するコンフィグを切り替えできるようです。
items: [{ xtype: 'grid', platformConfig: { desktop: { plugins: { gridcellediting: true } }, '!desktop': { plugins: { grideditable: true } } }, ...] }]
試してみると、PCのユーザエージェントだと↓のようにセル単位でのインライン編集となり
タブレットのユーザエージェントだと↓のように編集フォームでの編集操作となりました。
インライン編集は、編集中にグリッドをスクロールしても編集の行はその位置に固定された状態になるようです。なるほど、スーパーパワー!(意味が分からない人はドキュメントをお読みください)
アプリケーションで使用するExtJSのバージョンを上げる
ExtJS6.5がリリースされましたね。
https://www.sencha.com/blog/announcing-ext-js-6-5-and-sencha-cmd-6-5-ga/
いろいろ変更ありますが、modern向け機能の強化がうれしいです。
早速SDKとSenchaコマンドをダウンロードしたので、アプリケーションで使用するExtJSのバージョンを6.5に上げてみます。
> cd (アプリケーションのパス) > sencha app upgrade (ExtJS6.5 SDKのパス) Sencha Cmd v6.5.0.180 [INF] Upgrading framework ext [INF] Copying framework to (アプリケーションのパス)/ext [INF] Framework 'ext' upgraded
workspace.jsonを見ると↓のように6.5になりました。
"frameworks": { "ext": { "path":"ext", "version":"6.5.0.775" } }
更新後は.senchaディレクトリのファイルがごそっと削除されています。cfgやxmlファイルをカスタマイズしてた場合は影響を受けそうですね。6.5以降は、.senchaディレクトリを使わなくなるのかな?
app.jsonのrequiresで、modernに存在しないext-localeを定義していたらsencha app buildでエラーとなるようになっていました。
requiresはclassicとmodernで分けておいたほうが良さそうです。
あとはES6サポートを試してみました。class Panel {} みたいに書けるのかと思っていましたが、まだそこまでの対応ではないようです。
PromiseのようなES6の機能を使って実装した場合、ビルドしたときにサポートしていないブラウザでも動作するように出力してくれるだけみたいです。
おそらくExtJS7で構文が変わるんでしょうね。
今後、他の機能をちょこちょこ試していこうと思います。
あ、以降の投稿ではExtJS6.5を使うつもりです。