2016年8月12日金曜日

ReactでModalを使いたいときに、body直下にModalをappendするみたいに、body直下にcomponentをおきたい的なお話

  • このエントリーをはてなブックマークに追加

ReactでModalを使おうと思った時に選択肢はいくつかあるわけで。
例えばReact BootstrapのModalとかMaterial UIのDialogとか。
けど実際こんなにたくさんcomponentを使うつもりはないんだよなぁってときはmodal単体のcomponentを使うということがある。
ちなみに実際に自分が使っているのはBoronっていうModal。
軽量だしそもそもそんなにオプションがないからすごく使いやすいしっていう。

けどこのBoronだけど面倒なことにbody直下にModalが生成されるわけではなく、componentを入れた位置にModalが生成される。
気持ち悪くないといえば気持ち悪くないけど、Modalみたいなものはbody直下においた方が気持ちいいよねっていう。

ということで今日はどうやってbody直下にModalを置くか、それを使うとどういうことができるか的なお話をば。

とりあえず普通のModalの設置方法は下記みたいな感じ。

import React from "react";
import ReactDOM from "react-dom";
import Modal from 'boron/DropModal';

export default class Page extends React.Component{
  render(){
    return (
      <div className="div1">
        <a href="#" onClick={(e) => {e.preventDefault();this.refs.modal.show()}}>open Modal</a>
        <div className="div2">
          <Modal ref="modal">modal contents</Modal>
        </div>
      </div>
    )
  }
}

ReactDOM.render(
  <Page />,
  document.getElementById("app")
);

ただこれだとdiv2の中にmodalが生成されていてなんていうかこう微妙に気持ち悪いというかなんというか。
で、これを解消するにはReactDOM.unstable_renderSubtreeIntoContainerReact.cloneElementを駆使してあげるっていう。

import React from "react";
import ReactDOM from "react-dom";
import Modal from 'boron/DropModal';

export default class Page extends React.Component{
  render(){
    return (
      <div className="div1">
        <a href="#" onClick={(e) => {e.preventDefault();this.refs.modal.show()}}>open Modal</a>
        <div className="div2">
          <ModalWrapper>
            <Modal ref="modal">modal contents</Modal>
          </ModalWrapper>
        </div>
      </div>
    )
  }
}

// modal wrapper
class ModalWrapper extends React.Component{
  constructor(props){
    super(props)
  }

  componentDidMount(){
    this._mountPoint = document.createElement('div');
    document.body.appendChild(this._mountPoint);
    this.renderPortal();
  }

  componentDidUpdate(){
    this.renderPortal();
  }

  componentWillUnmount() {
    if(!this._mountPoint) return
    ReactDOM.unmountComponentAtNode(this._mountPoint);
    document.body.removeChild(this._mountPoint)
  }
  
  renderPortal(){
    let { children, ...props } = this.props;
    ReactDOM.unstable_renderSubtreeIntoContainer(this,React.cloneElement(children,props),this._mountPoint);
  }

  render() {
    return null
  }
}

ReactDOM.render(
  <Page />,
  document.getElementById("app")
);
こんな感じでOK。
このModalWrapperがchildren(ModalWrapperで入れ子になっているもの)をcloneElementとして取得し、
componentDidMount時に生成したbody直下のdiv内にpropsを維持した状態で渡すっていう。

とりあえずこのunstable_renderSubtreeIntoContainerっていうのを使うことで任意の位置にReact Elementを渡せるっていう。
なのでReactDOM.unstable_renderSubtreeIntoContainer(this,<p>test</p>,this._mountPoint)でも問題はなかったりする。

ってな感じでやれば気持ち悪さはなくなるから、CSSとかを適用する際にも変なことを気にしないで大丈夫っていう。
ちなみにReact BoortstrapのModalは最初からBody直下に生成されるけどっていう。

Adsense