I'm building a Modal component. This component takes modal content as children and the button to trigger the modal as a button prop.
This Modal component should render the button. When clicked it has to position a fixed element exactly on top of that button that then animates to a modal dialog. For this the Modal component needs a ref to the button DOM element to measure it's size and position with getBoundingClientRect.
I want the Modal component to be able to receive through button prop both
button DOM element or a custom React element that renders button.
The api of the component looks like this then
const ModalUser = () => (
<div>
<Modal button={<button>Button</button>}/>
<Modal button={<CustomButton>Button</CustomButton>}/>
</div>
)
The render method of Modal looks like this then
class Modal extends React.PureComponent {
render() {
return (
<div>
{React.cloneElement(this.props.button, {
ref: (el) => { this.button = el && el.button ? el.button : el },
onClick: this.onClick,
})}
<span>top: {this.state.top}</span>
<span>left: {this.state.left}</span>
</div>
);
}
}
And thus requires any CustomElement button to expose this.button as a ref to it's containing button.
class CustomButton extends React.PureComponent {
render() {
return (
<button
ref={(el) => { this.button = el }}
onClick={this.props.onClick}
>
<span>Custom</span>
<span>{this.props.children}</span>
</button>
)
}
}
For me this feels not optimal, but it works. I feel like there should be a more elegant solution to this. Does anyone have a suggestion how to do this better.
Here is a working demo
Codepen
Related
I am using React v18.1, react-bootstrap v2.4. I have a Modal component I am trying to get to display upon a button press. The modal component is quite simple:
class AdjustmentModal extends React.Component {
constructor(props) {
super(props);
this.state = {
'show': this.props.show
};
this.handleClose = this.handleClose.bind(this);
}
handleClose() {
this.setState({ show: false })
}
render() {
return (
<Modal show={this.state.show} onHide={this.handleClose}>
[ ... Modal Content Here ... ]
</Modal>
);
}
}
export default AdjustmentModal;
As you can see, I bind the modal's show property to the value of show in state.
Then, in the component in which I want to display my modal, I have the following:
// Within render() ...
<AdjustmentModal
show={this.state.showAdjustment}
partNo={this.state.partNo}
onHandQty={this.state.onHandQty}
/>
// Futher on in the code, display the modal on click:
<Button className="icon" onClick={this.handleDisplayAdjustment}>
<i className="bi bi-pencil-square"></i>
</Button>
handleDisplayAdjustment :
handleDisplayAdjustment(event) {
event.preventDefault();
this.setState({
showAdjustment : true
});
}
Now, despite the value showAdjustment in the parent component changing to true, the modal doesn't display.
I could set the <Modal show={this.props.show} .../> instead, but props are read-only, so there is no way to close the modal again if reading from props rather than state.
You can use props, which is a better way to handle this if you want to close it then pass a method from the parent which when called update the state in the parent to false and due state update the parent component will re render and though the child component that is the modal component and the Modal will get the updated value which will be false. below is the code on how you can achieve that.
closeModal() {
this.setState({
showAdjustment: false
})
}
// Within render() ...
<AdjustmentModal
show={this.state.showAdjustment}
partNo={this.state.partNo}
onHandQty={this.state.onHandQty}
onClose={this.closeModal.bind(this)}
/>
// Futher on in the code, display the modal on click:
<Button className="icon" onClick={this.handleDisplayAdjustment}>
<i className="bi bi-pencil-square"></i>
</Button>
For the child component
class AdjustmentModal extends React.Component {
handleClose() {
this.props.onClose()
}
render() {
return (
<Modal show={this.props.show} onHide={this.handleClose}>
[ ... Modal Content Here ... ]
</Modal>
);
}
}
export default AdjustmentModal;
EDIT: Explaining the approach
This will make your Modal component a Controlled component that is controlled by Parent, also updating props as a state inside the child component is not the right way, which may create potential bugs.
I have a toggle button that show and hides text. When the button is clicked I want it to hide another component and if clicked again it shows it.
I have created a repl here:
https://repl.it/repls/DapperExtrasmallOpposites
I want to keep the original show / hide text but I also want to hide an additional component when the button is clicked.
How to I pass that state or how do I create an if statement / ternary operator to test if it is in show or hide state.
All makes sense in the repl above!
To accomplish this you should take the state a bit higher. It would be possible to propagate the state changes from the toggle component to the parent and then use it in any way, but this would not be the preferred way to go.
If you put the state in the parent component you can use pass it via props to the needed components.
import React from "react";
export default function App() {
// Keep the state at this level and pass it down as needed.
const [isVisible, setIsVisible] = React.useState(false);
const toggleVisibility = () => setIsVisible(!isVisible);
return (
<div className="App">
<Toggle isVisible={isVisible} toggleVisibility={toggleVisibility} />
{isVisible && <NewComponent />}
</div>
);
}
class Toggle extends React.Component {
render() {
return (
<div>
<button onClick={this.props.toggleVisibility}>
{this.props.isVisible ? "Hide details" : "Show details"}
</button>
{this.props.isVisible && (
<div>
<p>
When the button is click I do want this component or text to be
shown - so my question is how do I hide the component
</p>
</div>
)}
</div>
);
}
}
class NewComponent extends React.Component {
render() {
return (
<div>
<p>When the button below (which is in another component) is clicked, I want this component to be hidden - but how do I pass the state to say - this is clicked so hide</p>
</div>
)
}
}
I just looked at your REPL.
You need to have the visibility state in your App component, and then pass down a function to update it to the Toggle component.
Then it would be easy to conditionally render the NewComponent component, like this:
render() {
return (
<div className="App">
{this.state.visibility && <NewComponent />}
<Toggle setVisibility={this.setVisibility.bind(this)} />
</div>
);
}
where the setVisibility function is a function that updates the visibility state.
I am trying to implement a collapsible component. I have designed it such as, on click of a button, a block of dynamic text will appear. I made a functional component and using the tags in a class. The name of the component is, CustomAccordion.jsx and using this component in Container.jsx
I have tried to create a button and a function for onClick event.
Part of the CustonAccordion.jsx
const handleToggle = () : string =>{
let content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
}else{
content.style.maxHeight = content.scrollHeight +'px';
}
}
export default function CustomAccordion(props: PropType): React.Component<*> {
const { title, children } = props
return(
<div>
<AccordionButton onClick={() => this.handleToggle()}>{title}</AccordionButton>
<AccordionContent>
<p>{children}
</p>
</AccordionContent>
</div>
)
}
Part of calling Container.jsx
<CustomAccordion title = {this.props.name}>
<p>This is the text passed to component.</p>
</CustomAccordion>
<br />
This does not show the expanded text and it seems that the click event does not work properly. I am very new in react, guessing the syntax might be incorrect.
In react you should generally try to avoid touching DOM directly unless you really have to.
Also you are accessing the handleToggle function wrongly. It should be onClick={() => handleToggle()} because this in your case is window/null and so it has no handleToggle method.
Instead you can use a stateful class component to achieve the same thing.
export default class CustomAccordion extends React.Component {
state = {show: false};
toggle = () => this.setState({show: !this.state.show});
render() {
const {title, children} = this.props;
const {show} = this.state;
return (
<div>
<AccordionButton onClick={this.toggle}>{title}</AccordionButton>
{show && (
<AccordionContent>
<p>{children}</p>
</AccordionContent>
)}
</div>
)
}
}
If you want to have some kind of animation, you can set different className based on the show state instead of adding/removing the elements.
I'm sure this is something trivial but I can't seem to figure out how to access the value of my button when the user clicks the button. When the page loads my list of buttons renders correctly with the unique values. When I click one of the buttons the function fires, however, the value returns undefined. Can someone show me what I'm doing wrong here?
Path: TestPage.jsx
import MyList from '../../components/MyList';
export default class TestPage extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleButtonClick(event) {
event.preventDefault();
console.log("button click", event.target.value);
}
render() {
return (
<div>
{this.props.lists.map((list) => (
<div key={list._id}>
<MyList
listCollection={list}
handleButtonClick={this.handleButtonClick}
/>
</div>
))}
</div>
);
}
}
Path: MyListComponent
const MyList = (props) => (
<div>
<Button onClick={props.handleButtonClick} value={props.listCollection._id}>{props.listCollection.title}</Button>
</div>
);
event.target.value is for getting values of HTML elements (like the content of an input box), not getting a React component's props. If would be easier if you just passed that value straight in:
handleButtonClick(value) {
console.log(value);
}
<Button onClick={() => props.handleButtonClick(props.listCollection._id)}>
{props.listCollection.title}
</Button>
It seems that you are not using the default button but instead some sort of customized component from another libray named Button.. if its a customezied component it wont work the same as the internatls might contain a button to render but when you are referencing the event you are doing it throug the Button component
Some component contains a Tabs with Tabs:
class App {
render() {
<div>
<p>Tabs here:</p>
<Tabs>
<Tab name="Page 1"> .. content here .. </Tab>
<Tab name="Page 2"> .. content here .. </Tab>
<Tab name="Page 3"> .. content here .. </Tab>
</Tabs>
</div>
}
}
Tabs is responsible for most of the markup:
class Tabs {
click() {
// I want the Tab component, because I want to tab.setState()
// `this` is not a `Tab` object
}
render() {
return (
( print the tab labels: )
<ul>
{ this.props.children.map(tab =>
// >>> `tab` is not a `Tab` object <<<
<li><a onClick={ this.click.bind(tab) } href="#">{ tab.props.name }</a></li>
) }
</ul>
( print the tab content: )
{ this.props.children }
);
}
}
A Tab itself is very little, maybe nothing:
const Tab = ({children}) => <div>{ children }</div>;
How does Tabs.click know which Tab was clicked. If I click.bind(tab) it's the child object, not the Tab object. If I click.bind(this) it's the Tabs object.
This might be a very longwinded way of asking why props.children don't contain the Component objects, but a kind of proxy (?) child object. It does have the props, but not the methods, state etc.
edit 1:
In this example, I'd like to let Tab decide how to render itself: once as tab link and once as tab content. Tabs would be responsible for calling the render methods, but Tab would know how. As you can see in Tabs.render() there are 2 renders. It'd be nice if it could do this:
<ul>
{ this.props.children.map(tab => tab.renderLink()) }
</ul>
{ this.props.children.map(tab => tab.renderContent()) }
or just
{ this.props.children }
because content is the normal render
But Tabs can't do that, because it doesn't have Tab objects for children... Why?
Well, there are a few methods to achieve this. To start, you're right - since you're using ES6 arrow functions in the map, this isn't rebound - it's "inherited" from the enclosing scope (I think, technically, it's less inherited and more just left alone - unchanged).
First, instead of binding in the render method, bind click in the constructor.
constructor(props) {
super(props);
this.click = this.click.bind(this);
}
or use ES6 Arrow Functions
click = (event) => {...}
Although this doesn't directly solve your issue, it cleans up the context mess that arises from using bound callbacks.
From there, you can get the clicked tab by using event.target.
You can also use partials - the bind method accepts additional parameters which are prepended to the parameters of the bound function. That would look something like this:
render (
<div>
{ this.props.children.map((tab) => {
<a <li><a onClick={ this.click.bind(null, tab) } href="#">{ tab.props.name }</a></li>
}) }
);
you would have to adjust your click method to accept tab as well: click(tab, event)
I also don't think it's wise, or even recommended to mess with a component's state from another component. Why aren't you using the Tab component? Pass in Tabs onClick as a prop and handle click events like this...
class Tab extends React.Component {
onClick = (event) => {
this.setState({...}); // Tab onClick
if (this.props.onClick) this.props.onClick(); // Tabs onClick
}
}
edit: Working example
Tabs class
class Tabs extends React.Component {
onTabClick = (tab) => {
tab.setState({ test: true });
}
render() {
return (
<div>
{ this.props.children.map((tab) => {
return React.cloneElement(tab, {
onClick: this.onTabClick
});
}) }
</div>
);
}
}
Tab class
class Tab extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
onClick = (event) => {
console.log(event);
event.preventDefault();
if (this.props.onClick) {
this.props.onClick(this);
}
}
render() {
return <div>{ !this.state.test && <a href={ this.props.url } onClick={ this.onClick }>{ this.props.label }</a> }</div>;
}
}
example tabs
<Tabs>
<Tab url="http://www.google.com" label="Google" />
<Tab url="http://www.google.com" label="Google" />
</Tabs>
although it begs the question again - why not just set the state inside of the Tab class? Why do you need to set the state of a child from within a parent? Can that state not be maintained by the parent (using this.setState(...) and passed into the child as a prop?