web-dev-qa-db-fra.com

Comment créer un React Modal (qui est ajouté à `<body>`) avec des transitions?

Il y a un modal dans cette réponse https://stackoverflow.com/a/26789089/883571 qui crée un modal basé sur React en l'ajoutant à <body>. Cependant, j’ai trouvé qu’il n’était pas compatible avec les addons de transition fournis par React.

Comment en créer une avec des transitions (pendant les entrées et les sorties)?

47
jiyinyiyong

À la réaction de 2015, Ryan Florence démontré en utilisant des portails . Voici comment créer un composant Portal simple ...

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);
  }
});

et puis tout ce que vous pouvez normalement faire dans React vous pouvez faire à l'intérieur du portail ...

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

jsbin demo

Vous pouvez aussi jeter un oeil à react-modal de Ryan, bien que je ne l'ait pas encore utilisé, je ne sais pas si cela fonctionne bien avec l'animation.

61
Gil Birman

J'ai écrit le module react-portal qui devrait vous aider.

30
tajo

Réagir 15.x

Voici une version ES6 de la méthode décrite dans cet article :

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;
    }
}

Entourez simplement les éléments que vous voulez être à la fin du DOM avec:

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

Réagir 16.x

Voici une version mise à jour pour 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,
        );
    }
}

Exemple de travail

21
mpen

Comme d'autres réponses l'ont indiqué, cela peut être fait en utilisant des portails. A partir de v16.0Portails sont inclus dans React.

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

Normalement, lorsque vous retournez un élément à partir de la méthode de rendu d'un composant, il est monté dans le DOM en tant qu'enfant du nœud parent le plus proche. Toutefois, avec les portails, vous pouvez insérer un enfant dans un emplacement différent du 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"));

Vérifiez l'exemple de travail ici .

9
Kristaps Taube

Le problème fondamental ici est que dans React vous êtes uniquement autorisé à monter un composant sur son parent, ce qui n’est pas toujours le comportement souhaité. Mais comment résoudre ce problème?

J'ai fait la solution, adressée pour résoudre ce problème. Vous pouvez trouver une définition plus détaillée du problème, un code src et des exemples ici: https://github.com/fckt/react-layer-stack#rationale

Raisonnement

react/react-dom vient avec 2 hypothèses/idées de base:

  • chaque interface utilisateur est naturellement hiérarchique. C’est pourquoi nous avons l’idée de components qui s’enveloppent
  • react-dom monte le composant enfant (physiquement) sur son noeud DOM parent par défaut

Le problème est que parfois la deuxième propriété n'est pas ce que vous voulez dans votre cas. Parfois, vous souhaitez monter votre composant sur un nœud DOM physique différent et établir une connexion logique entre le parent et l'enfant en même temps.

Un exemple canonique est un composant de type Tooltip: à un moment donné du processus de développement, vous devrez peut-être ajouter une description à votre UI element: il rendra en couche fixe et devrait connaître ses coordonnées (qui sont que UI element coord ou mouse coords) et en même temps, il a besoin d'informations pour savoir si elles doivent être affichées maintenant ou non, son contenu et un certain contexte à partir des composants parents. Cet exemple montre que parfois la hiérarchie logique ne correspond pas à la hiérarchie physique du DOM.

Jetez un coup d'œil à https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example pour voir l'exemple concret qui est answer à votre question:

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>)
// ...
3
fckt

J'ai écrit une bibliothèque pour aider avec ceci. J'évite les piratages d'insertion DOM utilisés par les stratégies de portail, mais utilise plutôt des registres basés sur le contexte pour transmettre des composants d'une source à une cible.

Mon implémentation utilise les cycles de rendu standard React. Les composants que vous téléportez/injectez/transportez ne provoquent pas un double cycle de rendu sur la cible - tout se passe de manière synchrone.

L'API est également structurée de manière à décourager l'utilisation de chaînes magiques dans votre code pour définir la source/cible. Au lieu de cela, vous devez explicitement créer et décorer les composants qui seront utilisés comme cible (Injectable) et source (Injector). Comme ce genre de chose est généralement considéré comme assez magique, je pense qu'une représentation explicite des Composants (nécessitant des importations et des utilisations directes) peut aider à dissiper toute confusion quant à l'endroit où un Composant est injecté.

Bien que ma bibliothèque ne vous permette pas d'effectuer le rendu en tant qu'enfant direct de document.body, vous pouvez obtenir un effet modal acceptable en le liant à un composant de niveau racine dans votre arborescence. Je prévois d'ajouter bientôt un exemple de ce cas d'utilisation.

Voir https://github.com/ctrlplusb/react-injectables pour plus d'informations.

2
ctrlplusb

Je pense que ce code est plus ou moins explicite et couvre la solution de base de ce que la plupart des gens recherchent:

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

J'espère que ça aide. Ceci est ma mise en œuvre actuelle d’un modal de transition basé sur la réponse ci-dessus:

  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()
0
jiyinyiyong