How can I append a component to a modal using a nested portal?
I am building a page that displays several modals. I want to reuse the same modal but when a button is clicked, the app loads different content into the modal.
WIP CodePen:
https://codepen.io/jtsharpsite/pen/gorvjR
render() {
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}
I have it pretty close to how I think it might work, but I cannot figure out how to append another component to an already appended modal component.
I have a button which calls a handler that opens the modal and specifies the component type.
<button onClick={this.handleShow.bind(this, "p3009", "product")}>
Product 3009
</button>
<button onClick={this.handleShow.bind(this, "s1", "special")}>
Special #1
</button>
The handler is in the App context and opens the modal sibling:
handleShow(modalId, modalType) {
this.setState({ showModal: true });
}
When the modal component mounts, I then try to append the Product when it mounts.
componentDidMount() {
//TODO how to append to parent modal?
modalRoot.appendChild(this.el);
}
How can I move the <Product> or the <Special> up into the <Modal>?
Here's an example of a reusable modal form... Theres a lot here you don't need, but just pay attention to the handleSubmit. Within the handleSubmit we call onComplete() which can be any function we pass through to the reusable modal when we call it, based on what function we want our modal to do... in our case it was capturing username, password, etc.
class ReusableModalForm extends React.Component {
constructor(props){
super(props);
this.state ={
};
}
handleChange(e) {
let {name, value} = e.target;
this.setState({
[name]: value,
usernameError: name === 'username' && !value ? 'username must have a value' : null,
emailError: name === 'email' && !value ? 'email must have a value' : null,
passwordError: name === 'password' && !value ? 'password must have a value' : null,
});
}
handleSubmit(e) {
e.preventDefault();
this.props.onComplete(this.state)
}
render() {
return (
<Modal
open={this.state.createAccountModalOpen}
trigger={<Link size="m" theme="bare" href="#" className="main-menu-item" onClick={this.handleSubmit}>{this.props.buttonText}</Link>}
closeIcon
onClose={() => { this.setState({
createAccountModalOpen: false,
}); }}
>
<Header icon='add user' content='Create account' />
<Modal.Content>
<Form />
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.handleSubmit}>
<Icon name='add user' /> {this.props.buttonText}
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default ReusableModalForm;
Based on this logic, you could construct a modal with a series of "trigger texts" to only render certain things based on what kind of props are passed down when you use your reusable modal.
Example.
<ReusableModal triggerText={'showAdress'} onComplete={this.showUsersHomeOnGoogle} />
Then in your reusableModal somewhere....
{this.props.triggerText === showAdress ? this.setState=({showHomeAdressPortion: true)}
UPDATES
class App extends React.Component {
constructor(props) {
super(props);
this.state = { showModal: false };
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow(modalId, modalType) {
console.log(
"Append content with ID: " + modalId + " of component type: " + modalType
);
//Here you should do a "fetch" first and grab your entire product information based on chosen ID and store it to state as an object...
//setState({activePortal: someContentObject})
//Then... I would use promise or aync await, show modal...
//Have Modal display from this state object
this.setState({ showModal: true, activePortal: });
//if it is a product
if (modalType == "product") {
//fetch additional images and add to state
//add images to {product.additionalPics}
}
//TODO re-render the modal with the appending element portal
}
handleHide() {
this.setState({ showModal: false });
}
render() {
//console.log("RENDER App");
// Show a Modal on click.
const modal = this.state.showModal ? (
<Modal>
<div className="modal">
<div className="modal-content-wrapper">
{/* I think what you're asking to do here is display basically "any" information that comes from a product fetch. This isnt possible without templeting somehow.. You will have to hard code like "title" "body" "image" into this modal...
<h1>this.state.activeProduct.title</h1>*/}
{/* CONTENT NEEDS TO GO HERE SOMEHOW */}
{this.props.children}
</div>
<button onClick={this.handleHide}>Hide modal</button>
</div>
</Modal>
) : null;
Have you tried using a parent-child pattern in the portal?
For example, make your children like this and just have ModalDisplay render its children. I don't know if what you are trying to do is possible because I think it requires portals to support fragments and I'm not sure if they do.
I don't know if I got the context of your question right but the idea is the same you have a single child that itself has children instead of having more than 1 child in the portal call.
<ModalDisplay>
<Special title="special" text="text" />
<Product title="product" pic="picture" />
</ModalDisplay>
Related
New to JavaScript over the past two weeks and am attempting to build a table from a button click. Using react-bootstrap/Button and react-bootstrap-table-next libraries. I have three files that I'm working on:
File that holds the layout of the page
A ReactButton class
Table class
I can render the button on the page, but can't get the table to render on a click. On the layout page, I'm building the button with:
render() {
return (
<div>
<ReactButton
name="Reset Table"
onClick={this.buttonClick}
keyField={{keyField: 'keyField'}}
products={DataPopulation.sampleData()}
columns={DataPopulation.getFields()}
selectRowProp={{mode: 'checkbox'}}
/>
</div>
The button code is:
class ReactButton extends Component {
constructor(props) {
super(props);
this.state = {
clicked: false
};
this.buttonClick = this.buttonClick.bind(this);
}
async buttonClick() {
this.setState({
clicked: true
});
console.log(this.props);
console.log("Button part")
}
render() {
return (
<Button
variant="primary"
size="lg"
onClick={this.buttonClick}>
{this.props.name}
</Button>
)
}
}
export default ReactButton;
When I click the button, I can see the log of the properties (keyField, products, columns, etc) passed to the Button class. Is it possible to then pass these properties to a table class file and have it render the table? Table code with the react-bootstrap-table-next libraries would be something like this, I would imagine:
class MultiSelectTable extends Component {
constructor(props) {
super(props)
}
...
<BootstrapTable
keyField= { this.props.keyField }
data={ this.props.products }
columns={ this.props.columns }
selectRow={ this.props.selectRowProp }
/>
But I'm unsure how the flow should be to call this from the main layout page. Should I call the table build from the Button class? And how would I render the table?
Edit: Currently reviewing this for more info about it. It seems like the logic needs to be: the page renders the button, which when clicked renders the table. What is the preferred best practice for referencing and building hierarchical components?
You could do something like this in the parent component:
buttonClick(clicked) {
this.setState({
buttonClicked: clicked
})
}
render() {
return (
<>
<ReactButton
name="Reset Table"
onClick={this.buttonClick}
keyField={{keyField: 'keyField'}}
products={DataPopulation.sampleData()}
columns={DataPopulation.getFields()}
selectRowProp={{mode: 'checkbox'}}
/>
{this.state.buttonClicked ? <MultiSelectTable {...props} /> : null}
</>
)
}
And then in the button component you pass the click state back to the parent components click function:
buttonClick() {
this.props.onClick(true);
}
That way you make sure the table is rendered only when the button is clicked and all the props gets passed to the table component.
I am trying to add an element from a list called "Others" to a list called "Favorites" and vice-versa once I click the "add to favorite" button, I am really new to reactJS and I am solving this purely for practicing purposes.
What I am trying to do is creating a handleClick that toggles the state of a single contact from "Other" to "Favorite" and vice-versa, this is done on the "User" component, meanwhile the caller of this is the "Users" component, however I am not finding a way to tell the "Users" component that a "User" has changed and should switchLists, this is my code right now.
Users
class Users extends Component{
render(){
this.props.users.sort(function(a,b){
if(a.name > b.name){
return 1;
}else{
return -1;
}
})
return(
<div>
<h1>Favorites</h1>
<div>{this.props.users.map(user => <User user={user}
favList = {true}
key={user.id}/>)}</div>
<h1>Others</h1>
<div>{this.props.users.map(user => <User user={user}
favList = {false}
key={user.id}/>)}</div>
</div>
)
}
}
User
class User extends Component{
StyleCompleted(){
return {
fontSize: '20px',
}
}
constructor(props) {
super(props);
this.state = {isToggleOn: this.props.user.isFavorite
};
// Este enlace es necesario para hacer que `this` funcione en el callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render(){
const {user} = this.props;
const {favList} = this.props;
if (favList==this.state.isToggleOn)
return (
<div >
<img src={UserImage} alt="userPic"/>
{user.name} -
{user.companyName}
<button onClick={this.handleClick}>
<Star star={
this.state.isToggleOn ?
true : false
}/>
</button>
</div>
)
else
return (<h1>Element changes but is not added</h1>)
}
}
Star
class Star extends Component{
render(){
const {star} = this.props;
return (
<img src={
star ? starTrue : starFalse
} alt="star"/>
)
}
}
The expected result when I click an element should be to remove it from the current list and add it to the other list, the actual result is just a change on the current node/element
You need to elevate your click handler to the parent function and manage state there instead of at the individual user level. You could pass down a prop from parent to child to pass this information back and forth while managing state in the child. However, this is extremely bad practice as it create the possibility of two sources of truth.
I've created modal with React Bootstrap. Despite the fact, that I'm using onHide function, nothing happens. Here's my modal:
<React.Fragment>
<Modal
{...this.props}
bsSize="large"
aria-labelledby="contained-modal-title-lg"
show={this.state.showModal}
onHide={this.handleHide}
>
<Modal.Body>
...
</Modal.Body>
<Modal.Footer>
<Button id = "closeModal" variant="danger" onClick=
{this.handleHide.bind(this)}>
Save and close
</Button>
</Modal.Footer>
</Modal>
</React.Fragment>
I'm passing "show" from other component, when click occurs on some element. onClick on that element is specified to: "showModal: true". Then I'm passing showModal to other component that is rendering different elements according to option choosed:
{this.state.addingState && (
<MyForm
{...this.state.item}
show={this.state.showModal}
...
/>
)}
At last in MyForm component I have function that passes props to component with modal:
createModalComponent {
...
let modalComponentProps= {
...
show: this.props.show,
}
So, this is the way "show" is going. In my file with modal component I have function for handling hide:
handleHide() {
this.setState({ showModal: false });
}
Now in this component showModal is initialize in state like so:
constructor(props) {
super(props);
this.state = {
showModal: this.props.show
};
this.handleHide = this.handleHide.bind(this);
}
I've tried many things. Other state variables, without initializing showModal in state and many more. When clicking on the button or beyond the modal, modal is still visible and not hiding. I will be very grateful for your help and/or suggestions how to fix this.
So, the way showModal is going: parent component (where this.state.addingState is happening) -> MyForm component (where let modalComponentProps= { show: this.props.show, ... happens) -> actual modal component
Code on CodePen
you have 2 possibilities: you can add the method to your parent and pass the method + the result of show like this (use same name of props and method for best practice, so you will be not confuse):
Parent
<Modal
{...this.props}
bsSize="large"
aria-labelledby="contained-modal-title-lg"
show={this.state.showModal}
handleHide={this.handleHide}
>
Child
<MyForm
show={this.props.showModal}
handleHide={this.props.handleHide}
/>
To use the modal from parent, you can call it like this in child: this.props.handleHide(true).
Or you let the child manage the state by itself, so you would place the method and state in child, not in parent (depending on the architecture).
It is not recommended to add the props in child state.
Also, you could use es6 function to avoid binding like this:
handleHide = () => this.setState({ showModal: false });
Look on the shouldComponentUpdate method
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
You are checking only props but you are changing the state of the component. Fix the method or remove it and it will be working
I have created a loading icon component, which simply displays a spinner while loading something. I load it into my Sign In component, and wish to display the icon once the user clicks the Login button (And the API call is busy).
So:
import Loading from '../common/loading';
I then set an isLoading variable, defaulted to false:
this.isLoading = false;
Then, within my render method, I wish to determin if I need to show the spinner or not.
render() {
var LoadingSpinner = this.state.canLogin ? Loading : '<div></div>';
This fails.
And then my button is where I show the spinner. I'm hoping to hide the 'Sign In' text, and replace it with the spinner, but ... first thing is to handle the spinner, based on the isLoading variable.
<button
className="btn btn-lg btn-primary btn-block"
type="button"
onClick={this.handleSignin}
disabled={!this.state.canLogin}>
<span>Sign in</span> <LoadingSpinner />
</button>
</div>
Can/should this be done this way, OR... should I maybe pass a prop to my Loading component, called 'Visible' or something, and I set that?
put isLoading to constructor with default false
and then inside the render method, just add a condition
{ this.state.canLogin ? <LoadingSpinner /> : null }
Here is what you could do, using a state variable.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false
}
}
onClick = () => {
this.setState({
loading: true
})
}
render() {
return (
<div>
{this.state.loading && <div>Loading</div>}
<button onClick={this.onClick}>Click to Load</button>
</div>
);
}
}
ReactDOM.render( < App / > , document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='root'>
</div>
I am trying to display the relevant tasks for a user after clicking on a button called "View Tasks". The problem I am having right now is, when I click this button, all React Bootstrap Collapse Components open up and the tasks being shown are displayed under all users, but it should only be shown for one specific user. I am rendering all these with a map function.
I need to open one Collapse at a time, but since I am rendering with a map, the onClick function is the same for every one created so this triggers all of them to open, how can I do it so that only one opens at a time and opens ones close when another View Tasks button is clicked?
Also how can I do it so that the relevant tasks shows up under the specific user where i clicked "View Tasks" for and not show up under everyone.
//Project Component
constructor(props) {
super(props);
this.state = {}
this.handleJoin = this.handleJoin.bind(this);
this.open = this.open.bind(this);
this.openCollapse = this.openCollapse.bind(this);
}
openCollapse(event) {
this.setState({open: !this.state.open})
var projectID = this.props.router.params.id;
var userID = event.target.getAttribute('data-memberID');
this.props.populateTasks(projectID, userID)
}
teamMems = members.map((members, i) =>
<div key={i} className='container'>
<h4>{members.username}</h4>
<Button bsStyle="success" data-memberID={members._id} onClick={this.openCollapse}>View Tasks</Button>
<Collapse in={this.state.open}>
<div>
<Well>
{ this.props.userTasks
?
this.props.userTasks.map((task, i) =>
<p key={i}>{task.task.toString()}</p>
)
:
nothing
}
</Well>
</div>
</Collapse>
//populateTasks action creator
export function populateTasks(projectID, userID){
return function(dispatch){
axios.get('/populate-tasks/' + projectID + '/' + userID).then((res) => {
console.log("RESPONSE IN POPULATE TASKS ACTIONS", res)
dispatch({ type: "SET_USER_TASKS", payload: res.data.task })
})
}
}
So as what I said, keep a separate component with collapse state for TeamMember whose tasks or toggled with button. Something like this
class TeamMember extends Component {
constructor(props);
this.state = {
open: false //collapse state for this member
}
render(){
<div>
<YourButton with onClick={someStateChangingMethod} />
<YourMemberContent />
</div>
}
}
And in your main component create the above components over teamMems loop.
teamMems = members.map((member, i) => (
<TeamMember key={i} {...this.props} member={member}>
)
Hope this helps.