ReactJS Call a function in render after an action - reactjs

I'm using ReactJS, and shopify's polaris in order to create a website. I'm very new to react so this might be a newbie question but I looked over the internet and couldn't manage to put the pieces together.
I have a dropdown list and basically whenever the user clicks on an item from a list I want to add a button next to the dropdown. Here is my code:
import React from "react";
import { ActionList, Button, List, Popover } from "#shopify/polaris";
export default class ActionListExample extends React.Component {
constructor(props) {
super(props);
this.state = {
active: false,
title: "Set Period",
};
}
renderButton() {
console.log("Button clicked")
return (
<div>
<Button fullWidth={true}>Add product</Button>;
</div>
);
}
togglePopover = () => {
this.setState(({ active }) => {
return { active: !active };
});
};
render() {
const activator = (
<Button onClick={this.togglePopover}>{this.state.title}</Button>
);
return (
<div style={{ height: "250px" }}>
<Popover
active={this.state.active}
activator={activator}
onClose={this.togglePopover}
>
<ActionList
items={[
{
content: "One",
onAction: () => {
this.setState({ title: "One" }, function() {
this.togglePopover();
this.renderButton() //THIS IS WHERE I CALL THE METHOD
});
}
}
]}
/>
</Popover>
</div>
);
}
}
I've placed a comment in the code to show where I call the renderButton() method. Whenever I click the "One" element in the dropdown, it prints out "Button clicked" but nothing gets rendered to the screen. Any help is greatly appreciated. Thanks in advance!

You need to add another variable to check if an item is clicked, and as #azium commented, you need to add the output to your JSX, not inside the onAction function.
As of right now, you close the Popper when an item is clicked, setting this.state.active to false, so you can't rely on that to render your button. You need to add something like this.state.isButton or something and in onAction include:
onAction: () => {
this.setState({ title: "One", isButton: true }, () => {
this.togglePopover();
});
}
and then in your JSX:
{this.state.isButton && this.renderButton()}

This is a perfect use case for conditional rendering.
You basically want to render a component based on a condition (Boolean from your state in this case).
Conditional rendering can be written is several ways as you can see in the docs.
In your case i would go for something like this:
return (
<div style={{ height: "250px" }}>
<Popover
active={this.state.active}
activator={activator}
onClose={this.togglePopover}
>
<ActionList
items={[
{
content: "One",
onAction: () => {
this.setState({ title: "One" }, function() {
this.togglePopover();
});
}
}
]}
/>
{this.state.active && this.renderButton()}
</Popover>
</div>
);
}
}
Note that i just placed it at a random place, feel free to move it wherever you need it in the markup.

Thanks to everyone's help I finally was able to do this. I placed an extra attribute in the state called isButton and I initially set it equal to false. Here is my render function:
render() {
const activator = (
<Button onClick={this.togglePopover}>{this.state.title}</Button>
);
return (
<div style={{ height: "250px" }}>
<Popover
active={this.state.active}
activator={activator}
onClose={this.togglePopover}
>
<ActionList
items={[
{
content: "One",
onAction: () => {
this.setState({ title: "One", isButton: true }, function() { //Set isButton to true
this.togglePopover();
});
}
}
]}
/>
{this.state.isButton && this.renderButton()} //ADDED HERE
</Popover>
</div>
);
}
Please look at the comments to see where code was changed. Thanks!

Related

Change Component property through onClick

I have a ButtonGroup with a few Buttons in it, and when one of the buttons gets clicked, I want to change its color, I kinda want to make them behave like radio buttons:
<ButtonGroup>
<Button
variant={"info"}
onClick={(e) => {
..otherFunctions..
handleClick(e);
}}
>
<img src={square} alt={".."} />
</Button>
</ButtonGroup>
function handleClick(e) {
console.log(e.variant);
}
But that doesnt work, e.variant is undefined.
If it was just a single button I would have used useState and I would be able to make this work, but how do I make it work when there are multiple buttons, how do I know which button is clicked and change the variant prop of that button? And then revert the other buttons to variant="info"
Another approach that I could think of is to create my own Button that wraps the bootstrap Button and that way I can have access to the inner state and use onClick inside to control each buttons state, but I'm not sure if that will work, as then how would I restore the other buttons that werent clicked..?
To further from my comment above, you could create your own button component to handle its own state and remove the need to have lots of state variables in your main component e.g.
const ColourButton = ({ children }) => {
const [colour, setColour] = React.useState(true)
return (
<button
onClick={ () => setColour(!colour) }
style = {{color: colour ? "red" : "blue"} }
>
{ children }
</button>
)
}
That way you can just wrap your image in your new ColourButton:
<ColourButton><img src={square} alt={".."} /></ColourButton>
Edit:
I actually like to use styled-components and pass a prop to them rather than change the style prop directly. e.g. https://styled-components.com/docs/basics#adapting-based-on-props
EDIT: Kitson response is a good way to handle your buttons state locally :)
I like to handle the generation of multiple elements with a function. It allows me to customize handleClick.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [buttons, setButtons] = useState([
{
id: 1,
variant: "info"
},
{
id: 2,
variant: "alert"
}
]);
const handleClick = id => {
setButtons(previous_buttons => {
return previous_buttons.map(b => {
if (b.id !== id) return b;
return {
id,
variant: "other color"
};
});
});
};
const generateButtons = () => {
return buttons.map(button => {
return (
<button key={button.id} onClick={() => handleClick(button.id)}>
Hey {button.id} - {button.variant}
</button>
);
});
};
return <div>{generateButtons()}</div>;
}
https://jrjvv.csb.app/
You can maintain a state variable for your selected button.
export default class ButtonGroup extends Component {
constructor(props) {
super(props);
this.state = {
selected: null
};
}
handleClick = e => {
this.setState({
selected: e.target.name
});
};
render() {
const selected = this.state.selected;
return (
<>
<button
name="1"
style={{ backgroundColor: selected == 1 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="2"
style={{ backgroundColor: selected == 2 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="1"
style={{ backgroundColor: selected == 3 ? "red" : "blue" }}
onClick={this.handleClick}
/>
</>
);
}
}
Here is a working demo:
https://codesandbox.io/live/OXm3G

Why does clicking expand button closes the side panel in react?

i have a side panel with items listed. when the list item content overflows expand button appears and clicking that expand btn would show the entire content of list item
For this i have created a expandable component. this will show arrow_down when list item content overflows and clicking arrow_down shows up arrow_up.
However with the below code, clicking button 1 just makes the sidpanel disappear instead of arrow_up appearing. could some one help me solve this. thanks.
export default class Expandable extends React.PureComponent{
constructor(props) {
super(props);
this.expandable_ref = React.createRef();
this.state = {
expanded: false,
overflow: false,
};
}
componentDidMount () {
if (this.expandable_ref.current.offsetHeight <
this.expandable_ref.current.scrollHeight) {
this.setState({overflow: true});
}
}
on_expand = () => {
this.setState({expanded: true});
console.log("in expnad");
};
on_collapse = () => {
this.setState({expanded: false});
};
render () {
return (
<div className={(this.state.overflow ?
this.props.container_classname : '')}>
<div className={(this.state.overflow ?
this.props.classname : '')} style={{overflow: 'hidden',
display: 'flex', height: (this.state.expanded ? null :
this.props.base_height)}}
ref={this.expandable_ref}>
{this.props.children}
</div>
{this.state.overflow && this.state.expanded &&
<div className={this.props.expand}>
<button onClick={this.on_collapse}>
{this.props.arrow_up}</button>
</div>}
{this.state.overflow && !this.state.expanded &&
<div className={this.props.expand}>
<button onClick={this.on_expand}>
{this.props.arrow_down}</button>
</div>}
</div>
);
}
}
In the above code i pass the base_height to be 42px.
Edit:
i have realised for the side panel component i add eventlistener click to close the side panel if user clicks anywhere outside sidepanel. When i remove that eventlistener it works fine....
class sidepanel extends React.PureComponent {
constructor(props) {
super(props);
this.sidepanel_ref = React.createRef();
}
handle_click = (event) => {
if (this.sidepanel_ref.current.contains(event.target)) {
return;
} else {
this.props.on_close();
}
};
componentDidMount() {
document.addEventListener('click', this.handle_click, false);
}
componentWillUnmount() {
document.removeEventListener('click', this.handle_click, false);
}
render() {
return (
<div>
<div className="sidepanel" ref=
{this.sidepanel_ref}>
{this.props.children}
</div>
</div>
);
}
}
when i log the event.target and sidepanel_ref.current i see the button element in both of them but svg seems different in both of them.
How can i fix this?
Probably it is because click events bubble up the component tree as they do in the DOM too. If you have an element with an onClick handler inside an element with another onClick handler it will trigger both. Use event.stopPropagation() in the handler of the inner element to stop the event from bubbling up:
export default class Expandable extends React.PureComponent{
constructor(props) {
super(props);
this.expandable_ref = React.createRef();
this.state = {
expanded: false,
overflow: false,
};
}
componentDidMount () {
if (this.expandable_ref.current.offsetHeight <
this.expandable_ref.current.scrollHeight) {
this.setState({overflow: true});
}
}
toggleCollapse = event => {
// use preventDefault here to stop the event from bubbling up
event.stopPropagation();
this.setState(({expanded}) => ({expanded: !expanded}));
};
render () {
const {className, container_classname, base_height, expand, arrow_up, arrow_down} = this.props;
const {overflow, expanded} = this.state;
return (
<div className={overflow ? container_classname : ''}>
<div
className={overflow ? classname : ''}
style={{
overflow: 'hidden',
display: 'flex',
height: expanded ? null : base_height
}}
ref={this.expandable_ref}
>
{this.props.children}
</div>
{overflow && (
<div className={expand}>
<button onClick={this.toggleCollapse}>
{expanded ? arrow_up : arrow_down}
</button>
</div>
)}
</div>
);
}
}

How to pass a prop back from child to parent in onclick handler and then to another component in React?

I am passing a function to a grandchild (SingleProject) from my parent (App) that opens a model. The grandchild just renders an li in the child (Gallery) that show a list of images(coming from the grandchild). I am trying to get the object from props on the grandchild, pass it back to App, and then through to the modal (Modal) to display the information about the image. The objects that hold the info about the images are stored in a service (module.exports). The openModal function is what is being passed along to the grandchild. The closeModal will be passed to the modal. I am having problems with this. Any help would be appreciated.
//APP (Parent)
class App extends Component {
constructor(props) {
super(props);
this.closeModal = this.closeModal.bind(this);
this.openModal = this.openModal.bind(this);
this.state = {
open: false,
projects: Service,
selectedProject: Service[0]
}
console.log('selectedProject: ', this.state.selectedProject)
}
closeModal(event) {
this.setState({open: false});
console.log('app: ', this.state.open);
}
openModal(event) {
this.setState({open: true});
console.log('app: ', this.state.open);
}
render() {
const show = {
display: 'block'
};
const hide = {
display: 'none'
};
return (
<div>
<div style={this.state.open === false ? hide : show}>
<Modal
value={this.state.open}
closeModal={this.closeModal}
project={this.state.selectedProject}
/>
</div>
<Header />
<Intro />
<WhatIDo />
<WhoIAm />
<Gallery
value={this.state.open}
projects={this.state.projects}
openModal={this.openModal}
/>
<Contact />
<Footer />
</div>
);
}
}
//GALLERY (child)
const Gallery = (props) => {
console.log('props: ', props)
const projectItems = props.projects.map(project => {
return (
<SingleProject
key={project.name}
project={project}
openModal={props.openModal}
/>
);
});
return (
<div className="gallery">
<h3>My Work</h3>
<ul>{projectItems}</ul>
</div>
);
}
//SINGLEPROJECT (grandchild)
const SingleProject = (props) => {
return (
<li className="gallery-images">
<img
src={props.project.img}
onClick={props.openModal}
/>
</li>
);
}
//MODAL (Modal)
const Modal = (props) => {
return(
<div className="modal">
<div
className="modal-close"
onClick={props.closeModal}
/>
<ModalDesc project={props.project} />
</div>
);
}
//SERVICE
module.exports = [
{
name: 'xxx',
img: '.././images/gallery/xxx',
link: 'xxx',
tech: 'AngularJS, jQuery, HTML, CSS, PostgreSQL, Node.js, Gulp, Express.js',
desc: 'A clone of the Texas restaurant Dixie Chicken\'s website featuring a
backend and some parallax effects. This site is not fully responsive.'
},
{
name: 'xxx',
img: '.././images/gallery/xx',
link: 'xxx',
tech: 'AngularJS, jQuery, HTML, CSS, PostgreSQL, Node.js, Gulp, Express.js',
desc: 'A fully responsive clone of the E-commerce website Stance. This was a
group project. I did the set up of the backend, the endpoints, the frontend
state management, the directives, most of the SQL files, the single products
view, the login view, the account view, and the register view.'
},
{
name: 'xxx',
img: '.././images/gallery/xxx',
link: 'xxx',
tech: 'AngularJS, jQuery, HTML, CSS',
desc: 'A fully responsive site for The Battlefield Pilot Club.'
}
];
If I understand what you need correctly, you want to pass the project information to the openModal method
You just have to make a small edit to the SingleProject component
const SingleProject = (props) => {
return (
<li className="gallery-images">
<img
src={props.project.img}
onClick={() => props.openModal(props.project)}
/>
</li>
);
}
and then in your openModal method you can set the selectedProject;
openModal(project) {
this.setState({
open: true,
selectedProject: project,
}, () => {
console.log('app: ', this.state.open); // this will print "true"
});
console.log('app: ', this.state.open); // this will print "false"
}
Explanation:
Instead of passing props.onClick to the onClick prop of <img/> we wrap it with an arrow function that will invoke props.onClick with the project object
When you're logging the state in the openModal method, if you want it to print true you'll need to print it in the callback to the setState method since the setState call does not immediately set the state of the class but rather, react will batch the updates. official documentation for the same here

Callback function, responsible for updating state, passed as props to child component not triggering a state update

The callback function (lies in Images component) is responsible for making a state update. I'm passing that function as props to the Modal component, and within it it's being passed into the ModalPanel component.
That function is used to set the state property, display, to false which will close the modal. Currently, that function is not working as intended.
Image Component:
class Images extends Component {
state = {
display: false,
activeIndex: 0
};
handleModalDisplay = activeIndex => {
this.setState(() => {
return {
activeIndex,
display: true
};
});
};
closeModal = () => {
this.setState(() => {
return { display: false };
});
}
render() {
const { imageData, width } = this.props;
return (
<div>
{imageData.resources.map((image, index) => (
<a
key={index}
onClick={() => this.handleModalDisplay(index)}
>
<Modal
closeModal={this.closeModal}
display={this.state.display}
activeIndex={this.state.activeIndex}
selectedIndex={index}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</Modal>
</a>
))}
</div>
);
}
}
export default Images;
Modal Component:
const overlayStyle = {
position: 'fixed',
zIndex: '1',
paddingTop: '100px',
left: '0',
top: '0',
width: '100%',
height: '100%',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.9)'
};
const button = {
borderRadius: '5px',
backgroundColor: '#FFF',
zIndex: '10'
};
class ModalPanel extends Component {
render() {
const { display } = this.props;
console.log(display)
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
</div>
);
return <div>{display ? overlay : null}</div>;
}
}
class Modal extends Component {
render() {
const {
activeIndex,
children,
selectedIndex,
display,
closeModal
} = this.props;
let modalPanel = null;
if (activeIndex === selectedIndex) {
modalPanel = (
<ModalPanel display={this.props.display} closeModal={this.props.closeModal} />
);
}
return (
<div>
{modalPanel}
{children}
</div>
);
}
}
export default Modal;
links to code
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Images.js
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Modal.js
You're dealing with this modal through a very non-react and hacky way.
Essentially, in your approach, all the modals are always there, and when you click on image, ALL modals display state becomes true, and you match the index number to decide which content to show.
I suspect it's not working due to the multiple children of same key in Modal or Modal Panel.
I strongly suggest you to ditch current approach. Here's my suggestions:
Only a single <Modal/> in Images component.
Add selectedImage state to your Images component. Every time you click on an image, you set selectedImage to that clicked image object.
Pass selectedImage down to Modal to display the content you want.
This way, there is only ONE modal rendered at all time. The content changes dynamically depending on what image you click.
This is the working code I tweaked from your repo:
(I'm not sure what to display as Modal content so I display public_id of image)
Images Component
class Images extends Component {
state = {
display: false,
selectedImage: null
};
handleModalDisplay = selectedImage => {
this.setState({
selectedImage,
display: true
})
};
closeModal = () => {
//shorter way of writing setState
this.setState({display: false})
}
render() {
const { imageData, width } = this.props;
return (
<div>
<Modal
closeModal={this.closeModal}
display={this.state.display}
selectedImage={this.state.selectedImage}
/>
{imageData.resources.map((image, index) => (
<a
//Only use index as key as last resort
key={ image.public_id }
onClick={() => this.handleModalDisplay(image)}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</a>
))}
</div>
);
}
}
Modal Component
class Modal extends Component {
render() {
const { display, closeModal, selectedImage } = this.props;
const overlayContent = () => {
if (!selectedImage) return null; //for when no image is selected
return (
//Here you dynamically display the content of modal using selectedImage
<h1 style={{color: 'white'}}>{selectedImage.public_id}</h1>
)
}
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
{
//Show Modal Content
overlayContent()
}
</div>
);
return <div>{display ? overlay : null}</div>;
}
}

Removing inputs leads to incorrect values

Ok, so the problem is pretty simple, but hard to explain.
I'm making an InputGenerator component, which generates a list of inputs.
Each generated input has a corresponding "remove" button next to it. The two elements (the input and the button) are wrapped in a div inside a map function. The div has a unique key prop. It looks like this (this is the whole component's jsx):
<div style={[InputGenerator.style.wrapper]}>
<div className="container" style={InputGenerator.style.container}>
{this.state.inputs.map((input, idx) => {
return (
<div key={idx} style={[
InputGenerator.style.row,
InputGenerator.count > 1 && idx > 0 ? InputGenerator.style.input.pushTop : {},
]}>
<InputText
id={input.id}
name={input.name}
placeholder={input.placeholder}
style={input.style}
/>
<Button
style={InputGenerator.style.remove}
type={Button.TYPES.BASIC}
icon="ion-close-round"
onClick={this.remove.bind(this, input.id)}
/>
</div>
);
})}
</div>
<div className="controls" style={InputGenerator.style.controls}>
<Button icon="ion-plus" type={Button.TYPES.PRIMARY} title="Add ingredient" onClick={this.add.bind(this)}/>
</div>
</div>
As you may see, all the inputs are kept in the this.state object and each one is given an unique id.
Here are the are add and remove methods:
add():
add() {
InputGenerator.count++;
const newInput = {
id: this.id,
name: this.props.name,
placeholder: this.props.placeholder,
style: this.style,
value: '',
};
const inputs = this.state.inputs;
inputs.push(newInput);
this.setState({ inputs });
}
remove():
remove(id) {
this.setState({
inputs: this.state.inputs.filter(i => i.id !== id),
});
}
The problem is:
I generate three inputs (using the add button)
I put random values in the inputs (e.g: 1, 2, 3)
I click on the remove button, corresponding to the first element (with value 1)
Result: Two input items with values 1 and 2
Expected: Two input items with values 2 and 3
The problem: I suggest that the key prop on the wrapping div is not enough for react to keep track of the input's values.
So, I'm open for ideas and suggestions how to proceed.
Here's an isolated sandbox to play around with my component and see the "bug" in action: https://codesandbox.io/s/5985AKxRB
Thanks in advance! :)
The issue you facing is because you are not handling state properly. You need to update state when you change input value.
handleChange(index,event) {
let inputs = this.state.inputs;
inputs[index].value = event.target.value;
this.setState({inputs:inputs})
}
DEMO : DEMO
Here is the updated code:
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
const styles = {
fontFamily: 'sans-serif',
textAlign: 'center',
};
const App = () =>
<div style={styles}>
<InputGenerator />
</div>;
class InputGenerator extends Component {
constructor() {
super();
this.state = {
inputs: [],
};
}
componentWillMount() {
this.add();
}
handleChange(index,event) {
let inputs = this.state.inputs;
inputs[index].value = event.target.value;
this.setState({inputs:inputs})
}
add() {
InputGenerator.count++;
const newInput = {
id: this.id,
name: this.props.name,
placeholder: this.props.placeholder,
style: this.style,
value: '',
};
const inputs = this.state.inputs;
inputs.push(newInput);
this.setState({ inputs });
}
get id() {
if (this.props.id) {
return `${this.props.id}-${InputGenerator.count}`;
}
return `InputGeneratorItem-${InputGenerator.count}`;
}
get style() {
return [];
}
remove(id) {
var state = this.state;
state.inputs = state.inputs.filter(i => i.id !== id);
this.setState({
inputs: state.inputs
});
}
render() {
return (
<div>
<div className="container">
{this.state.inputs.map((input, idx) => {
return (
<div key={idx}>
<input
id={input.id}
name={input.name}
value = {input.value}
onChange={this.handleChange.bind(this,idx)}
placeholder={input.placeholder}
/>
<button onClick={this.remove.bind(this, input.id)}>
Remove
</button>
</div>
);
})}
</div>
<div className="controls">
<button onClick={this.add.bind(this)}>Add</button>
</div>
</div>
);
}
}
InputGenerator.count = 0;
render(<App />, document.getElementById('root'));

Resources