react modal close javascript modal-dialog redux react-redux

javascript - close modal react js



¿Cómo puedo mostrar un diálogo modal en Redux que realiza acciones asincrónicas? (5)

Estoy creando una aplicación que necesita mostrar un diálogo de confirmación en algunas situaciones.

Digamos que quiero eliminar algo, luego enviaré una acción como deleteSomething(id) para que algún reductor capture ese evento y llene el reductor de diálogo para mostrarlo.

Mi duda surge cuando se presenta este diálogo.

  • ¿Cómo puede este componente despachar la acción adecuada de acuerdo con la primera acción despachada?
  • ¿Debería el creador de la acción manejar esta lógica?
  • ¿Podemos agregar acciones dentro del reductor?

editar:

para que quede más claro:

deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id) createThingB(id) => Show dialog with Questions => createThingBRemotely(id)

Entonces estoy tratando de reutilizar el componente de diálogo. Mostrar / ocultar el diálogo no es el problema, ya que esto se puede hacer fácilmente en el reductor. Lo que estoy tratando de especificar es cómo despachar la acción desde el lado derecho de acuerdo con la acción que inicia el flujo en el lado izquierdo.


Aquí se pueden encontrar muchas buenas soluciones y comentarios valiosos de expertos conocidos de la comunidad de JS sobre el tema. Podría ser un indicador de que no es un problema tan trivial como parece. Creo que es por eso que podría ser la fuente de dudas e incertidumbre sobre el tema.

El problema fundamental aquí es que en React solo se le permite montar el componente en su padre, que no siempre es el comportamiento deseado. Pero, ¿cómo abordar este problema?

Propongo la solución, dirigida a solucionar este problema. Una definición más detallada del problema, src y ejemplos se pueden encontrar aquí: https://github.com/fckt/react-layer-stack#rationale

Razón fundamental

react / react-dom viene viene con 2 supuestos básicos / ideas:

  • cada interfaz de usuario es jerárquica de forma natural. Por eso tenemos la idea de components que se envuelven
  • react-dom monta el componente hijo (físicamente) en su nodo DOM padre de forma predeterminada

El problema es que a veces la segunda propiedad no es lo que desea en su caso. A veces, desea montar su componente en un nodo DOM físico diferente y mantener una conexión lógica entre padre e hijo al mismo tiempo.

El ejemplo canónico es un componente similar a la información sobre herramientas: en algún punto del proceso de desarrollo, podría encontrar que necesita agregar alguna descripción para su UI element : se representará en una capa fija y debe conocer sus coordenadas (que son ese UI element coord o mouse coords) y, al mismo tiempo, necesita información sobre si debe mostrarse en este momento o no, su contenido y algún contexto de los componentes principales. Este ejemplo muestra que a veces la jerarquía lógica no coincide con la jerarquía física del DOM.

Eche un vistazo a https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example para ver el ejemplo concreto que es la respuesta a su pregunta:

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


El enfoque que sugiero es un poco detallado, pero encontré que escala bastante bien en aplicaciones complejas. Cuando desee mostrar un modal, active una acción que describa qué modal le gustaría ver:

Envío de una acción para mostrar el modal

this.props.dispatch({ type: ''SHOW_MODAL'', modalType: ''DELETE_POST'', modalProps: { postId: 42 } })

(Las cadenas pueden ser constantes, por supuesto; estoy usando cadenas en línea para simplificar).

Escribir un reductor para administrar el estado modal

Luego, asegúrese de tener un reductor que solo acepte estos valores:

const initialState = { modalType: null, modalProps: {} } function modal(state = initialState, action) { switch (action.type) { case ''SHOW_MODAL'': return { modalType: action.modalType, modalProps: action.modalProps } case ''HIDE_MODAL'': return initialState default: return state } } /* .... */ const rootReducer = combineReducers({ modal, /* other reducers */ })

¡Excelente! Ahora, cuando despacha una acción, state.modal se actualizará para incluir la información sobre la ventana modal actualmente visible.

Escribir el componente modal raíz

En la raíz de su jerarquía de componentes, agregue un componente <ModalRoot> que esté conectado a la tienda Redux. Escuchará state.modal y mostrará un componente modal apropiado, reenviando los accesorios desde state.modal.modalProps .

// These are regular React components we will write soon import DeletePostModal from ''./DeletePostModal'' import ConfirmLogoutModal from ''./ConfirmLogoutModal'' const MODAL_COMPONENTS = { ''DELETE_POST'': DeletePostModal, ''CONFIRM_LOGOUT'': ConfirmLogoutModal, /* other modals */ } const ModalRoot = ({ modalType, modalProps }) => { if (!modalType) { return <span /> // after React v15 you can return null here } const SpecificModal = MODAL_COMPONENTS[modalType] return <SpecificModal {...modalProps} /> } export default connect( state => state.modal )(ModalRoot)

Que hemos hecho aqui ModalRoot lee el modalType y modalProps de state.modal al que está conectado, y representa un componente correspondiente como DeletePostModal o ConfirmLogoutModal . ¡Cada modal es un componente!

Escribir componentes modales específicos

No hay reglas generales aquí. Son solo componentes React que pueden enviar acciones, leer algo del estado de la tienda y ser modales .

Por ejemplo, DeletePostModal podría verse así:

import { deletePost, hideModal } from ''../actions'' const DeletePostModal = ({ post, dispatch }) => ( <div> <p>Delete post {post.name}?</p> <button onClick={() => { dispatch(deletePost(post.id)).then(() => { dispatch(hideModal()) }) }}> Yes </button> <button onClick={() => dispatch(hideModal())}> Nope </button> </div> ) export default connect( (state, ownProps) => ({ post: state.postsById[ownProps.postId] }) )(DeletePostModal)

DeletePostModal está conectado a la tienda para que pueda mostrar el título de la publicación y funciona como cualquier componente conectado: puede enviar acciones, incluido hideModal cuando es necesario ocultarse.

Extraer un componente de presentación

Sería incómodo copiar y pegar la misma lógica de diseño para cada modal "específico". Pero tienes componentes, ¿verdad? Por lo tanto, puede extraer un componente de presentational <Modal> que no sabe qué hacen los modales particulares, pero maneja cómo se ven.

Entonces, modales específicos como DeletePostModal pueden usarlo para renderizar:

import { deletePost, hideModal } from ''../actions'' import Modal from ''./Modal'' const DeletePostModal = ({ post, dispatch }) => ( <Modal dangerText={`Delete post ${post.name}?`} onDangerClick={() => dispatch(deletePost(post.id)).then(() => { dispatch(hideModal()) }) }) /> ) export default connect( (state, ownProps) => ({ post: state.postsById[ownProps.postId] }) )(DeletePostModal)

Depende de usted proponer un conjunto de accesorios que <Modal> puede aceptar en su aplicación, pero me imagino que puede tener varios tipos de modales (por ejemplo, información modal, modal de confirmación, etc.) y varios estilos para ellos .

Accesibilidad y ocultación al hacer clic fuera o tecla de escape

La última parte importante sobre los modales es que generalmente queremos ocultarlos cuando el usuario hace clic afuera o presiona Escape.

En lugar de darle consejos sobre cómo implementar esto, le sugiero que simplemente no lo implemente usted mismo. Es difícil acertar teniendo en cuenta la accesibilidad.

En su lugar, le sugiero que utilice un componente modal accesible estándar como react-modal . Es completamente personalizable, puede poner lo que quiera dentro de él, pero maneja la accesibilidad correctamente para que las personas ciegas puedan seguir usando su modal.

Incluso puede ajustar react-modal en su propio <Modal> que acepta accesorios específicos para sus aplicaciones y genera botones secundarios u otro contenido. ¡Todo son solo componentes!

Otros enfoques

Hay más de una forma de hacerlo.

A algunas personas no les gusta la verbosidad de este enfoque y prefieren tener un componente <Modal> que puedan representar directamente dentro de sus componentes con una técnica llamada "portales". Los portales le permiten representar un componente dentro del suyo, mientras que en realidad se representará en un lugar predeterminado en el DOM, lo cual es muy conveniente para los modales.

De hecho, el react-modal me vinculé anteriormente ya lo hace internamente, por lo que técnicamente ni siquiera necesita renderizarlo desde arriba. Todavía me parece agradable desacoplar el modal que quiero mostrar del componente que lo muestra, pero también puede usar react-modal directamente desde sus componentes y omitir la mayoría de lo que escribí anteriormente.

Te animo a que consideres ambos enfoques, experimentes con ellos y elijas lo que creas que funciona mejor para tu aplicación y para tu equipo.


En mi opinión, la implementación mínima mínima tiene dos requisitos. Un estado que realiza un seguimiento de si el modal está abierto o no, y un portal para representar el modal fuera del árbol de reacción estándar.

El componente ModalContainer a continuación implementa esos requisitos junto con las funciones de representación correspondientes para el modal y el desencadenante, que es responsable de ejecutar la devolución de llamada para abrir el modal.

import React from ''react''; import PropTypes from ''prop-types''; import Portal from ''react-portal''; class ModalContainer extends React.Component { state = { isOpen: false, }; openModal = () => { this.setState(() => ({ isOpen: true })); } closeModal = () => { this.setState(() => ({ isOpen: false })); } renderModal() { return ( this.props.renderModal({ isOpen: this.state.isOpen, closeModal: this.closeModal, }) ); } renderTrigger() { return ( this.props.renderTrigger({ openModal: this.openModal }) ) } render() { return ( <React.Fragment> <Portal> {this.renderModal()} </Portal> {this.renderTrigger()} </React.Fragment> ); } } ModalContainer.propTypes = { renderModal: PropTypes.func.isRequired, renderTrigger: PropTypes.func.isRequired, }; export default ModalContainer;

Y aquí hay un caso de uso simple ...

import React from ''react''; import Modal from ''react-modal''; import Fade from ''components/Animations/Fade''; import ModalContainer from ''components/ModalContainer''; const SimpleModal = ({ isOpen, closeModal }) => ( <Fade visible={isOpen}> // example use case with animation components <Modal> <Button onClick={closeModal}> close modal </Button> </Modal> </Fade> ); const SimpleModalButton = ({ openModal }) => ( <button onClick={openModal}> open modal </button> ); const SimpleButtonWithModal = () => ( <ModalContainer renderModal={props => <SimpleModal {...props} />} renderTrigger={props => <SimpleModalButton {...props} />} /> ); export default SimpleButtonWithModal;

Utilizo funciones de renderizado, porque quiero aislar la administración de estado y la lógica repetitiva de la implementación del componente modal y desencadenante. Esto permite que los componentes renderizados sean lo que usted quiera que sean. En su caso, supongo que el componente modal podría ser un componente conectado que recibe una función de devolución de llamada que despacha una acción asincrónica.

Si necesita enviar accesorios dinámicos al componente modal desde el componente de disparo, lo que con suerte no ocurre con demasiada frecuencia, le recomiendo envolver el Contenedor Modal con un componente contenedor que gestione los accesorios dinámicos en su propio estado y mejore los métodos de renderización originales como asi que.

import React from ''react'' import partialRight from ''lodash/partialRight''; import ModalContainer from ''components/ModalContainer''; class ErrorModalContainer extends React.Component { state = { message: '''' } onError = (message, callback) => { this.setState( () => ({ message }), () => callback && callback() ); } renderModal = (props) => ( this.props.renderModal({ ...props, message: this.state.message, }) ) renderTrigger = (props) => ( this.props.renderTrigger({ openModal: partialRight(this.onError, props.openModal) }) ) render() { return ( <ModalContainer renderModal={this.renderModal} renderTrigger={this.renderTrigger} /> ) } } ErrorModalContainer.propTypes = ( ModalContainer.propTypes ); export default ErrorModalContainer;


Envuelva el modal en un contenedor conectado y realice la operación asíncrona aquí. De esta manera, puede llegar tanto al despacho para activar acciones como al accesorio onClose también. Para llegar al dispatch desde los accesorios, no pase la función mapDispatchToProps para connect .

class ModalContainer extends React.Component { handleDelete = () => { const { dispatch, onClose } = this.props; dispatch({type: ''DELETE_POST''}); someAsyncOperation().then(() => { dispatch({type: ''DELETE_POST_SUCCESS''}); onClose(); }) } render() { const { onClose } = this.props; return <Modal onClose={onClose} onSubmit={this.handleDelete} /> } } export default connect(/* no map dispatch to props here! */)(ModalContainer);

La aplicación donde se representa el modal y se establece su estado de visibilidad:

class App extends React.Component { state = { isModalOpen: false } handleModalClose = () => this.setState({ isModalOpen: false }); ... render(){ return ( ... <ModalContainer onClose={this.handleModalClose} /> ... ) } }


Actualización : React 16.0 introdujo portales a través del link ReactDOM.createPortal

Actualización : las próximas versiones de React (Fiber: probablemente 16 o 17) incluirán un método para crear portales: link ReactDOM.unstable_createPortal()

Usa portales

La primera parte de la respuesta de Dan Abramov está bien, pero involucra muchas repeticiones. Como él dijo, también puedes usar portales. Ampliaré un poco esa idea.

La ventaja de un portal es que la ventana emergente y el botón permanecen muy cerca del árbol React, con una comunicación padre / hijo muy simple usando accesorios: puede manejar fácilmente acciones asíncronas con portales, o dejar que el padre personalice el portal.

¿Qué es un portal?

Un portal le permite representar directamente dentro de document.body un elemento que está profundamente anidado en su árbol React.

La idea es que, por ejemplo, conviertas en cuerpo el siguiente árbol React:

<div className="layout"> <div className="outside-portal"> <Portal> <div className="inside-portal"> PortalContent </div> </Portal> </div> </div>

Y obtienes como salida:

<body> <div class="layout"> <div class="outside-portal"> </div> </div> <div class="inside-portal"> PortalContent </div> </body>

El nodo del inside-portal se ha traducido dentro de <body> , en lugar de su lugar normal, profundamente anidado.

Cuando usar un portal

Un portal es particularmente útil para mostrar elementos que deben ir por encima de sus componentes React existentes: ventanas emergentes, menús desplegables, sugerencias, puntos de acceso

¿Por qué usar un portal?

Ya no hay problemas con el índice z : un portal le permite renderizar a <body> . Si desea mostrar una ventana emergente o desplegable, esta es una muy buena idea si no quiere tener que luchar contra los problemas del índice z. Los elementos del portal se agregan do document.body en orden de montaje, lo que significa que a menos que juegue con z-index , el comportamiento predeterminado será apilar los portales uno encima del otro, en orden de montaje. En la práctica, significa que puede abrir una ventana emergente de forma segura desde dentro de otra ventana emergente, y asegúrese de que la segunda ventana emergente se mostrará encima de la primera, sin tener que pensar siquiera en z-index .

En la práctica

Lo más simple: use el estado Reaccionar local: si cree que, para una simple ventana emergente de confirmación de eliminación, no vale la pena tener el repetitivo de Redux, entonces puede usar un portal y simplifica enormemente su código. Para tal caso de uso, donde la interacción es muy local y en realidad es un detalle de implementación, ¿realmente le importa la recarga en caliente, el viaje en el tiempo, el registro de acciones y todos los beneficios que Redux le brinda? Personalmente, no lo hago y uso el estado local en este caso. El código se vuelve tan simple como:

class DeleteButton extends React.Component { static propTypes = { onDelete: PropTypes.func.isRequired, }; state = { confirmationPopup: false }; open = () => { this.setState({ confirmationPopup: true }); }; close = () => { this.setState({ confirmationPopup: false }); }; render() { return ( <div className="delete-button"> <div onClick={() => this.open()}>Delete</div> {this.state.confirmationPopup && ( <Portal> <DeleteConfirmationPopup onCancel={() => this.close()} onConfirm={() => { this.close(); this.props.onDelete(); }} /> </Portal> )} </div> ); } }

Simple: aún puede usar el estado de Redux : si realmente lo desea, puede usar connect para elegir si se muestra o no DeleteConfirmationPopup . Como el portal permanece profundamente anidado en su árbol React, es muy sencillo personalizar el comportamiento de este portal porque su padre puede pasar accesorios al portal. Si no usa portales, generalmente tiene que mostrar sus ventanas emergentes en la parte superior de su árbol React por razones de z-index , y generalmente tiene que pensar en cosas como "¿cómo personalizo el DeleteConfirmationPopup genérico que construí de acuerdo con el uso? caso". Y, por lo general, encontrará soluciones bastante hacky para este problema, como el envío de una acción que contiene acciones de confirmación / cancelación anidadas, una clave de paquete de traducción o, lo que es peor, una función de representación (o algo más no serializable). No tiene que hacer eso con los portales, y solo puede pasar accesorios regulares, ya que DeleteConfirmationPopup es solo un hijo del DeleteButton

Conclusión

Los portales son muy útiles para simplificar su código. No podría prescindir de ellos nunca más.

Tenga en cuenta que las implementaciones de portal también pueden ayudarlo con otras características útiles como:

  • Accesibilidad
  • Atajos de espacio para cerrar el portal
  • Manejar el clic externo (cerrar el portal o no)
  • Manejar el clic del enlace (cerrar portal o no)
  • Contexto de reacción disponible en el árbol del portal

react-portal o react-modal son buenos para ventanas emergentes, modales y superposiciones que deberían estar en pantalla completa, generalmente centradas en el medio de la pantalla.

react-tether es desconocido para la mayoría de los desarrolladores de React, sin embargo, es una de las herramientas más útiles que puedes encontrar. Tether permite crear portales, pero posicionará automáticamente el portal, en relación con un objetivo determinado. Esto es perfecto para información sobre herramientas, menús desplegables, zonas interactivas, cajas de ayuda ... Si alguna vez ha tenido algún problema con la posición absolute / relative y z-index , o si su menú desplegable sale de su ventana gráfica, Tether resolverá todo eso por usted.

Puede, por ejemplo, implementar fácilmente zonas activas de incorporación, que se expanden a una información sobre herramientas una vez que se hace clic en ellas:

Código de producción real aquí. No puede ser más simple :)

<MenuHotspots.contacts> <ContactButton/> </MenuHotspots.contacts>

Editar : acaba de descubrir react-gateway que permite representar portales en el nodo de su elección (no necesariamente el cuerpo)

Editar : parece que react-popper puede ser una alternativa decente a react-tether. PopperJS es una biblioteca que solo calcula una posición apropiada para un elemento, sin tocar el DOM directamente, lo que permite al usuario elegir dónde y cuándo quiere colocar el nodo DOM, mientras que Tether se agrega directamente al cuerpo.

Editar : también hay react-slot-fill que es interesante y puede ayudar a resolver problemas similares al permitir representar un elemento en un espacio de elemento reservado que coloque en cualquier lugar que desee en su árbol