Как создать модификатор React Modal (который добавляется к `<body>`) с переходами?
В этом ответе есть модальный qaru.site/info/152308/..., который создает модификацию на основе React, добавляя ее к <body>
. Однако я обнаружил, что он несовместим с переходными аддонами, предоставляемыми React.
Как создать один с переходами (во время входа и выхода)?
Ответы
Ответ 1
В реале conf 2015, Ryan Florence продемонстрировал использование порталов. Здесь вы можете создать простой компонент 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>
Вы также можете взглянуть на Ryan react-modal, хотя я на самом деле не использовал его, поэтому я не знаю, насколько хорошо он работает с анимацией.
Ответ 2
Я написал модуль react-portal, который должен вам помочь.
Ответ 3
Теперь у React 16 есть portals встроенный. Вероятно, вы должны использовать .
Здесь версия 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>
Ответ 4
Основная проблема здесь заключается в том, что в React вам разрешено монтировать компонент только его родительскому элементу, что не всегда является желаемым поведением. Но как решить эту проблему?
Я сделал решение, адресованное для исправления этой проблемы. Более подробное определение проблемы, src и примеры можно найти здесь: https://github.com/fckt/react-layer-stack#rationale
Обоснование
react
/react-dom
поставляется с двумя основными предположениями/идеями:
- каждый пользовательский интерфейс является естественным. Вот почему мы имеем идею
components
, которые обертывают друг друга -
react-dom
по умолчанию монтирует (физически) дочерний компонент в родительский DOM node
Проблема в том, что иногда второе свойство не то, что вы хотите в твоем случае. Иногда вы хотите подключить свой компонент в различные физические DOM node и провести логическое соединение между родитель и ребенок одновременно.
Канонический пример - это компонент, подобный подсказке: в какой-то момент процесс разработки, вы можете обнаружить, что вам нужно добавить некоторые описание для вашего UI element
: оно будет отображаться на фиксированном уровне и должен знать свои координаты (которые заключаются в том, что UI element
мышь), и в то же время ему нужна информация о том, должен быть показан прямо сейчас или нет, его содержание и некоторый контекст из родительских компонентов. Этот пример показывает, что иногда логическая иерархия не соответствует физической иерархии DOM.
Взгляните на 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>)
// ...
Ответ 5
Я написал библиотеку, чтобы помочь с этим. Я избегаю хакеров ввода DOM, используемых стратегиями портала, и вместо этого использую реестры на основе контекста для передачи компонентов от источника к цели.
В моей реализации используются стандартные циклы рендеринга React. Компоненты, которые вы телепортируете/вставляете/переносите, не вызывают двойного цикла рендеринга на цели - все происходит синхронно.
API также структурирован таким образом, чтобы препятствовать использованию магических строк в вашем коде для определения источника/цели. Вместо этого вам необходимо явно создавать и украшать компоненты, которые будут использоваться в качестве цели (Injectable) и источника (Injector). Поскольку такого рода вещи обычно считаются довольно волшебными, я думаю, что явное представление Компонента (требующее прямого импорта и использования) может помочь облегчить путаницу в том, где вводится Компонент.
Хотя моя библиотека не позволит вам отображать как прямой дочерний объект document.body, вы можете добиться приемлемого модального эффекта, привязавшись к компоненту корневого уровня в своем дереве компонентов. В ближайшее время я планирую добавить пример этого варианта использования.
Подробнее см. https://github.com/ctrlplusb/react-injectables.
Ответ 6
Как утверждают другие ответы, это можно сделать с помощью порталов. Начиная с v16.0
Portals включены в React.
<body>
<div id="root"></div>
<div id="portal"></div>
</body>
Обычно, когда вы возвращаете элемент из метода рендеринга компонента, он монтируется в DOM как дочерний элемент ближайшего родителя node, но с порталами вы можете вставить ребенка в другое место в 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"));
Проверьте рабочий пример здесь.
Ответ 7
Надеюсь, это поможет. Это моя текущая реализация модальности перехода на основе вышеперечисленного:
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()