Riot.jsでどんどん重なっていくモーダルの背景色を薄いまま保つ
最近Riot.jsを使った案件をやっていて、そこで困ったことをどう解決したかを整理しておく。
やりたかったこと
Webの表現ではよくあると思うのだけど、要素をクリックすると詳細な画面とかメッセージがポップアップっぽく表示されて、後ろが半透明の黒背景で覆われていて他の要素がクリックできないようになる、みたいな要件。
今回の要件では、モーダルの中で再び別の要素をクリックすると、それが更に重なってモーダルで表示されるというものになっていた。モーダルがどんどん重なっていく、ということ。
パーツ的には半透明の黒背景(以下、カバーって呼びます)とコンテンツという構成になっているので、何も考えずにカバーとコンテンツを束ねた、modal-content.tag
のようなタグを作ってしまって、 mount
して一番外側の div
に appendChild
するという感じにしたかった。
ただ、ここで問題になってくるのはモーダルが重なれば重なるほど、カバーの色が濃くなっていってしまうということ。そんなたくさん重ねることもないだろうけど、モーダルが増える度に濃くなっていくというのは目に見えて明らかで、なんだかかっこ悪いなあということで対処方法を考えた。
トライしたこと
カバーをタグとして切り出してしまって、常にアプリケーション内にカバータグを1つだけ存在させるという方法をまず試した。
モーダルのタグが mount
されたら、modal_open
みたいなイベントを trigger
してインクリメント、 unmount
したら逆にデクリメントして、0になったらカバーを display: none
させるみたいな実装。
一応これでもそれらしい動きをしたのだけど、今回の要件ではモーダルのサイズとか位置がいくつかパターンがあり、重なり方によっては一個奥で開かれているモーダルがカバーに覆われていないということが発生した。
まあ常に一番奥にカバーをおいているから当たり前なんだけど。
結局どうしたか
結局、 modal-content.tag
自身に cover
というタグを内包させることにした。そして、モーダルが重なる度に、常に一番手前にある cover
だけに色をつける、という方法をとった。
mount
される度に自分が一番手前の cover
かどうかを調べるっていう事も考えたけど、イマイチかなあと思ってやめた。自分の1つ手前のやつが消えたら色を変えるっていうのもやらなくてはならないので、 mount
だけが色の決定のトリガーにはならない。どこかのカバーが消えたことと出たことに反応して自分の色を決定しなければいけない。
そのときに、Riot.jsの機能の一つである、Mixinというのを使ってcover
タグのクラス変数みたいな使い方をしてみた。これが今回のエントリの本題になる。
MixInの中で、modal
が閉じたり開いたりしたかどうかのイベントをlisten
して、 自分が手前に来たら isDark
みたいなフラグを立ててあげるようにした。
自分自身が unmount
されるときは、 cover
のリストからIDを取り除きつつ、閉じたことを trigger
するという感じ。
import riot from 'riot'; const coverObservable = riot.observable(); let nextId = 0; let ids = []; const CoverEvent = { OPEN_MODAL: Symbol('OPEN_MODAL'), CLOSE_MODAL: Symbol('CLOSE_MODAL') }; export default class CoverMixin { init() { this.coverId = ++nextId; ids.push(this.coverId); this.isDark = true; this.on('mount', () => { coverObservable.trigger(CoverEvent.OPEN_MODAL); coverObservable.on(CoverEvent.OPEN_MODAL, this.onOtherCoverOpen); coverObservable.on(CoverEvent.CLOSE_MODAL, this.onOtherCoverClose); }); this.on('unmount', () => { _.remove(ids, (item) => { return item === this.coverId }); coverObservable.off(CoverEvent.OPEN_MODAL, this.onOtherCoverOpen); coverObservable.off(CoverEvent.CLOSE_MODAL, this.onOtherCoverClose); coverObservable.trigger(CoverEvent.CLOSE_MODAL); }); } onOtherCoverOpen() { this.toggle(); } onOtherCoverClose() { this.toggle(); } toggle() { if (_.last(ids) === this.coverId) { this.toDarken(); } else { this.toLighten(); } } toDarken() { this.isDark = true; this.update(); } toLighten() { this.isDark = false; this.update(); } }
このMixinを cover
タグに mixin
しておけば、 mount
されたときと unmount
されたときに勝手に背景に色を付けるべきかどうかを判断してくれるという仕組みになっている。
イベントのやりとりもすべて、observable
を中で持っているMixinの中で閉じているので、全体の observable
に表示用のロジックが紛れ込むこともないので、結構良いのではと思っている。
cover
をタグとして切り出さなくても、modal
のタグにMixInすればいいんじゃないの、とかも思ったがただ後ろに半透明の黒背景出したいだけなのに毎回毎回カバー用のCSSとか処理を書くのもなあと思って切り出している。あと、 coverId
っていうインスタンス変数的なものは追加されてしまうので、それを万が一上書きするような事があっても面倒だし。
全部のコードは以下。
まとめ
Riot.jsで困ったという話ではなくて、おそらく何を使っていても困ったと思うのだけど、Riot.jsではこういう風に表現できるかもねということを整理した。
本当は、CSSとかでシュッと処理できるのではないかとも思っているのだけれど、すぐには思いつかなかった。どこかで出会ったら書き直したほうが効率が良さそう。