Accessing parent method from a component received as a prop - javascript

I'm trying to build a reusable Confirmation component that renders a button and when clicked, it should open a Material UI Dialog. The button component gets passed in as a prop for the Confirmation component
<Confirmation component={() => <MUIButton>Click me</MUIButton>} />
The parent component looks like this
const Confirmation = ({ component: Component }) => {
const handleClick = () => {
...logic to open the dialog...
}
return (
<>
<Component onClick={handleClick} <-- how to trigger this? />
<Dialog />
</>
)
}
Now how would I get this to work without having to specify the onClick in the passed button component itself? For this situation one can assume the component passed as a prop is always some kind of a button.
<Confirmation
component={() => (
<MUIButton
onClick={...logic} <-- don't want to have to specify this
>
Click me
</MUIButton>
)
/>
OR am I approaching this from a wrong perspective? Should this be solved by passing the button as a child instead? As in
<Confirmation>
<MUIButton> Click me </MUIButton>
</Confirmation>
and how would the implementation be in this situation?
Thank you in advance!

Ended up solving this by creating a higher order component as suggested by John
const withConfirmation = (WrappedComponent) => {
return (props) => {
return (
<>
<WrappedComponent
// Overrides the possible onClick logic passed as a prop
onClick={ ...dialog opening logic }
{...props}
/>
<Dialog>
...
</Dialog>
</>
);
};
};
const Confirmation = withConfirmation(MuiButton)
<Confirmation>Clicking me opens a dialog</Confirmation>

Related

How to have all component logic inside of it and export it to parent components

I have a modal component that is being used to display a form for the user. This modal accepts three parameters: is_visible (if the modal is shown), toggleModal (state function for managing ifthe modal will show or not), passport_id (irrelevant here). This component is used inside another component, the passport component bellow. This passport component is where the state that manages the visibility of modal is created, and then, as said above, the state function and state variable are passed down to the modal (child component).
This works fine, but it seems wrong to have part of the functionality of the modal defined on its parent. I believe that the state that controls wether the modal will show or not should be defined inside the modal component and then, if it's necessary to use these functions ouside the modal, they should be exported. I've tried this by defining a hook inside the modal and exporting to the passport component, but it didn't work because the state variable inside the modal was not updating.
So my question is if there's a proper way to apply the idea of "Tell, don't ask" (https://martinfowler.com/bliki/TellDontAsk.html) here, making it possible to tell the component to do something instead of passing down data. Or, in other words, put the logic of the modal only inside the modal component
Modal component:
const TransferPassportModal = ({is_visible, toggleModal, passport_id}) => {
const [input_text, setInputText] = useState(''),
[loading, setLoading] = useState(false);
function closeModal() {
toggleModal(false);
}
return (
<Modal animationType="fade" transparent={true} visible={is_visible}>
<View style={styles.modal}>
<View style={styles.modal_content}>
<TouchableOpacity
onPress={closeModal}
style={styles.close_modal_button}>
<Icons name="close" size={20} />
</TouchableOpacity>
// Rest of code
</View>
</View>
</Modal>
);
};
export default TransferPassportModal;
Passport component:
const Passport = ({passport}) => {
const [toggleModal, setToggleModal] = useState(false);
function openModal() {
setToggleModal(!toggleModal);
}
return (
<View style={styles.wrapper}>
<View style={styles.infos_wrapper}>
<Text style={styles.name}>{passport.nome}</Text>
<Text style={styles.text}>{passport.codigo}</Text>
</View>
<View style={styles.buttons_wrapper}>
// Resto of code
<TransferPassportModal
is_visible={toggleModal}
passport_id={passport.ingresso}
toggleModal={setToggleModal}
/>
</View>
</View>
);
};
export default Passport;
I lately do this like so:
export const OpenModalAction = () =>{
return (
<>
<Button/>
<Modal> …. <Modal/>
</>
)
}
So the button and modal is one component

How can I pass function call in a component as a prop in React Native?

I am trying to pass my function updateProfile as a prop in the Button component shown below. The function is not working and the application crashes when I press the button.
Here is the code of button component.
const Button = (props) => {
return (
<>
<TouchableOpacity
style={styles.buttonContainer}
disabled={props.disabled}
onPress={props.functionCall}
>
<Text style={styles.buttonText}>{props.title}</Text>
</TouchableOpacity>
</>
);
};
And here is how I am using
<Button disabled={isLoading ? true : false} functionCall="updateProfile" title="Edit Profile"/>
The function
const updateProfile = () => {
navigation.navigate("updateProfile", member);
};
Actually, I have many buttons and each button has different function call. So, it needs to be sented through props. Help needed!
Change this:
functionCall="updateProfile"
to this:
functionCall={updateProfile}

How to render React Route component in an entirely new, blank page

I'm trying to render a print page using React Router. So I have two components:
export default class PurchaseOrder extends React.Component{
....
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRoute>
<Button
component={Link}
to="/order/print"
target="_blank"
>
Print
</Button>
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</BrowserRoute>
}
}
And the OrderPrint:
export default function OrderPrint(props) {
return (
<div>props.orderDate</div>
<div>props.client.name</div>
);
}
As you can see, I'm trying to present the printable version of the purchase order with a click of a button. The OrderPrint component gets rendered, but it's rendered right below the button. I could put the Route inside my root component, which is App, that way making sure that I get only the contents of the OrderPrint component rendered like this:
class App extends Component {
render() {
return (
<div className="App">
<Router>
<Route exact path="/" component={PurchaseOrder} />
<Route exact path="/order/print" component={OrderPrint} />
</Router>
</div>
);
}
}
But in that case, I won't be able to pass the necessary props to it. So in this particular case, how to replace entire page content with the contents of OrderPrint component and still be able to pass the necessary input to it?
Update
As #Akalanka Weerasooriya mentioned in comments, I could have the entire state kept in the App component. But one thing stopped me from doing this: This means I'll practically always have to use the render prop of the Route component, instead of the component prop. Ok, that's not a problem, but if it's the way to go, then why does React Router documentation almost always use the
<Route path="/about" component={About} />
pattern as the standard way of using it? So to recap it, if I go the Single Source of Truth way and store all my state in one place, then doesn't it mean that I will always use
<Route path="/about" render={props=>(<div>props.someProp</div>)} />
I don't say there's a problem with it, it's just mentioning it in the documentation only after component={SomeComponent} pattern confuses me.
Not sure why you need a different route for a print page, but anyway if you want it on a new empty page, you can take advantage of the ReactDOM.createPortal feature.
You can create a new page and or even a new window using window.open while keeping the flow of react data in sync.
Here is a running example of a portal on a new window with live state updates from the component that triggered this window using a portal:
running example, i'm sharing an external snippet and not using stack-snippets here because window.open returns null in the contexts of stack-snippets
Source code:
class WindowPortal extends React.PureComponent {
containerEl = document.createElement("div");
externalWindow = null;
componentDidMount() {
const { width = 450, height = 250, left = 150, top = 150 } = this.props;
const windowFetures = `width=${width},height=${height},left=${left},top=${top}`;
this.externalWindow = window.open("", "", windowFetures);
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
this.externalWindow.close();
}
render() {
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
class App extends React.PureComponent {
state = {
counter: 0,
showWindowPortal: false
};
componentDidMount() {
window.setInterval(() => {
this.setState(state => ({
counter: state.counter + 1
}));
}, 1000);
}
toggleWindowPortal = () => {
this.setState(state => ({
...state,
showWindowPortal: !state.showWindowPortal
}));
};
closeWindowPortal = () => {
this.setState({ showWindowPortal: false });
};
render() {
return (
<div>
<h1>Counter: {this.state.counter}</h1>
<button onClick={this.toggleWindowPortal}>
{this.state.showWindowPortal ? "Close the" : "Open a"} Portal
</button>
{this.state.showWindowPortal && (
<WindowPortal closeWindowPortal={this.closeWindowPortal}>
<h2>We are in a portal on a new window</h2>
<h3>{`This is the current state: ${this.state.counter}`}</h3>
<p>different window but sharing the state!!</p>
<button onClick={() => this.closeWindowPortal()}>Close me!</button>
</WindowPortal>
)}
</div>
);
}
}
here you have a PrivateRoute which is a custom route which holds a header and header is rendered in PrivateRoute routes only so when you try to navigate to new route like path="/order/print" then you won't get header which has button in it.
function Header(props) {
return (
<div>
<Button
component={Link}
to="/order/print"
target="_blank">
Print</Button>
{props.children}
</div>
)
}
const PrivateRoute = ({ component: Component, layout: Layout, ...rest }) => {
return <Route {...rest} render={props => {
return <Layout>
<Component {...props} />
</Layout>
}} />
}
export default class PurchaseOrder extends React.Component{
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRouter>
<div>
<PrivateRoute exact path="/" layout={Header} component={Landing} />
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</div>
</BrowserRouter>
}
}

Render button in child component only on certain screen

Im using a flat list on 2 different screens.
On the EventListScreen:
this is the main screen and should display all events.
and on the 2nd page UserProfile.js this page should only display that users events.
in both flat lists I'm using a pure component stored in a seperate class, to where the flat lists are i.e
My Question is, I want to display an "Edit" button on the Event.js child component only if the User is on the
UserProfileScreen.js
I have looked up a lot of example but cant really find any that show how to do it
with a child pure component like I'm doing.
Any Help would be greatly appreciated! Thank you
EventListScreen.js
<FlatList
data={this.state.events}
// Get the item data by referencing as a new function to it
renderItem={({item}) =>
<Event
openEventDetail={() => this.openEventDetail(item)}
{...item}
/>}
/>
UserProfileScreen.js
<FlatList
data={this.state.events}
// Get the item data by referencing as a new function to it
renderItem={({item}) =>
<Event
openEventDetail={() => this.openEventDetail(item)}
openEditEvent={() => this.openEditEvent(item)}
{...item}
/>}
/>
Event.js
export default class Event extends Component {
render() {
return (
<Card>
<CardSection>
<Text>{this.props.eventName}</Text>
//I want this button to be displayed only if user is viewing
//from the UserProfile.js
<Button onPress={() =>this.props.openEditEvent()}>
{this.props.displayButton}
</Button>
</CardSection>
<TouchableOpacity
onPress={() => this.props.openEventDetail()}
>
}
You don't need additional properties.
We can assume that the "Edit" button should be available when openEditEvent prop is defined.
Condition in event (using convertion to bool, false for undefined):
<CardSection>
<Text>{this.props.eventName}</Text>
{!!this.props.openEditEvent &&
<Button onPress={() =>this.props.openEditEvent()}>
{this.props.displayButton}
</Button>
}
</CardSection>
Use propTypes to define openEditEvent prop as a function, optional (not required).
If I understand your problem correctly an option to solve this problem would be to pass a boolean "showable prop" to show the edit button only when required:
EventListScreen.js (Stays the same, we don't show the edit button here)
<FlatList
data={this.state.events}
// Get the item data by referencing as a new function to it
renderItem={({item}) =>
<Event
openEventDetail={() => this.openEventDetail(item)}
{...item}
/>}
/>
UserProfileScreen.js (we add the shouldShowEditButton prop to event in order to show the button)
<FlatList
data={this.state.events}
// Get the item data by referencing as a new function to it
renderItem={({item}) =>
<Event
openEventDetail={() => this.openEventDetail(item)}
openEditEvent={() => this.openEditEvent(item)}
shouldShowEditButton
{...item}
/>}
/>
Event.js (We add some propTypes and defaultProps to handle the new prop, it won't show the edit button if not specified)
export default class Event extends Component {
render() {
return (
<Card>
<CardSection>
<Text>{this.props.eventName}</Text>
//I want this button to be displayed only if user is viewing
//from the UserProfile.js
{this.props.shouldShowEditButton && <Button onPress={() =>this.props.openEditEvent()}>
{this.props.displayButton}
</Button>}
</CardSection>
<TouchableOpacity
onPress={() => this.props.openEventDetail()}
>
...
...
);
...
}
}
// We add some default propTypes and definitions
Event.propTypes = {
shouldShowEditButton: PropTypes.bool
};
Event.defaultProps = {
shouldShowEditButton: false
};
In this way you're only showing the edit button for the components that have the prop shouldShowEditButton defined, and because its default value is defined as false, the components that don't have the property will behave in the same way they were before.

Passing props to a react component wrapped in withRouter() function

I am using React-Router v4 to navigate in my React app. The following is a component wrapped in the withRouter() function to make it able to change route on click:
const LogoName = withRouter(({history, props}) => (
<h1
{...props}
onClick={() => {history.push('/')}}>
BandMate
</h1>
));
As you can see I pass the props to the component, which I need in order to change the class of the component. The problem here is that props is undefined in the <LogoName> component. I need to be able to change the class of this component when I click on another component, like this:
<LogoName className={this.state.searchOpen ? "hidden" : ""} />
<div id="search-container">
<SearchIcon
onClick={this.handleClick}
className={this.state.searchOpen ? "active" : ""} />
<SearchBar className={this.state.searchOpen ? "active" : ""}/>
</div>
Here is how I handle the click. Basically just setting the state.
constructor(){
super();
this.state = {
searchOpen: false
}
}
handleClick = () => {
this.setState( {searchOpen: !this.state.searchOpen} );
}
Is there a way for me to pass props to a component that is wrapped inside the withRouter() function or is there a similar way to create a component which has the ability to navigate with React-Router and still receive props?
Thanks in advance.
The problem is that while destructuring, you want to destructure props but you are not passing any prop named props to LogoName component
You can change your argument to
const LogoName = withRouter((props) => (
<h1
{...props}
onClick={() => {props.history.push('/')}}>
BandMate
</h1>
));
However you can still destructure the props like #Danny also suggested by using the spread operator syntax like
const LogoName = withRouter(({history, ...props}) => (
<h1
{...props}
onClick={() => {history.push('/')}}>
BandMate
</h1>
));
You're close, just spread the props in your function signature as well:
const LogoName = withRouter(({ history, ...props }) => (
<h1
{...props}
onClick={() => {history.push('/')}}>
BandMate
</h1>
));
This worked for me:
import {withRouter} from 'react-router-dom';
class Login extends React.Component
{
handleClick=()=>{
this.props.history.push('/page');
}
render()
{
return(
<div>
.......
<button onClick={this.handleClick()}>Redirect</button>
</div>);
}
}
export default withRouter(({history})=>{
const classes = useStyles();
return (
<Login history={history} classes={classes} />
)
});

Categories