Confirming navigation with with custom dialog using setRouteLeaveHook - javascript

I am trying to use a custom dialog to ask a user for confirmation before navigating away with unsaved data.
Following the docs I have:
componentDidMount() {
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
)
}
But instead of
routerWillLeave(nextLocation) {
if (!this.props.pristine) {
return 'You have unsaved information, are you sure you want to leave this page?'
}
I have
routerWillLeave(nextLocation) {
if (!this.props.pristine) {
this.setState({open: true})
this.forceUpdatate() //necessary or else render won't be called to open dialog
}
The dialog component I am using comes from material-ui which just expects an open boolean to control the dialog, it also takes a handleCancel and handleContinue methods, but I am not sure how to hook it up with routerWillLeave.
The handleCancel method is simple as it just closes the dialog:
handleCancel() {
this.setState({open: false})
};
I have wrapped the dialog component in component called Notification
export default class Notification extends React.Component {
render() {
const { open, handleCancel, handleContinue } = this.props
const actions = [
<FlatButton
label="Cancel"
primary={true}
onTouchTap={handleCancel}
/>,
<FlatButton
label="Continue"
primary={true}
onTouchTap={handleContinue}
/>,
];
return (
<div>
<Dialog
actions={actions}
modal={false}
open={open}
>
You have unsaved data. Discard changes?
</Dialog>
</div>
);
}
}
And I can call it from the parent component, I have this in the render method:
<Notification open={open} handleCancel={this.handleCancel} handleContinue={this.handleContinue}/>
Basically my question is how can I wire this up with routerWillLeave instead of showing the native browser alert?

When you call createHistory, one of the options to it is getUserConfirmation, which takes a prompt message and a callback. For DOM histories (browserHistory and hashHistory), getUserConfirmation calls window.confirm, passing it the message. The callback function receives the return value of window.confirm [0].
What you need to do is to provide your own getUserConfirmation method that replicates window.confirm. When it gets called, you should display your modal and trigger the callback depending on which button is clicked.
Notification.js
The <Notification> component should take the prompt message and the callback function to call based on the user's action.
class Notification extends React.Component {
contructor(props) {
this.state = {
open: false
}
}
handleCancel() {
this.props.callback(false)
this.setState({ open: false })
}
handleContinue() {
this.props.callback(true)
this.setState({ open: false })
}
render() {
const { message } = this.props
const { open } = this.state
const actions = [
<FlatButton
label="Cancel"
primary={true}
onTouchTap={this.handleCancel.bind(this)}
/>,
<FlatButton
label="Continue"
primary={true}
onTouchTap={this.handleContinue.bind(this)}
/>,
];
return (
<div>
<Dialog
actions={actions}
modal={true}
open={open}
>
{message}
</Dialog>
</div>
);
}
}
ModalConfirmation.js
The confirmation modal isn't really a part of your UI, which is why I am rendering it in a separate render process than the rest of the app.
import React from 'react'
import ReactDOM from 'react-dom'
import Notification from './components/Notification'
export default function = (holderID) => {
var modalHolder = document.getElementById(holderID)
return function ModalUserConfirmation(message, callback) {
ReactDOM.render((
<Notification open={true} message={message} callback={callback} />
), modalHolder)
}
}
This will obviously force you to create your own history object. You cannot just import browserHistory or hashHistory because those use window.confirm. Luckily it is trivial to create your own history. This is essentially the same code as is used in browserHistory [1], but it passes createBrowserHistory your getUserConfirmation function.
createConfirmationHistory.js
import createBrowserHistory from 'history/lib/createBrowserHistory'
import createRouterHistory from './createRouterHistory'
export default function(getUserConfirmation) {
return createRouterHistory(createBrowserHistory({
getUserConfirmation
})
}
index.js
Finally, you need to put this all together.
import createHistory from './createConfirmationHistory'
import ModalConfirmation from './ModalConfirmation'
const getModalConfirmation = ModalConfirmation('modal-holder')
const history = createHistory(getModalConfirmation)
ReactDOM.render((
<Router history={history}>
// ...
</Router>
), document.getElementById('root')
You would have to refactor this a bit if you wanted to use a history singleton, but otherwise it should work. (I haven't actually tested it, though).
[0] https://github.com/mjackson/history/blob/v2.x/modules/DOMUtils.js#L38-L40
[1] https://github.com/ReactTraining/react-router/blob/master/modules/browserHistory.js

You can try using react-router-navigation-prompt.
This worked for me.

I did this using Prompt from react-router-dom with a custom modal from antd. Material UI should have very similar functionality.
In my index.js I set up my dialog in getUserConfirmation of the Router:
import { MemoryRouter as Router } from 'react-router-dom'
import { Modal } from 'antd'
import App from './App'
const { confirm } = Modal
const confirmNavigation = (message, callback) => {
confirm({
title: message,
onOk() {
callback(true)
},
onCancel() {
callback(false)
}
})
}
ReactDOM.render(
<Router getUserConfirmation={confirmNavigation}>
<App />
</Router>
document.getElementById('root')
)
Then you use in it the component you want to prompt if you try to navigation away, using the when prop to give a condition to pop up the modal.
import { Prompt } from 'react-router-dom'
class MyComponent extends React.Component {
render() {
return (
<div>
<Prompt
when={myDataHasNotSavedYet}
message="This will lose your changes. Are you sure want leave the page?"
/>
<RestOfMyComponent/>
</div>
)
}
}

Related

Gatsby Context API doesn't render its value

Trying to render state from Context API, but in console it shows as undefined and doesn't render anything.
here is Context file
import React, { useReducer, createContext } from "react"
export const GlobalStateContext = createContext()
export const GlobalDispatchContext = createContext()
const initialState = {
isLoggedIn: "logged out",
}
function reducer(state, action) {
switch (action.type) {
case "TOGGLE_LOGIN":
{
return {
...state,
isLoggedIn: state.isLoggedIn === false ? true : false,
}
}
break
default:
throw new Error("bad action")
}
}
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<GlobalStateContext.Provider value={state}>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalContextProvider
and here is where value should be rendered
import React, { useContext } from "react"
import {
GlobalStateContext,
GlobalDispatchContext,
} from "../context/GlobalContextProvider"
const Login = () => {
const state = useContext(GlobalStateContext)
console.log(state)
return (
<>
<GlobalStateContext.Consumer>
{value => <p>{value}</p>}
</GlobalStateContext.Consumer>
</>
)
}
export default Login
I tried before the same thing with class component but it didn't solve the problem. When I console log context it looks like object with undefined values.
Any ideas?
The Context API In General
From the comments, it seems the potential problem is that you're not rendering <Login /> as a child of <GlobalContextProvider />. When you're using a context consumer, either as a hook or as a function, there needs to be a matching provider somewhere in the component tree as its parent.
For example, these would not work:
<div>
<h1>Please log in!</h1>
<Login />
</div>
<React.Fragment>
<GlobalContextProvider />
<Login />
</React.Fragment>
because in both of those, the Login component is either a sibling of the context provider, or the provider is missing entirely.
This, however, would work:
<React.Fragment>
<GlobalContextProvider>
<Login />
</GlobalContextProvider>
</React.Fragment>
because the Login component is a child of the GlobalContextProvider.
Related To Gatsby
This concept is true regardless of what library or framework you're using to make your app. In Gatsby specifically there's a little bit of work you have to do to get this to work at a page level, but it's possible.
Let's say you have a Layout.jsx file defined, and the following page:
const Index = () => (
<Layout>
<h1>{something that uses context}</h1>
</Layout>
)
You have 2 options:
The easier option is to extract that h1 into its own component file. Then you can put the GlobalContextProvider in the Layout and put the context consumer in the new component. This would work because the h1 is being rendered as a child of the layout.
Is to do some shuffling.
You might be inclined to put the Provider in the layout and try to consume it in the page. You might think this would work because the h1 is still being rendered as a child of the Layout, right? That is correct, but the context is not being consumed by the h1. The context is being rendered by the h1 and consumed by Index, which is the parent of <Layout>. Using it at a page level is possible, but what you would have to do is make another component (IndexContent or something similar), consume your context in there, and render that as a child of layout. So as an example (with imports left out for brevity):
const Layout = ({children}) => (
<GlobalContextProvider>
{children}
</GlobalContextProvider>
);
const IndexContent = () => {
const {text} = useContext(GlobalStateContext);
return <h1>{text}</h1>;
}
const Index = () => (
<Layout>
<IndexContent />
</Layout>
);

Odd behavior with react-modal

I'm trying to build a quiz that uses react-modal to provides hints. I will need multiple modals inside the quiz. I'm new to React so it's quite possible that I'm making a simple mistake.
I'm not sure it matters, but I've built this using create-react-app.
My App.js looks like this:
import React, { Component } from 'react';
import HintModal from './hintModal';
import Modal from 'react-modal';
import './App.css';
Modal.setAppElement('#root');
class App extends Component {
state = {
modalIsOpen: false,
hint: ''
};
openModal = (hint) => {
this.setState({ modalIsOpen: true, hint: hint });
}
closeModal = () => {
this.setState({ modalIsOpen: false, hint: '' });
}
render() {
return (
<React.Fragment>
<h1>Modal Test</h1>
<h2>First Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="mango"
/>
<hr />
<h2>Second Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="banana"
/>
</React.Fragment>
);
}
}
export default App;
hintModal.jsx looks like this:
import React, { Component } from 'react';
import Modal from 'react-modal';
const HintModal = (props) => {
const {openModal, modalIsOpen, closeModal, hint} = props;
return (
<React.Fragment>
<button onClick={ () => openModal(hint) }>Open Modal</button>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Example Modal"
>
<h2>Hint</h2>
<p>{hint}</p>
<button onClick={closeModal}>Close</button>
</Modal>
<p>We should see: {hint}</p>
</React.Fragment>
);
};
export default HintModal;
Here's the problem: I need the content of the modal to change based on the hint prop passed to HintModal. When I output hint from outside <Modal>, it behaves as expected, displaying the value of the prop. But when I output hint within <Modal>, it returns "banana" (the value of the hint prop for the second instance of HintModal) when either modal is activated.
Any help would be greatly appreciated!
You are controlling all of your modals with the same piece of state and the same functions to open and close the modal.
You need to either have just one modal and then dynamically render the message inside it or you need to store a modalIsOpen variable in your state for every single modal.

How to call React's render method() from another component?

A client request a feature to implement dashboard switching. I'm working on it:
Dashboard.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
// components
import UserDashboard from '../components/dashboard/user-dashboard/UserDashboard.js';
import NewUserDashboard from '../components/new-dashboard/user-dashboard/NewUserDashboard.js';
#connect((state) => {
return {
identity: state.identity.toJS().profile
};
})
export default class Dashboard extends Component {
render() {
const msisdn = this.props.location.state ? this.props.location.state.msisdn : null;
return (
<UserDashboard msisdn={ msisdn }/>
);
}
}
Dashboard.js is the dashboard controller. I have 2 dashboards: UserDashboard, and NewDashboard.
Let's say an user is viewing another screen, and in that screen there's a button. If that button is clicked, the Dashboard will call it's render method, returning NewDashboard instead. And NewDashboard will be automatically displayed. Is this possible?
Calling render method programmatically not possible.
You have to do state update of that particular component if you want to call render method of that component.
Say,if you want to call render method of Dashboard Component,you must call setState on this component. You can do some dummy state lifting for that.
Imagine you have this dashboard:
function DashBoard({index}) {
return index == 0 ? <UserDashBoard /> : <SecondDashBoard />;
}
Without a router:
class ParentComponent extends ReactComponent {
state = {
dashboardIndex: 0
}
changeDashboard() {
this.setState({
dashBoardIndex: (state.dashboardIndex + 1) % 2
})
}
render() {
return (
<div>
<button onclick={() => this.changeDashboard()}>Change dashboard</button>
<Dashboard index={this.state.dashboardIndex} />
</div>
)
}
}
With a router:
<Switch>
<Route match="/component1" component={UserDashboard} />
<Route match="/component2" component={SecondDashboard} />
</Switch>
Also you can use redux.
You can use conditional rendering using state.
You can keep track of currently active tab and use that state to render the desired component.
More often than not, in order to change page views, you would make use of Router. You can configure Routes corresponding to Dashboard
import UserDashboard from '../components/dashboard/user-dashboard/UserDashboard.js';
import NewUserDashboard from '../components/new-dashboard/user-dashboard/NewUserDashboard.js';
#connect((state) => {
return {
identity: state.identity.toJS().profile
};
})
export default class Dashboard extends Component {
render() {
const msisdn = this.props.location.state ? this.props.location.state.msisdn : null;
return (
<BrowserRouter>
<Route path="/dashboard/user" render={(props) => <UserDashboard msisdn={ msisdn } {...props}/>} />
<Route path="/dashboard/new" render={(props) => <NewUserDashboard msisdn={ msisdn } {...props}/>} />
</BrowserRouter>
);
}
}
and on button click you can use a link.
Or else you can conditionally render component based on state change
// components
import UserDashboard from '../components/dashboard/user-dashboard/UserDashboard.js';
import NewUserDashboard from '../components/new-dashboard/user-dashboard/NewUserDashboard.js';
#connect((state) => {
return {
identity: state.identity.toJS().profile
};
})
export default class Dashboard extends Component {
state = {
userDashboard: true
}
onToggle=(state)=> {
this.setState(prevState => ({
userDashboard: !prevState.userDashboard
}))
}
render() {
const msisdn = this.props.location.state ? this.props.location.state.msisdn : null;
return <div>{userDashboard? <UserDashboard msisdn={ msisdn }/>
: <NewUserDashboard msisdn={ msisdn }/>}
<button onClick={this.onToggle}>Toggle</button>
</div>
);
}
}
Probably something like:
class NewDashboard extends React.Component {
static triggerRender() {
this.forceUpdate();
}
// or
static altTriggerRender() {
this.setState({ state: this.state });
}
render() {...}
}
Force React Component Render
Though, it's better to show/hide other components by conditional rendering.
Update:
"This" is not accessible inside a static method. Ignore the code.

How to connect component in html props in react-tippy to redux form

I have got a problem with react-tippy component. I would like to pass in html props react component with redux form, but I receive an error like this:
Uncaught Error: Could not find "store" in either the context or props of "Connect(Form(AssignDriverForm))". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Form(AssignDriverForm))".
Here is code of react tippy component:
class AssignDriverToolTip extends Component {
state = { open: false }
setIsOpen = () => this.setState({ open: true });
setIsClose = () => this.setState({ open: false });
render() {
return (
<CustomTooltip
theme="light"
open={this.state.open}
arrow={false}
html={(
<AssignDriverForm/>
)}
position='right'
trigger="click" >
<CustomButton infoSmallWithIcon onClickHandler={() => this.setIsOpen()}>
<SVGComponent icon="pen" color="white" width="12px" height="12px" />
{strings.assignDriver}
</CustomButton>
</CustomTooltip>
)
}
}
export default AssignDriverToolTip;
And also here is an AssignDriverForm component:
class AssignDriverForm extends Component {
handleSubmit = (data) => {
console.log(data);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<Field
name="message"
type="textarea"
autoComplete="off"
component="textarea" />
</form>
)
}
}
AssignDriverForm = reduxForm({
form: 'assignDriver'
})(AssignDriverForm)
export default AssignDriverForm;
When I change it to component without redux-form everything works fine. But I really dont know how to fix it. Could you help me ?
As the error message says: find the root component where you create your store. (Something like this in index.jx)
const store = createStore(reducer)
Then make sure you wrap your component in the <Provider> wrapper.
// render app
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
)
Redux form uses a normal Redux store, so somewhere you need to add that reducer to your main reducer. You may have something like this:
// combine reducers
const reducer = combineReducers({
myReducer,
...
form: formReducer // for redux-form
})

React Native - navigation issue "undefined is not an object (this.props.navigation.navigate)"

Im following this tutorial https://reactnavigation.org/docs/intro/ and im running into a bit of issues.
Im using the Expo Client app to render my app every time and not a simulator/emulator.
my code is seen down below.
I originally had the "SimpleApp" const defined above "ChatScreen" component but that gave me the following error:
Route 'Chat' should declare a screen. For example: ...etc
so I moved the decleration of SimpleApp to just above "AppRegistry" and that flagged a new error
Element type is invalid: expected string.....You likely forgot to export your component..etc
the tutorial did not add the key words "export default" to any component which I think it may have to do with the fact that im running it on the Expo app? so I added "export default" to "HomeScreen" and the error went away.
The new error that I cant seem to get rid off(based on the code below) is the following:
undefined is not an object (evaluating 'this.props.navigation.navigate')
I can't get rid of it unless I remove the "{}" around "const {navigate}" but that will break the navigation when I press on the button from the home screen
import React from 'react';
import {AppRegistry,Text,Button} from 'react-native';
import { StackNavigator } from 'react-navigation';
export default class HomeScreen extends React.Component {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigate } = this.props.navigation;
return (
<View>
<Text>Hello, Chat App!</Text>
<Button
onPress={() => navigate('Chat')}
title="Chat with Lucy"
/>
</View>
);
}
}
class ChatScreen extends React.Component {
static navigationOptions = {
title: 'Chat with Lucy',
};
render() {
return (
<View>
<Text>Chat with Lucy</Text>
</View>
);
}
}
const SimpleApp = StackNavigator({
Home: { screen: HomeScreen },
Chat: { screen: ChatScreen },
});
AppRegistry.registerComponent('SimpleApp', () => SimpleApp);
Additional Info:
When you are nesting child components, you need to pass navigation as prop in parent component.
//parent.js
<childcomponent navigation={this.props.navigation}/>
And you can access navigation like this
//child.js
this.props.navigation.navigate('yourcomponent');
Reference: https://reactnavigation.org/docs/en/connecting-navigation-prop.html
With Expo you should't do the App registration your self instead you should let Expo do it, keeping in mind that you have to export default component always:
Also you need to import View and Button from react-native: please find below the full code:
import React from 'react';
import {
AppRegistry,
Text,
View,
Button
} from 'react-native';
import { StackNavigator } from 'react-navigation';
class HomeScreen extends React.Component {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigate } = this.props.navigation;
return (
<View>
<Text>Hello, Chat App!</Text>
<Button
onPress={() => navigate('Chat', { user: 'Lucy' })}
title="Chat with Lucy"
/>
</View>
);
}
}
class ChatScreen extends React.Component {
// Nav options can be defined as a function of the screen's props:
static navigationOptions = ({ navigation }) => ({
title: `Chat with ${navigation.state.params.user}`,
});
render() {
// The screen's current route is passed in to `props.navigation.state`:
const { params } = this.props.navigation.state;
return (
<View>
<Text>Chat with {params.user}</Text>
</View>
);
}
}
const SimpleAppNavigator = StackNavigator({
Home: { screen: HomeScreen },
Chat: { screen: ChatScreen }
});
const AppNavigation = () => (
<SimpleAppNavigator />
);
export default class App extends React.Component {
render() {
return (
<AppNavigation/>
);
}
}
As Bobur has said in his answer, the navigation prop isn't passed to children of the routed component. To give your components access to navigation you can pass it as a prop to them, BUT there is a better way.
If you don't want to pass the navigation prop all the way down your component hierarchy, you can use useNavigation instead. (Which in my opinion is just cleaner anyways, and reduces the amount of code we have to write):
function MyBackButton() {
const navigation = useNavigation();
return (
<Button
title="Back"
onPress={() => {
navigation.goBack();
}}
/>
);
}
https://reactnavigation.org/docs/use-navigation/
This is just really nice because if you have multiple levels of components you wont have to continuously pass the navigation object as props just to use it. Passing navigation just once requires us to 1. Add a prop to the component we want to pass it to. 2. Pass the prop from the parent component. 3. Use the navigation prop to navigate. Sometimes we have to repeat steps 1 and 2 to pass the prop all the way down to the component that needs to use navigation. We can condense steps 1 and 2, no matter how many times they are repeated, into a single useNavigation call with this method.
I think it is best.
Try this Code: onPress={() => this.props.navigation.navigate('Chat')}
<ChildComponent navigation={props.navigation} {...data} />
This will make the navigation from the parent propagated to the subsequent child navigations.
const AppNavigation =()=>{ <SimpleApp />}
export default class App extends React.Componet{
render(){
return (
<AppNavigation/>
);
}
}

Categories