I want to make a modal view has dynamic content by injecting a component to it.
class RootView extends Component {
state = {
modalBody: null
}
setModalBody = (body) => {
this.setState({modalBody: body})
}
render() {
return(<ContextProvider value={this.setModalBody}><Modal>{this.state.modalBody}</Modal></ContextProvider>)
}
}
Then inside any children view i use setState to change parent modalBody
The modalBody can be setted on each route, which means the modalBody can be input list, selection list or text only. So the modalBody must have its state for controlling these inputs.
By this way, it renders ok, but the dynamic content couldn't be updated after state changed. The parent's dynamic content couldn't receive the ChildView new state, i have to setModalBody again and again after it rerendered.
For example, if input in modalBody has changed, the parent couldn't be updated.
class ChildView extends Component {
state = {
inputValue: null
}
handleChange = (e) => {
this.setState({inputValue: e.target.value})
}
setModalBody(body) {
this.props.context.setModalBody(<input value={this.state.inputValue} onChange={this.handleChange} />)
}
render() {
return(<Modal>{this.state.modalBody}</Modal>)
}
}
Full code: https://codesandbox.io/s/lp5p20mx1m
Any proper way to render dynamic content to parent?
I'm not sure why you'd need to create a parent Modal component, when you can make the Modal a simple reusable child component.
See here for a detailed explanation on how to achieve a stateful parent that controls a child modal.
However, if you MUST have a parent Modal component, then you can create a render prop to pass down props to be used by its children.
Working example:
components/Modal.js (parent component -- this has a lot of smaller components that were separated for reusability and ease of understanding -- they're basically simple divs with some styles attached -- see notes below)
import React, { Fragment, Component } from "react";
import PropTypes from "prop-types";
import BackgroundOverlay from "../BackgroundOverlay"; // grey background
import ClickHandler from "../ClickHandler"; // handles clicks outside of the modal
import Container from "../Container"; // contains the modal and background
import Content from "../Content"; // renders the "children" placed inside of <Modal>...</Modal>
import ModalContainer from "../ModalContainer"; // places the modal in the center of the page
class Modal extends Component {
state = { isOpen: false };
handleOpenModal = () => {
this.setState({ isOpen: true });
};
handleCloseModal = () => {
this.setState({ isOpen: false });
};
// this is a ternary operator (shorthand for "if/else" -- if cond ? then : else)
// below can be read like: if isOpen is true, then render the modal,
// else render whatever the child component is returning (in this case,
// initially returning an "Open Modal" button)
render = () =>
this.state.isOpen ? (
<Container>
<BackgroundOverlay />
<ModalContainer>
<ClickHandler
isOpen={this.state.isOpen}
closeModal={this.handleCloseModal}
>
<Content>
{this.props.children({
isOpen: this.state.isOpen,
onCloseModal: this.handleCloseModal,
onOpenModal: this.handleOpenModal
})}
</Content>
</ClickHandler>
</ModalContainer>
</Container>
) : (
<Fragment>
{this.props.children({
isOpen: this.state.isOpen,
onCloseModal: this.handleCloseModal,
onOpenModal: this.handleOpenModal
})}
</Fragment>
);
}
// these proptype declarations are to ensure that passed down props are
// consistent and are defined as expected
Modal.propTypes = {
children: PropTypes.func.isRequired // children must be a function
};
export default Modal;
components/Example.js (child component accepting isOpen, onCloseModal and onOpenModal from the parent -- with this approach, as you'll notice, there's duplicate isOpen logic. While this approach gives you full control over the parent, it's repetitive. However, you can simplify your components by moving the "Open Modal" button logic to the parent, and passing in a prop like <Modal btnTitle="Open Modal"> to make it somewhat flexible, BUT you'll still lose some control of what's being initially rendered when isOpen is false.)
import React, { Fragment } from "react";
import Modal from "../Modal";
import "./styles.css";
const Example = () => (
<div className="example">
<h2>Parent Modal Example</h2>
<Modal>
{({ isOpen, onCloseModal, onOpenModal }) =>
isOpen ? (
<Fragment>
<h1 className="title">Hello!</h1>
<p className="subtitle">There are two ways to close this modal</p>
<ul>
<li>Click outside of this modal in the grey overlay area.</li>
<li>Click the close button below.</li>
</ul>
<button
className="uk-button uk-button-danger uk-button-small"
onClick={onCloseModal}
>
Close
</button>
</Fragment>
) : (
<button
className="uk-button uk-button-primary uk-button-small"
onClick={onOpenModal}
>
Open Modal
</button>
)
}
</Modal>
</div>
);
export default Example;
Related
Hi I am a beginner in React, I am using Fluent UI in my project .
I am planning to use Panel control from Fluent UI and make that as common component so that I can reuse it.I use bellow code
import * as React from 'react';
import { DefaultButton } from '#fluentui/react/lib/Button';
import { Panel } from '#fluentui/react/lib/Panel';
import { useBoolean } from '#fluentui/react-hooks';
export const PanelBasicExample: React.FunctionComponent = () => {
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
return (
<div>
<Panel
headerText="Sample panel"
isOpen={isOpen}
onDismiss={dismissPanel}
// You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
closeButtonAriaLabel="Close"
>
<p>Content goes here.</p>
</Panel>
</div>
);
};
https://developer.microsoft.com/en-us/fluentui#/controls/web/panel#best-practices
I remove <DefaultButton text="Open panel" onClick={openPanel} /> from the example .
So my question is how can I open or close this panel from any other component ?
I would use React useState hook for this.
Make a state in the component that you want render the Panel like
const [openPanel, setOpenPanel] = useState({
isOpen: false,
headerText: ''
})
Lets say for example you will open it from button
<Button onClick={() => setOpenPanel({
isOpen: true,
headerText: 'Panel-1'
})
}> Open me ! </Button>
Then pass the state as props to the Panel component
<PanelBasicExample openPanel={openPanel} setOpenPanel={setOpenPanel} />
in PanelBasicExample component you can extract the props and use it.
export const PanelBasicExample(props) => {
const {openPanel, setOpenPanel} = props
const handleClose = () => {setOpenPanel({isOpen: false})}
return (
<div>
<Panel
headerText={openPanel.headerText}
isOpen={openPanel.isOpen}
onDismiss={() => handleClose}
// You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
closeButtonAriaLabel="Close"
>
<p>Content goes here.</p>
</Panel>
</div>
);
}
I have 2 classes to provide the modal-dialog functionality:
import React from 'react'
import Modal from 'react-modal'
export default class ModalBase extends React.Component {
state = { show:false }
handleOpen = opts => {
this.setState( { ...opts, show:true } )
console.info( 'ModalBase handleOpen', this.constructor.name, 'show', this.state.show )
}
handleClose = () => this.setState( { show:false } )
render() {
console.info( 'ModalBase render show', this.state.show )
return <Modal isOpen={this.state.show} onRequestClose={this.handleClose} className="Modal" overlayClassName="Overlay">
{this.props.children}
</Modal>
}
}
and
export default class InfoPopup extends ModalBase {
state = { ...this.state, tech:{} }
render() {
console.info('InfoPopup render show', this.state.show)
return (
<ModalBase>
<div/><div/>
</ModalBase>
)
}
}
When I call InfoPopup.handleOpen({a:42}), the following shows up in the console:
ModalBase handleOpen InfoPopup show true
InfoPopup render show true
ModalBase render show false
so, the ModalBase's state.show is not changed and hence the popup is not shown.
How shall I properly propagate the state to enclosing parent object?
TIA
Use composition instead of inheritance
From the React docs:
React has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components.
See: https://reactjs.org/docs/composition-vs-inheritance.html
So export default class InfoPopup extends ModalBase is not advised.
1. Let InfoPopup render ModalBase but keep track of open/close state
You could turn it around and have a generic BaseModal component for modal styling that you pass props such as title and content. The InfoPopup keeps track of the opened/closed state. From the same React docs page:
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
2. Render ModalBase and pass the type of modal as prop
You could also always render ModalBase for an info, warning, error modal etc. Then you pass the type of modal as prop to ModalBase. ModalBase determines some specifics based on that type prop.
3. use a render prop
Described here: https://reactjs.org/docs/render-props.html
Let ModalBase accept a function as children prop.
So in InfoPopup:
<ModalBase>
{({ toggle }) => (
<button onClick={toggle} />
)}
</ModalBase>
And in ModalBase:
render() {
return <Modal ...>{this.props.children({ toggle: this.openOrClose })}</Modal>
}
4. Pass a component to ModalBase to render when open
A bit of a variant on 2. You could also pass a component as prop to ModalBase that it should show when it's open.
<ModalBase
modalContent={<InfoPopup />}
/>
I have the following component which makes up my modal:
import React from 'react';
import { ModalBody, Button, Alert } from 'bootstrap';
import { AppModalHeader } from '../../common/AppModalHeader';
import ModalWrapper from './ModalWrapper';
const QuestionModal= ({
title,
noText = 'No',
yesText = 'Yes',
questionText,
onYesAction
children
}) => {
const { toggle, isOpen, openModal } = useModalForm();
return (
<React.Fragment>
<ModalWrapper className={className} isOpen={isOpen} toggle={toggle}>
<AppModalHeader toggle={toggle}>{modalTitle}</AppModalHeader>
{isOpen ? (
<ModalBody>
<p>{questionText}</p>
<Button
className="float-right"
color="primary"
onClick={() => {
if (onYesAction !== undefined) {
onYesAction(toggle);
}
}}
>
{yesText != null ? yesText : 'Yes'}
</Button>
</ModalBody>
) : null}
</ModalWrapper>
{children({
triggerModal: () => openModal({ id: undefined }),
toggle
})}
</React.Fragment>
);
};
export default QuestionModal;
I want to use it as such, where I can dynamically choose the name of the trigger that opens the modal:
In use e.g. (note: the inner question modal would be repeated, used 4 or 5 times in my application):
....
<QuestionModal
//....params that match up with above
>
{({ triggerModal }) => (
<QuestionModal
//....params that match up with the component
>
{({ triggerModal2 }) => (
<>
<Button onClick={()=>triggerModal();}>Trigger Modal 1</Button>
<div>
<Button onClick={()=>triggerModal2();}>Trigger Modal 2</Button>
</div>
</>
</>
)}
</QuestionModal>
....
How could I achieve this, by extending the question modal to pass a dynamic function? Just because I keep getting stuck in having to think about duplicating the original component, I want to make this component as reusable as I can. Any help would be greatly appreciated.
Thanks in advance
I think you're overcomplicating things. The problem is you're trying to control whether or not the modal is rendered from inside the modal itself. If you really want to have reusable components, it's good to decouple presentation from logic. In your case, you want to have a modal component with all the presentation/layout/styling stuff and pass in via props the actual content.
For example:
import React from 'react';
import { ModalBody, Button, Alert } from 'bootstrap';
import { AppModalHeader } from '../../common/AppModalHeader';
import ModalWrapper from './ModalWrapper';
const QuestionModal= ({
title,
noText = 'No',
yesText = 'Yes',
questionText,
onYesAction
children
}) => {
return (
<React.Fragment>
<ModalWrapper>
<AppModalHeader toggle={toggle}>{title}</AppModalHeader>
<ModalBody>
<p>{questionText}</p>
<Button
className="float-right"
color="primary"
onClick={onYesAction}
>
{yesText}
</Button>
</ModalBody>
</ModalWrapper>
</React.Fragment>
);
};
export default QuestionModal;
Now this is a purely presentational component, it creates a skeleton in which you put the actual content. And for using it, you'll control whether or not the modal is rendered from where it is actually used, like so:
import React, {useState} from 'react';
import QuestionModal from './QuestionModal'
const SomeComponent = (props) => {
const [showModal, setShowModal] = useState(false);
const toggleModal = () => {
setShowModal(!showModal);
}
const yesActionLogic = () => {
// Your yes-action logic...
}
return (
<div>
{showModal ? (
<QuestionModal
title="Sample title",
questionText="Question?"
onYesAction={yesActionLogic}
/>
) : null}
<Button onClick={toggleModal}>Toggle Modal</Button>
{/* The rest of your stuff... */}
</div>
);
}
If you want to create reusable components, it's good practice to not put any business logic on it. Use props to pass in functions that will be triggered from inside the components, and lift all the work to the components that actually hold your business logic.
One of the SOLID principles of software engineering is called Single-responsibility principle, and you can apply it to your React components:
Your Modal component is responsible for displaying data in its correct layout and triggering some set of functions from outside, regardless of what data/logic you pass.
This Modal component will be used by some other component whose responsibility is to show the user a modal with some specific data, at the right time.
So it makes sense that you should toggle your modal from outside.
On a personal note, I like to structure a React app in components that hold only presentational logic, and are used by containers, which are more logic-dense (generally having async requests).
I have a component (ListItem.js), which shows some photos from an API request.
Inside the same component I have a function called showAlert, that also uses some data from the API request and shows them in an alert dialog.
To invoke the showAlert function I use this line:
onClick={() => this.showAlert()}
This is the whole component (simplified):
import React, { Component } from "react";
import SweetAlert from "react-bootstrap-sweetalert";
class ListItem extends Component {
state = {
alert: null // initialising an empty alert
};
showAlert() {
const getAlert = () => (
<SweetAlert confirmBtnBsStyle="info" onConfirm={() => this.closeAlert()}>
<img src={this.props.photo.urls.small} />
</SweetAlert>
);
this.setState({
alert: getAlert() // Fire up the dialog box
});
}
closeAlert() {
this.setState({
alert: null // colse the dialog window
});
}
render() {
return (
<>
<div className="card__body">
<img
src={this.props.photo.urls.small}
onClick={() => this.showAlert()}
/>
</div>
{this.state.alert}
</>
);
}
}
export default ListItem;
I want to remove the showAlert function and put it in a separate component, so I can use it in different components.
For this I created a new component called Alert.js, cut the showAlert function and the state, put them inside the Alert.js and exported it.
Then inside ListItem.js, I imported the Alert component and replaced this line:
{this.state.alert}
With this line:
<Alert photoPath={this.props.photo.urls.small} />
But what I don't know is, how can I invoke showAlert funcion, which is inside the Alert.js (child component) from the ListItem.js (parent component)?
The question is: How can I invoke a function inside the child component from the parent component?
There is no way to trigger function calls from the parent. In that situation, what you want to do is pass the function showAlert as a prop to the Alert component, and bind this to the parent component and trigger it when needed in the parent.
I have a parent ButtonGroup component and the child buttonItem component:
//ButtonGroup Component (Parent)
clicky(){
//capture the props of the clicked button, ie, caption and disabled here.
}
render() {
return (
<div onClick={this.clicky}>
{this.props.children}
</div>
)
}
//buttonItem component:
render() {
return (
<button disabled={this.props.disabled}>{this.props.caption}</button>
)
}
//final render
<ButtonGroupComponent>
<buttonItem caption="Nothing"/>
<buttonItem caption="Something" disabled={true}/>
<buttonItem caption="Refresh"/>
</ButtonGroupComponent>
from the above code is there any way i can capture the props of the clicked child buttonItem?
In your case, you need to merge this.props.children with your custom prop. So, I suggest you use React.Children to operate with it.
By the way, after adding new prop you need to return this child, so cloneElement will help you with this.
Inside import section of ButtonGroupComponent:
import React, { Children, Component, cloneElement } from 'react';
Its render function will look like this:
render() {
const childrenWithCustomHandler = Children.map(this.props.children, itemChild => {
// Arguments of cloneElement (component, [customProps], [customChildren])
return cloneElement(itemChild, { onClickItem: this.clicky })
}
);
return <div>{childrenWithCustomHandler}</div>;
}
And the code of buttonItem component will look like:
return (
<button
disabled={this.props.disabled}
onClick={() => {
this.props.onClickItem({ ...this.props });
}}
>
{this.props.caption}
</button>
)
I used Spread operator to clone the object, so if you will want to change props in your clicky function, the child won't be rendered.