如何创建带有过渡的 React Modal(附加到 <body>)?

How to create a React Modal (which is appended to <body>) with transitions?

这个答案 中有一个模态,它通过将它附加到 <body> 创建一个基于 React 的模态。但是,我发现它与 React 提供的过渡插件不兼容。

如何创建一个带有转换的(进入和离开期间)?

在 2015 年的 React 大会上,Ryan Florence demonstrated using portals。以下是创建简单 Portal 组件的方法...

var Portal = React.createClass({
  render: () => null,
  portalElement: null,
  componentDidMount() {
    var p = this.props.portalId && document.getElementById(this.props.portalId);
    if (!p) {
      var p = document.createElement('div');
      p.id = this.props.portalId;
      document.body.appendChild(p);
    }
    this.portalElement = p;
    this.componentDidUpdate();
  },
  componentWillUnmount() {
    document.body.removeChild(this.portalElement);
  },
  componentDidUpdate() {
    React.render(<div {...this.props}>{this.props.children}</div>, this.portalElement);
  }
});

然后您通常可以在 React 中执行的所有操作都可以在门户内部执行...

    <Portal className="DialogGroup">
       <ReactCSSTransitionGroup transitionName="Dialog-anim">
         { activeDialog === 1 && 
            <div key="0" className="Dialog">
              This is an animated dialog
            </div> }
       </ReactCSSTransitionGroup>
    </Portal> 

jsbin demo

你也可以看看Ryan的react-modal,虽然我没有实际使用过所以我不知道它在动画方面的效果如何。

希望对您有所帮助。这是我当前基于上面的 anwser 实现的过渡模式:

  React = require 'react/addons'

  keyboard = require '../util/keyboard'
  mixinLayered = require '../mixin/layered'

  $ = React.DOM
  T = React.PropTypes
  cx = React.addons.classSet

  module.exports = React.createFactory React.createClass
    displayName: 'body-modal'
    mixins: [mixinLayered]

    propTypes:
      # this components accepts children
      name:             T.string.isRequired
      title:            T.string
      onCloseClick:     T.func.isRequired
      showCornerClose:  T.bool
      show:             T.bool.isRequired

    componentDidMount: ->
      window.addEventListener 'keydown', @onWindowKeydown

    componentWillUnmount: ->
      window.removeEventListener 'keydown', @onWindowKeydown

    onWindowKeydown: (event) ->
      if event.keyCode is keyboard.esc
        @onCloseClick()

    onCloseClick: ->
      @props.onCloseClick()

    onBackdropClick: (event) ->
      unless @props.showCornerClose
        if event.target is event.currentTarget
          @onCloseClick()

    renderLayer: ->
      className = "body-modal is-for-#{@props.name}"
      $.div className: className, onClick: @onBackdropClick,
        if @props.showCornerClose
          $.a className: 'icon icon-remove', onClick: @onCloseClick
        $.div className: 'box',
          if @props.title?
            $.div className: 'title',
              $.span className: 'name', @props.title
              $.span className: 'icon icon-remove', @onCloseClick
          @props.children

    render: ->
      $.div()

我写了模块 react-portal 应该可以帮助你。

用法:

import { Portal } from 'react-portal';
 
<Portal>
  This text is portaled at the end of document.body!
</Portal>
 
<Portal node={document && document.getElementById('san-francisco')}>
  This text is portaled into San Francisco!
</Portal>

我已经编写了一个库来帮助解决这个问题。我避免了 Portal 策略使用的 DOM 插入技巧,而是利用基于上下文的注册表将组件从源传递到目标。

我的实现使用了标准的 React 渲染周期。您 teleport/inject/transport 的组件不会在目标上导致双重渲染周期 - 一切都是同步发生的。

API 的结构也不鼓励在代码中使用魔术字符串来定义 source/target。相反,您需要显式创建和装饰将用作目标(可注入)和源(注入器)的组件。由于这种事情通常被认为是非常神奇的,我认为显式组件表示(需要直接导入和使用)可能有助于减轻对组件注入位置的混淆。

尽管我的库不允许您作为 document.body 的直接子项进行渲染,但您可以通过绑定到组件树中的根级组件来实现可接受的模态效果。我计划很快添加这个用例的示例。

有关详细信息,请参阅 https://github.com/ctrlplusb/react-injectables

这里的根本问题是,在 React 中,您只能将组件挂载到其父级,这并不总是期望的行为。但是如何解决这个问题呢?

我已制定解决方案,旨在解决此问题。可以在此处找到更详细的问题定义、src 和示例:https://github.com/fckt/react-layer-stack#rationale

Rationale

react/react-dom comes comes with 2 basic assumptions/ideas:

  • every UI is hierarchical naturally. This why we have the idea of components which wrap each other
  • react-dom mounts (physically) child component to its parent DOM node by default

The problem is that sometimes the second property isn't what you want in your case. Sometimes you want to mount your component into different physical DOM node and hold logical connection between parent and child at the same time.

Canonical example is Tooltip-like component: at some point of development process you could find that you need to add some description for your UI element: it'll render in fixed layer and should know its coordinates (which are that UI element coord or mouse coords) and at the same time it needs information whether it needs to be shown right now or not, its content and some context from parent components. This example shows that sometimes logical hierarchy isn't match with the physical DOM hierarchy.

查看 https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example 以查看回答您的问题的具体示例:

import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
  const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
  return (
    <Cell {...props}>
        // the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
        <Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
            hideMe, // alias for `hide(modalId)`
            index } // useful to know to set zIndex, for example
            , e) => // access to the arguments (click event data in this example)
          <Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
            <ConfirmationDialog
              title={ 'Delete' }
              message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
              confirmButton={ <Button type="primary">DELETE</Button> }
              onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
              close={ hideMe } />
          </Modal> }
        </Layer>

        // this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
        <LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
          <div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
            <Icon type="trash" />
          </div> }
        </LayerContext>
    </Cell>)
// ...

反应 15.x

这是 this article 中描述的方法的 ES6 版本:

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

export default class BodyEnd extends React.PureComponent {
    
    static propTypes = {
        children: PropTypes.node,
    };
    
    componentDidMount() {
        this._popup = document.createElement('div');
        document.body.appendChild(this._popup);
        this._render();
    }

    componentDidUpdate() {
        this._render();
    }

    componentWillUnmount() {
        ReactDOM.unmountComponentAtNode(this._popup);
        document.body.removeChild(this._popup);
    }

    _render() {
        ReactDOM.render(this.props.children, this._popup);
    }
    
    render() {
        return null;
    }
}

只需将您想要放在 DOM 末尾的任何元素用它包裹起来即可:

<BodyEnd><Tooltip pos={{x,y}}>{content}</Tooltip></BodyEnd>

反应 16.x

这是 React 16 的更新版本:

import React from 'react';
import ReactDOM from 'react-dom';

export default class BodyEnd extends React.Component {

    constructor(props) {
        super(props);
        this.el = document.createElement('div');
        this.el.style.display = 'contents';
        // The <div> is a necessary container for our
        // content, but it should not affect our layout.
        // Only works in some browsers, but generally
        // doesn't matter since this is at
        // the end anyway. Feel free to delete this line.
    }
    
    componentDidMount() {
        document.body.appendChild(this.el);
    }

    componentWillUnmount() {
        document.body.removeChild(this.el);
    }
    
    render() {
        return ReactDOM.createPortal(
            this.props.children,
            this.el,
        );
    }
}

Working example

正如其他答案所说,这可以使用门户来完成。从 v16.0 Portals 开始包含在 React 中。

<body>
  <div id="root"></div>
  <div id="portal"></div>
</body>

通常,当您 return 来自组件渲染方法的元素时,它会作为最近父节点的子节点安装到 DOM 中,但是对于门户网站,您可以将子节点插入到DOM 中的不同位置。

const PortalComponent = ({ children, onClose }) => {
  return createPortal(
    <div className="modal" style={modalStyle} onClick={onClose}>
      {children}
    </div>,
    // get outer DOM element
    document.getElementById("portal")
  );
};

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      modalOpen: false
    };
  }

  render() {
    return (
      <div style={styles}>
        <Hello name="CodeSandbox" />
        <h2>Start editing to see some magic happen {"\u2728"}</h2>
        <button onClick={() => this.setState({ modalOpen: true })}>
          Open modal
        </button>
        {this.state.modalOpen && (
          <PortalComponent onClose={() => this.setState({ modalOpen: false })}>
            <h1>This is modal content</h1>
          </PortalComponent>
        )}
      </div>
    );
  }
}

render(<App />, document.getElementById("root"));

检查工作示例here

我认为这段代码或多或少是不言自明的,涵盖了大多数人正在寻找的核心解决方案:

ReactDOM.render(
  <Modal />,
  document.body.appendChild( document.createElement( 'div' ) ),
)