あーあーそのー

最近はshuyuheyと名乗ることが多いですが面倒なのでBLThunder1991のままにします。

Riot.jsでどんどん重なっていくモーダルの背景色を薄いまま保つ

最近Riot.jsを使った案件をやっていて、そこで困ったことをどう解決したかを整理しておく。

やりたかったこと

Webの表現ではよくあると思うのだけど、要素をクリックすると詳細な画面とかメッセージがポップアップっぽく表示されて、後ろが半透明の黒背景で覆われていて他の要素がクリックできないようになる、みたいな要件。

f:id:BLThunder1991:20180722123557p:plain

今回の要件では、モーダルの中で再び別の要素をクリックすると、それが更に重なってモーダルで表示されるというものになっていた。モーダルがどんどん重なっていく、ということ。

パーツ的には半透明の黒背景(以下、カバーって呼びます)とコンテンツという構成になっているので、何も考えずにカバーとコンテンツを束ねた、modal-content.tag のようなタグを作ってしまって、 mount して一番外側の divappendChild するという感じにしたかった。

ただ、ここで問題になってくるのはモーダルが重なれば重なるほど、カバーの色が濃くなっていってしまうということ。そんなたくさん重ねることもないだろうけど、モーダルが増える度に濃くなっていくというのは目に見えて明らかで、なんだかかっこ悪いなあということで対処方法を考えた。

f:id:BLThunder1991:20180722124152g:plain

トライしたこと

カバーをタグとして切り出してしまって、常にアプリケーション内にカバータグを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 っていうインスタンス変数的なものは追加されてしまうので、それを万が一上書きするような事があっても面倒だし。

全部のコードは以下。

github.com

まとめ

Riot.jsで困ったという話ではなくて、おそらく何を使っていても困ったと思うのだけど、Riot.jsではこういう風に表現できるかもねということを整理した。

本当は、CSSとかでシュッと処理できるのではないかとも思っているのだけれど、すぐには思いつかなかった。どこかで出会ったら書き直したほうが効率が良さそう。