Ich baue eine App, die in einigen Situationen ein Bestätigungsdialogfeld anzeigen muss.
Nehmen wir an, ich möchte etwas entfernen, dann schicke ich eine Aktion wie deleteSomething(id)
ab, damit ein Reducer das Ereignis fängt und den Dialog-Reducer füllt, um es anzuzeigen.
Ich bezweifle, dass sich dieser Dialog ergibt.
bearbeiten:
um es klarer zu machen:
deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id)
createThingB(id) => Show dialog with Questions => createThingBRemotely(id)
Ich versuche also, die Dialogkomponente wiederzuverwenden. Das Anzeigen/Ausblenden des Dialogs ist nicht das Problem, da dies im Reducer leicht gemacht werden kann. Was ich versuche zu spezifizieren, ist, wie die Aktion von der rechten Seite abgesetzt wird, entsprechend der Aktion, die den Fluss auf der linken Seite startet.
Der Ansatz, den ich vorschlage, ist ein wenig ausführlich, aber ich fand es gut in komplexe Apps zu skalieren. Wenn Sie einen Modal anzeigen möchten, lösen Sie eine Aktion aus, in der welche-Modal beschrieben wird:
this.props.dispatch({
type: 'SHOW_MODAL',
modalType: 'DELETE_POST',
modalProps: {
postId: 42
}
})
(Strings können natürlich Konstanten sein; ich verwende zur Vereinfachung Inline-Strings.)
Stellen Sie dann sicher, dass Sie einen Reduzierer haben, der diese Werte akzeptiert:
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 */
})
Großartig! Wenn Sie nun eine Aktion auslösen, wird state.modal
aktualisiert, um die Informationen zum aktuell sichtbaren modalen Fenster einzuschließen.
Fügen Sie im Stammverzeichnis Ihrer Komponentenhierarchie eine <ModalRoot>
-Komponente hinzu, die mit dem Redux-Store verbunden ist. Es hört auf state.modal
und zeigt eine entsprechende modale Komponente an, wobei die Requisiten vom state.modal.modalProps
weitergeleitet werden.
// 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)
Was haben wir hier gemacht? ModalRoot
liest die aktuelle modalType
und modalProps
aus state.modal
, an die sie angeschlossen ist, und rendert eine entsprechende Komponente wie DeletePostModal
oder ConfirmLogoutModal
. Jeder Modal ist eine Komponente!
Hier gibt es keine allgemeinen Regeln. Sie sind nur React-Komponenten, die Aktionen auslösen können, etwas aus dem Speicherstatus lesen und sind und zufällig Modals sind.
Zum Beispiel könnte DeletePostModal
so aussehen:
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)
Die Variable DeletePostModal
ist mit dem Geschäft verbunden, sodass sie den Posttitel anzeigen kann und wie jede verbundene Komponente funktioniert: Sie kann Aktionen auslösen, einschließlich hideModal
, wenn es notwendig ist, sich selbst auszublenden.
Es wäre umständlich, für jedes "spezifische" Modal die gleiche Layout-Logik zu kopieren und einzufügen. Aber Sie haben Komponenten, richtig? Sie können also eine presentational<Modal>
-Komponente extrahieren, die nicht weiß, was bestimmte Modale tun, aber wie sie aussehen soll.
Dann können bestimmte Modals wie DeletePostModal
es für das Rendern verwenden:
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)
Es liegt an Ihnen, eine Reihe von Requisiten auszuarbeiten, die <Modal>
in Ihrer Anwendung akzeptieren kann, aber ich könnte mir vorstellen, dass Sie verschiedene Modalarten (z. B. Info-Modal, Bestätigungs-Modal usw.) und verschiedene Stile dafür haben.
Der letzte wichtige Aspekt von Modalen ist, dass wir sie im Allgemeinen ausblenden möchten, wenn der Benutzer nach außen klickt oder die Escape-Taste drückt.
Anstatt Ihnen Ratschläge zu geben, schlage ich vor, dass Sie es nicht selbst implementieren. In Anbetracht der Barrierefreiheit ist es schwierig, richtig zu werden.
Stattdessen würde ich Ihnen empfehlen, eine modulare Komponente von zugänglich zu verwenden, z. B. react-modal
. Es ist vollständig anpassbar. Sie können alles, was Sie möchten, darin einfügen, aber es funktioniert korrekt mit der Zugänglichkeit, so dass Blinde Ihren Modal weiterhin verwenden können.
Sie können react-modal
sogar in Ihren eigenen <Modal>
einwickeln, der die für Ihre Anwendungen spezifischen Requisiten akzeptiert und untergeordnete Schaltflächen oder anderen Inhalt generiert. Es sind alles nur Komponenten!
Es gibt mehr als eine Möglichkeit, dies zu tun.
Einige Leute mögen die Ausführlichkeit dieses Ansatzes nicht und bevorzugen es, eine <Modal>
-Komponente zu haben, die sie direkt in ihren Komponenten mit einer als "Portale" bezeichneten Technik darstellen können. Mit Portalen können Sie eine Komponente in Ihrem darstellen, während tatsächlich an einer vorbestimmten Stelle im DOM gerendert wird, was für Modale sehr praktisch ist.
Tatsächlich react-modal
Ich habe schon früher mit einem Link zu tun, tut dies intern. Technisch gesehen müssen Sie es nicht einmal von oben darstellen. Ich finde es immer noch schön, die Modalität, die ich zeigen möchte, von der Komponente zu entkoppeln, die sie anzeigt, aber Sie können auch react-modal
direkt von Ihren Komponenten verwenden und die meisten der oben genannten Informationen überspringen.
Ich möchte Sie dazu ermutigen, beide Ansätze in Betracht zu ziehen, mit ihnen zu experimentieren und auszuwählen, was für Ihre App und Ihr Team am besten geeignet ist.
Update : React 16.0 führte Portale durch ReactDOM.createPortal
Link ein
Update : Die nächsten Versionen von React (Fibre: wahrscheinlich 16 oder 17) enthalten eine Methode zum Erstellen von Portalen: ReactDOM.unstable_createPortal()
link
Dan Abramovs Antwort auf den ersten Teil ist in Ordnung, beinhaltet aber eine Menge Boilerplate. Wie er sagte, können Sie auch Portale benutzen. Ich werde auf diese Idee etwas näher eingehen.
Der Vorteil eines Portals besteht darin, dass das Popup und die Schaltfläche in der React -Baumstruktur sehr nahe beieinander liegen, wobei die Kommunikation zwischen Eltern und Kindern mithilfe von Requisiten sehr einfach ist: Sie können problemlos asynchrone Aktionen mit Portalen ausführen oder die Eltern anpassen das Portal.
Über ein Portal können Sie direkt in document.body
Ein Element rendern, das tief in Ihrem React -Baum verschachtelt ist.
Die Idee ist, dass Sie zum Beispiel den folgenden React -Baum in body rendern:
<div className="layout">
<div className="outside-portal">
<Portal>
<div className="inside-portal">
PortalContent
</div>
</Portal>
</div>
</div>
Und Sie erhalten als Ausgabe:
<body>
<div class="layout">
<div class="outside-portal">
</div>
</div>
<div class="inside-portal">
PortalContent
</div>
</body>
Der Knoten inside-portal
Wurde innerhalb von <body>
Anstelle seines normalen, tief verschachtelten Platzes übersetzt.
Ein Portal ist besonders hilfreich für die Anzeige von Elementen, die über Ihre vorhandenen React Komponenten gestellt werden sollen: Popups, Dropdowns, Vorschläge, Hotspots
Keine Z-Index-Probleme mehr : Ein Portal erlaubt es Ihnen, nach <body>
Zu rendern. Wenn Sie ein Popup oder eine Dropdown-Liste anzeigen möchten, ist dies eine wirklich gute Idee, wenn Sie nicht gegen Z-Index-Probleme kämpfen müssen. Die hinzugefügten Portal-Elemente führen document.body
In der Reihenfolge des Einbindens aus. Wenn Sie also nicht mit z-index
Spielen, werden die Portale standardmäßig in der Reihenfolge des Einbindens übereinander gestapelt. In der Praxis bedeutet dies, dass Sie ein Popup sicher von einem anderen Popup aus öffnen und sicherstellen können, dass das zweite Popup über dem ersten angezeigt wird, ohne dass Sie sich auch nur Gedanken über z-index
Machen müssen.
Am einfachsten: Verwenden Sie local React state: Wenn Sie der Meinung sind, dass es für ein einfaches Bestätigungs-Popup zum Löschen nicht wert ist, den Redux zu haben Boilerplate, dann können Sie ein Portal verwenden und es vereinfacht Ihren Code erheblich. Interessieren Sie sich für einen solchen Anwendungsfall, in dem die Interaktion sehr lokal ist und tatsächlich ein ziemlich detailliertes Implementierungsdetail darstellt, wirklich für Hot-Reloading, Zeitreisen, Aktionsprotokollierung und alle Vorteile, die Ihnen Redux bietet? Persönlich verwende ich in diesem Fall kein lokales Bundesland. Der Code wird so einfach wie:
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>
);
}
}
Einfach: Sie können immer noch den Redux-Status verwenden : Wenn Sie wirklich wollen, können Sie immer noch connect
verwenden, um zu wählen, ob das DeleteConfirmationPopup
wird angezeigt oder nicht. Da das Portal tief in Ihrem React -Stammbaum verschachtelt bleibt, ist es sehr einfach, das Verhalten dieses Portals anzupassen, da Ihre Eltern Requisiten an das Portal übergeben können. Wenn Sie keine Portale verwenden, müssen Sie Ihre Popups normalerweise aus z-index
Gründen am oberen Rand Ihres React Baums rendern und in der Regel über Dinge wie "Wie passe ich sie an?" Nachdenken das generische DeleteConfirmationPopup, das ich je nach Anwendungsfall erstellt habe ". Und normalerweise finden Sie ziemlich hackige Lösungen für dieses Problem, wie das Auslösen einer Aktion, die verschachtelte Bestätigungs-/Abbruchaktionen enthält, eines Übersetzungspaketschlüssels oder, noch schlimmer, einer Renderfunktion (oder etwas anderem, was unserialisierbar ist). Sie müssen das nicht mit Portalen machen und können nur normale Requisiten übergeben, da DeleteConfirmationPopup
nur ein Kind von DeleteButton
ist.
Portale sind sehr nützlich, um Ihren Code zu vereinfachen. Ich konnte nicht mehr ohne sie auskommen.
Beachten Sie, dass Portalimplementierungen Ihnen auch bei anderen nützlichen Funktionen helfen können, z.
reagiere-portal oder reagiere-modal eignen sich für Popups, Modals und Overlays, die im Vollbildmodus angezeigt werden sollen und im Allgemeinen in der Mitte des Bildschirms zentriert sind.
react-tether ist den meisten React Entwicklern unbekannt, aber es ist eines der nützlichsten Tools, die Sie dort finden können. Tether ermöglicht das Erstellen von Portalen, positioniert das Portal jedoch automatisch relativ zu einem bestimmten Ziel. Dies ist perfekt für Tooltips, Dropdowns, Hotspots, Hilfefelder ... Wenn Sie jemals ein Problem mit Position absolute
/relative
und z-index
Oder Ihrem Dropdown außerhalb von hatten In Ihrem Ansichtsfenster löst Tether all das für Sie.
Sie können zum Beispiel einfach Onboarding-Hotspots implementieren, die durch Klicken zu einem Tooltip erweitert werden:
Echter Seriencode hier. Einfacher geht es nicht :)
<MenuHotspots.contacts>
<ContactButton/>
</MenuHotspots.contacts>
Bearbeiten : gerade entdeckt Reagieren-Gateway das erlaubt, Portale in den Knoten Ihrer Wahl zu rendern (nicht notwendigerweise Körper)
Bearbeiten : es scheint React-Popper kann eine anständige Alternative zu React-Tether sein. PopperJS ist eine Bibliothek, die nur eine geeignete Position für ein Element berechnet, ohne das DOM direkt zu berühren, und dem Benutzer die Wahl lässt, wo und wann er den DOM-Knoten platzieren möchte, während Tether direkt an den Körper angehängt wird.
Bearbeiten : Es gibt auch React-Slot-Fill , das interessant ist und ähnliche Probleme lösen kann, indem es erlaubt, ein Element zu rendern Ein reservierter Element-Slot, den Sie beliebig in Ihren Baum einfügen können
Hier finden Sie viele gute Lösungen und wertvolle Kommentare von bekannten Experten der JS-Community zum Thema. Es könnte ein Hinweis darauf sein, dass es nicht so trivial ist, wie es scheint. Ich denke, dass dies der Grund für Zweifel und Unsicherheit in dieser Angelegenheit sein kann.
Grundlegendes Problem hierbei ist, dass Sie in React nur Komponenten an das übergeordnete Element anhängen dürfen, was nicht immer das gewünschte Verhalten ist. Aber wie soll dieses Problem gelöst werden?
Ich schlage die Lösung vor, um dieses Problem zu beheben. Eine detailliertere Problemdefinition, Quelle und Beispiele finden Sie hier: https://github.com/fckt/react-layer-stack#rationale
Begründung
react
react-dom
kommt mit zwei grundlegenden Annahmen/Ideen:
- jede Benutzeroberfläche ist natürlich hierarchisch. Deshalb haben wir die Idee von
components
, die sich umschließenreact-dom
hängt (physisch) die untergeordnete Komponente standardmäßig an den übergeordneten DOM-Knoten anDas Problem ist, dass manchmal die zweite Eigenschaft nicht Ihren Vorstellungen entspricht in Ihrem Fall. Manchmal möchten Sie Ihre Komponente in .__ einbinden. unterschiedlichen physischen DOM-Knoten und halten eine logische Verbindung zwischen Elternteil und Kind gleichzeitig.
Ein kanonisches Beispiel ist eine Tooltip-ähnliche Komponente: an einem Punkt von Entwicklungsprozess könnten Sie feststellen, dass Sie einige .__ hinzufügen müssen. Beschreibung für Ihren
UI element
: er wird in fester Ebene und .__ gerendert. sollte seine Koordinaten kennen (wasUI element
coord oder mouse coords ist) und gleichzeitig benötigt es Informationen, ob es muss jetzt oder nicht gezeigt werden, seinen Inhalt und einen Kontext aus übergeordnete Komponenten. Dieses Beispiel zeigt manchmal die logische Hierarchie stimmt nicht mit der physischen DOM-Hierarchie überein.
Schauen Sie sich _/https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example an, um das konkrete Beispiel zu sehen, das die Antwort auf Ihre Frage ist :
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>)
// ...
Meines Erachtens hat die minimale Implementierung zwei Anforderungen. Ein Status, der verfolgt, ob die Modalität offen ist oder nicht, und ein Portal, um die Modalität außerhalb des Standardreaktionsbaums darzustellen.
Die folgende ModalContainer-Komponente implementiert diese Anforderungen zusammen mit den entsprechenden Renderfunktionen für das Modal und den Trigger, der für die Ausführung des Callbacks zum Öffnen des Modal verantwortlich ist.
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;
Und hier ist ein einfacher Anwendungsfall ...
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;
Ich verwende Renderfunktionen, da ich die Zustandsverwaltung und die Boilerplate-Logik von der Implementierung der gerenderten Modal- und Trigger-Komponente trennen möchte. Auf diese Weise können die gerenderten Komponenten so sein, wie Sie es möchten. In Ihrem Fall gehe ich davon aus, dass die modale Komponente eine verbundene Komponente ist, die eine Rückruffunktion empfängt, die eine asynchrone Aktion auslöst.
Wenn Sie dynamische Requisiten aus der Triggerkomponente an die modale Komponente senden müssen, was hoffentlich nicht allzu häufig vorkommt, empfehle ich, den ModalContainer mit einer Containerkomponente zu umschließen, die die dynamischen Requisiten in ihrem eigenen Zustand verwaltet und die ursprünglichen Rendermethoden verbessert so.
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;
Wickeln Sie das Modal in einen verbundenen Container und führen Sie den asynchronen Vorgang hier aus. Auf diese Weise können Sie sowohl den Dispatch zum Auslösen von Aktionen als auch die Eigenschaft onClose erreichen. (Um dispatch
von Requisiten zu erreichen, übergeben Sie die Funktion not mapDispatchToProps
an connect
.
class ModalConteiner 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);
Die App, in der das Modal gerendert wird und dessen Sichtbarkeitsstatus festgelegt ist:
class App extends React.Component {
state = {
isModalOpen: false
}
handleModalClose = () => this.setState({ isModalOpen: false });
...
render(){
return (
...
<ModalContainer onClose={this.handleModalClose} />
...
)
}
}