(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 } ] } } ] });
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 } ] } ] });
weightが小さいほうがより先頭位置になります。
(ExtJS6.5)フォームの新機能[modern] (2)
今回までフォーム関連の新機能についてです。
デバイスに応じて日付フィールドの入力方法が切り替わる
これまでmodernでの日付入力といえば、画面下部からスライドインするパネルでドラムをクルクル回して日付を選択するというものでした。
ExtJS6.5になっても、モバイルのブラウザで操作する場合は基本的には変わらないのですが、PCブラウザで操作する場合には、classicと同じ入力形式に切り替わるようになりました。
ドラムでの入力ってもう古いと思うんですが、どうなんでしょうね?個人的には、画面中央に日付選択用のダイアログが表示されたほうが入力しやすいんですが・・。
あと、時間入力用のフィールドを作ってほしいな・・。あのアナログ時計形式のやつ。
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か月分に広がりました。
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: エラーが無い場合の処理をここに記述 } } });
複数のチェックがある場合は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' } } }, ・・・