独自コンポーネントでng-model
を使い、値を変更・参照する方法。
AngularJS1を使ったウェブアプリで要素を選択させる際に通常のselect
〜option
を使っていたのだけど、環境によって見た目が異なる、CSS Transformでscale
を使っていても無視して元のフォントサイズで描画される、スタイルを上手く指定できない、という問題があったため別の手段を使う必要があった。
Bootstrapのドロップダウンを使おうともしたんだけど、選択ボックスに選択された候補のテキストを当て込むようにしたい、そのため横幅を選択させる候補中の最大のものにしたい、それとBootstrapのドロップダウンは普通のインライン要素にできない?ので独自で実装することにした。
デモ
使用側
<my-select items="['apple', 'banana', 'carrot']" |
仮にコンポーネントのタグをmy-select
として、items
に配列を渡すとその中から1つをユーザが選択できる。
選択された項目はng-model
で指定した変数に格納される。
変更はng-change
で通知される。
- itemsの各要素は固定で、状態によって変わらないという前提
- 選択された項目はそのインデクスが
ng-model
で指定した変数に格納されるものとする- 未選択の場合に
-1
を指定しておくことで、表示の位置調整を簡潔にする
- 未選択の場合に
- 変更があったら
ng-change
が実行される
コンポーネントの実装
クラス定義:
class MySelect { |
$element.controller('ngModel')
でタグの属性として指定されたngModel
のコントローラを取得できる- 値の変更を通知するときに使用するため、保持しておく
$onInit() { |
- 初期化で
items
の各要素の幅を調べ、一番広いものを選択ボックスの幅とする - 要素の高さは選択ボックスの高さから求める(ボーダーが1pxという前提…)
- なぜ
$timeout
で待たなければいけないのかよくわからない…
テキストの幅計算:
static calcTextWidth(parent, text) { |
- 埋め込み先の要素にテキストを生成し、幅を取得する
white-space:nowrap
とした上でテキストを設定し、offsetWidth
で取得
選択ボックスがクリックされたとき:
onClick($event) { |
- ポップアップを開くためにスタイルを変更
- その際、フェードインさせるため
opacity
を操作(CSSでtransition-duration
を指定しておく) - フェードインが効かないことがある…なぜだ
- その際、フェードインさせるため
- 現在選択されている項目がポップアップ内の項目と合うように、現在の値(
this.ngModel.$viewValue
)から位置を計算 - ポップアップやその他どこかをクリックされた時に閉じるために、
document
にクリックハンドラを設定する
項目をクリックされたとき:
onClickItem($event, index) { |
ngModel
の$setViewValue()
で、ng-model
で指定された変数に設定できる- イベントの伝播を止めてやらないと、クリックイベントが選択ボックスに渡って再度開いてしまう
ポップアップを閉じる
closePopup() { |
- フェードアウトさせてから非表示にする
コンポーネント定義:
angular.module('mySelect', []) |
require: 'ngModel'
で必須にしてやるngModel
を扱うにはdirective
を使い、link
でなんか処理する必要があるのかと思っていたが、component
だけでできたngModel
を使用すれば自動的にngChange
が呼び出される?ようで、明示的に扱う必要はなかった
HTML:
<span class="select" |
- 未指定の場合(テキストが空の場合)に見た目を変えられるように、
empty
クラスを追加してやる - ポップアップ中の選択されている要素には
selected
クラスを追加する
SCSS:
my-select { |
- 選択ボックス、ポップアップなどは
display: inline-block
を指定 - 選択ボックスを
position:relative
にしてポップアップをposition:absolute
にすることで、ポップアップを選択ボックスからの相対位置で指定できるようにする - ポップアップ要素のhtmlは選択ボックス内の後の方に記述することで、
z-index
を指定しなくても他の要素より手前に描画されている(かぶる位置にz-index
を指定している要素がない前提)