React Component で作る window.confirm 代替品

window.confirmESLint でエラーが出るし、UI が良くない。なので似たような API で扱うことの出来る window.confirm の代用品を作ってみた。

参考: http://reactkungfu.com/2015/08/beautiful-confirm-window-with-react/

環境: React (16.1.1), React-Bootstrap (0.32.1)

Confirmation Modal

まずはスタイリッシュな Modal Window を作成。Modal が作れるなら別に React-Bootstrap じゃなくても良い。

class ConfirmationModal extends Component {
  constructor() {
    super();
    this.state = {
      show: true,
    };
  }

  abort = () => {
    this.setState({ show: false });
  };

  confirm = () => {
    this.setState({ show: false });
  };

  render() {
    const { show } = this.state;
    const { body, title } = this.props;

    return (
      <div className="static-modal">
        <Modal show={show} onHide={this.abort} backdrop>
          <Modal.Header>
            <Modal.Title>{title}</Modal.Title>
          </Modal.Header>
          <Modal.Body>{body}</Modal.Body>
          <Modal.Footer>
            <Button onClick={this.abort}>Cancel</Button>
            <Button
              className="button-l"
              bsStyle="primary"
              onClick={this.confirm}
            >
              Confirm
            </Button>
          </Modal.Footer>
        </Modal>
      </div>
    );
  }
}

confirm 関数で div を作成してそこに ConfirmationModal コンポーネントを描画。resolvecleanup を渡す。 Promiseresolve / reject 時に cleanup 関数で DOM を掃除しないとモーダル作成時に無限に div が増えていくことになる。

const confirm = ({ body, title }) => {
  const wrapper = document.body.appendChild(document.createElement('div'));
  const cleanup = () => {
    ReactDOM.unmountComponentAtNode(wrapper);
    return setTimeout(() => wrapper.remove());
  };
  const promise = new Promise((resolve, reject) => {
    try {
      ReactDOM.render(
        <ConfirmationModal
          cleanup={cleanup}
          resolve={resolve}
          title={title}
          body={body}
        />,
        wrapper
      );
    } catch (e) {
      cleanup();
      reject(e);
      throw e;
    }
  });
  return promise;
};

ConfirmationModal コンポーネントabort / confirm メソッドで真偽値を解決する。

class ConfirmationModal extends Component {
  constructor() {
    super();
    this.state = {
      show: true,
    };
  }

  abort = () => {
    const { resolve, cleanup } = this.props;
    this.setState({ show: false }, () => {
      resolve(false);
      cleanup();
    });
  };

  confirm = () => {
    const { resolve, cleanup } = this.props;
    this.setState({ show: false }, () => {
      resolve(true);
      cleanup();
    });
  };

  ...
}

使い方

ConfirmationModal の応答結果 (真偽値) を resolve しているので、async / await で次のように使うことが出来る:

async function() {
    const isConfirmed = await confirm({
      title: 'CAUTION',
      body: 'Are you sure?',
    });
    console.log(isConfirmed); // boolean
}

confirm

HOC

で、これを HOC 化したのがこちら

github.com

// confim.js
import { createConfirm } from 'react-confirm-decorator';

import ConfirmationModal from './ConfirmationModal';

const confirm = props => createConfirm(ConfirmationModal, props);

export default confirm;

好きな Modal のライブラリを使うことが出来る:

// ConfirmationModal.js
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import Button from 'react-bootstrap/lib/Button';

import { setConfirm } from 'react-confirm-decorator';

const ConfirmationModal = ({ show, confirm, abort, title, body }) => (
  <div className="static-modal">
    <Modal show={show} onHide={abort} backdrop>
      <Modal.Header>
        <Modal.Title>{title}</Modal.Title>
      </Modal.Header>
      <Modal.Body>{body}</Modal.Body>
      <Modal.Footer>
        <Button onClick={abort}>Cancel</Button>
        <Button className="button-l" bsStyle="primary" onClick={confirm}>
          Confirm
        </Button>
      </Modal.Footer>
    </Modal>
  </div>
);

export default setConfirm(ConfirmationModal);
import confirm from './confirm';

async function() {
    const isConfirmed = await confirm({
      title: 'CAUTION',
      body: 'Are you sure?',
    });
    console.log(isConfirmed); // boolean
}

react-confirm という似たふるまいのライブラリを途中見つけたけど、やりたかったこと (window.confirmの代用) とはちょっと違った。