In react-router v3, I've been using router.setRouteLeaveHook to check if a form has unsaved changes, and if so return false to prevent the transition. Then I would display a custom bootstrap modal dialog with 3 buttons: Save Changes, Discard Changes, and Stay Here.
I can't use react-router v4's Prompt component to do this because it's not possible to customize the buttons shown in a browser confirm dialog. It seems like they got rid of any way to cancel the transition in favor of only allowing you to ask the user to approve the transition in a browser confirm dialog.
I tried looking in the code for Prompt but it just passes the dialog message to history, so that doesn't give me any idea how to set up a v3-style route leave hook.
Is it even possible anymore or did the react-router devs intentionally decide to remove this capability this for some reason?
According to the history package docs, you can replace window.confirm with anything you like:
By default, window.confirm is used to show prompt messages to the user. If you need to override this behavior (or if you're using createMemoryHistory, which doesn't assume a DOM environment), provide a getUserConfirmation function when you create your history object.
So if you want to use your own dialog, something like this should see you through:
const history = createHistory({
getUserConfirmation(message, callback) {
showMyCustomDialog(message)
.then(result => callback(result === 'The YES button'))
}
})
This means that whatever getUserConfirmation message you set is set for the entire session, but you could abstract it out to your navigation blocking layer that holds additional details for your dialog, e.g. title, button text, colours etc.
Alternatively, you could hijack the message argument and use it for dialog configuration, though that may smell a bit nasty. But it's not a perfect system so anything you do will probably be a bit of a hack.
React Router v4 allows you to pass this method through when you create your router (see here):
<BrowserRouter getUserConfirmation={yourConfirmationFunction} />
Can use Prompt to show custom dialogue. Credit and detailed explanation here.
Prompt requires a message prop, here we can use a custom function for a dialogue and it should return false to prevent navigation.
const BlockingPage = () => {
const [block, setBlock] = useState(true);
const blockedNavigation = (nLocation) => {
//nLocation gives the next location object
/**
* Your custom logic
*
**/
//required to block navigation
return false
}
return(
<div>
<Prompt when={block} message={blockedNavigation}/>
</div>
)
}
I don't think this is possible. react-router-v4 uses a package called history that in turns uses the HTML5 history api. The history api only notifies you when you hit the back button (onpopstate), If you think about it this makes lots of sense, since you would not want to give a website the power of not letting you move between pages.
The best you can do is the window onbeforeunload event, that creates a prompt for you asking confirmation from the user, but this is exactly what react-router exposes for you to use.
You might get some of the functionality you want by monkey-patching react-router's internal history object, and that way you can add your own behaviour. But there is a caveat, this is only going to work when you react-router's <Link /> component and friends, so you will not be able to intercept refreshes and other things you may want.
If you want to go that route, let me know I can give you some insight or code examples about how might that work and I will update my answer.
Related
I would like to know what is the best way to refresh the page after typing.
For example, I press a button that modifies the name of a product, how can I do that from the moment I validate my modification, the changes appear immediately, without having to manually refresh the browser page.
I saw some example with react-router, doing a redirect or using 'history.push', I also saw with the 'window.location.reload (false)' but it doesn't feel right because you can 'see' that the page refresh (yes you don't manually refresh but ... maybe there is something better to do)
Well, the best way is to not refresh the page at all.
Specially if you are using React. Every piece of data that you display on your UI is supposed to be stored in some kind of state. Even if that data should somehow be validated asynchronously (I think this is your case), once done, you should trigger a state change that will cause the interested components to re-render and display the new information.
You can use useState hooks of react to view the changes without refreshing the window.
In every state change, which means the change of value on the given state, react automagically rerender to show the latest data.
const [productName, setProductName] = useState('');
cosnt handleButtonClick = (name) => setProductName(name)
return (
<>
Production Name: {productName} // Product name changes on every button click.
<button onClick={() => handlebuttonClick(dynamicallySendRequiredDataFromHere)}> Change product name </button>
<>
)
It all depends on your requirement actually.
If you want to reload the page to get something which can only be achieved by reloading then use
window.location.reload()
If you want to reload just to get the data then make the API call and connect your component with the state that gets the value after the API call
If you want to maintain the history stack the use
history.push()
If you dont want to maintain the history stack the use
history.replace()
Some fancy times when you want to set cookie or storage to your page but dont want to refresh current page you can use window.open with the origin and target
window.open(window.location.origin, '_blank')
I'm developing with react-router-dom. Now, I'm creating a page where users can enter information in a form. What I want to do is prevent the user from losing the information they enter if they accidentally return to the previous page.
I first found a way to stop the browser back in the article below. It seems that the moment you return to the previous page, you immediately return to the original page, effectively preventing you from returning to the previous page.
React.useEffect (() => {
return () => {
props.history.goForward ();
}
}, []);
React Router Dom v4 handle browser back button
However, in this case, the previous page will be returned once, so all the current page information (state) will be reset. Is there a solution to prevent the state from resetting?
Or is there a smarter way to solve this?
Below, I have prepared an image for explanation.
You have several ways to approach your answer, but all of them have 1 concept, and that is Higher-order-component, so in this case, you have to have a top-level component (Higher than react-router), so when the location has changed, you don't lose the information in the state. in another word, you have to have a general state.
So how you can reach this goal? you have several ways and I'm here to help you use them.
redux - https://redux.js.org/.
context - https://reactjs.org/docs/context.html.
react state - https://reactjs.org/docs/higher-order-components.html.
localStorage or sessionStorage - https://www.robinwieruch.de/local-storage-react.
...
these are just some examples of what you can do to prevent losing state when the browser location has changed.
I'm doing a Chrome plugin for Facebook and I want to modify a post using external javascript. I can change the textContent of a span but the state is not changed and the Save button is not activated.
The html looks like that :
<div class="" data-block="true" data-editor="1477r" data-offset-key="a699d-0-0">
<div data-offset-key="a699d-0-0" class="_1mf _1mj">
<span data-offset-key="a699d-0-0">
<span data-text="true">test</span>
</span>
</div>
</div>
The component in React Developper Tools :
[
I profiled a change of the text and this is the result for a 6 ms timeline activating the save button.
There are ways to modify a React input but did not found ways for my problem :
https://github.com/facebook/react/issues/11488
https://github.com/facebook/react/issues/10135#issuecomment-314441175
How can you change a Facebook post and his state with external javascript?
What would you do?
That’s not possible, you can’t imperatively mutate a React element tree from outside the app. You’d have to declaratively render a tree, either by modifying Facebook’s frontend code (beware of legal ramifications) or implementing your own post UI.
While it's nearly impossible to change the state of React component from your plugin, nothing stops you from emulating user's input by sending keystrokes, mouse clicks etc. The only thing you need it to figure out - which DOM-element listens to these events (not necessary one of those 4 in you question).
About the possibility of direct state change: let's say the component you need to changes is a functional one. Then it has a form of
Component() {
const [state, setState] = useState(...)
...
setState(something)
...
}
so you need to somehow access the setState function of the component. But how? It's private to the function call. If you think that instead you can call the useState directly, then be aware that in another component it will return another setState. I have no idea what would happen if you'll call useState outside of a component, but surely it will not be able to guess which setState you want.
If you want you can check the source code of react-devtools to find out how you can dig out the state from the depths of React... but would you really want to try? And for what? The next time Facebook or React will be updated your code will definitely break.
I am having an issue with navigation blocking in React.
I use React + Redux with React Router. I have been using the component in order to prevent navigation from incomplete forms within React. This works fine as long as the navigation is actually within React. What it doesn't block, however, is navigation via URL entry, or clicking on an external hyperlink.
Is there an accepted method in React for handling external hyperlinks and blocking navigation outside of React?
You didn't provide any code here, so I'm trying to guess. If I understand you correctly, you are able to manage you internal redirects thought the React app via react-router without any issues.
As per your statement:
What it doesn't block, however, is navigation via URL entry, or clicking on an external hyperlink.
Let's tackle both questions. First can't prevent a user from going to the navigation bar and enter a new URL, that done by design on the browsers side, it would be bad for the user, and of course invasive!
But regarding your second question about clicking on a link that will send you outside your domain or application, you can do something about it.
You have multiple options here, I will give you three:
First: Use history.block
You can block or better said, ask the user to acknowledge the transition to another page by using history.block
As an example from the history docs:
const unblock = history.block('Are you sure you want to leave this page?')
Second: Use history.push instead of href
Just don't use anchor elements href, but rely on the history of react-router.
You can read more about it here: react-router/history
But basically you can wire your redirection using the push method from history, which will look something like:
onClick() {
//code to control if you want to redirect or not
history.push('http://yoururl.com');
}
Third: Use Redirect component with conditional rendering
Then you have other options like for example using conditional rendering combined with Redirect components, but the other approach would be enough to solve your problem.
I think you are looking for Blocking Transitions under the history api for React Router.
As per the example on react router history github page:
You can register a simple prompt message that will be shown to the user before they navigate away from the current page.
const unblock = history.block('Are you sure you want to leave this page?')
Detailed info at https://github.com/ReactTraining/history#properties
Suppose I have an Angular app for editing eCards. Creating a new eCard uses a path like #/ecard/create and editing an existing eCard uses a path like #/ecard/:id. A tabbing system lets us have multiple eCards open for editing at a time.
We'd like an autosave feature like what users would expect from e.g. modern webmail or wiki software (or StackOverflow itself). We don't want to save an eCard draft the moment the user opens the Create form, which would give us a lot of drafts of blank eCards, so we start autosaving once the user starts typing.
I'd like to write code like this in our controller (this is simplified to not include e.g. error handling or stopping the autosave when the tab is closed, etc):
$scope.autosave = function () {
ECardService.autosave($scope.eCard).then(function (response) {
$location.path('/ecard/' + response.id).replace();
$timeout($scope.autosave, AUTOSAVE_INTERVAL);
});
};
$timeout($scope.autosave, AUTOSAVE_INTERVAL);
The above code works great, except for one thing: when the location changes, our controller reloads and the view re-renders. So if the user is in the middle of typing when the autosave completes, there's a brief flicker, and they lose their place.
I've considered several approaches to mitigate this problem:
1) Change the path to use the search path and set reloadOnSearch to false in the ngRoute configuration. So the path would change from #/ecard?id=create to e.g. #/ecard/id=123 and thus not force a reload. The problem is that I might have multiple eCards open and I do want changing from e.g. #/ecard/id=123 to #/ecard/id=321 to trigger a route change and reload the controller. So this isn't really feasible.
2) Don't bother editing the URL and deal with the back button giving a weird behavior in this case. This is tempting, but if a user opens their list of existing eCards and tries to open the specific eCard that has been saved, we want the tabbing system to recognize that it should just display the currently existing tab rather than open a new tab.
We could theoretically address this by updating our tabbing system to be smarter; instead of just checking the path, it could check both the path and the persistent id, which we could store somewhere. This would make the tabbing system significantly more complex, and that seems like overkill for this feature.
3) Only change the URL when the user is not actively editing, e.g. write a $scope.userIsIdle() function which returns true if it's been at least 10 seconds since the user made any edits, then update the path based on that. A simplified version of this would look something like:
$scope.updatePathWhenSafe = function (path) {
if ($scope.userIsIdle()) {
$location.path(path).replace();
} else {
$timeout(function () {
$scope.updatePathWhenSafe(path);
}, 1000);
}
};
I ended up going with option #3; it was significantly simpler than option #2, but a lot more complicated to implement and test than I'd like, especially once I account for edge cases such as "what if the tab is no longer the active tab when this timeout fires?" I'd love for option #4 to be possible.
4) Go outside Angular to edit the current location and history, assuming this is necessary and possible. This would be my preferred solution, but my research indicates it's not safe/advisable to try to go around the $location service for changing your path or editing history. Is there some safe way to do this? It would make things so much simpler if I could just say, "Change the current path but don't reload the controller."
Is option #4 possible/feasible? If not, then is there a better way? Maybe some magical "Do it the angular way but somehow don't refresh the controller"?
This is not angular way, but it can be useful. After receiving data you can check whether there is an focused element (user is typing). If so, then you need to define a function that is performed once when element lose focus. If no focused element, the change url immediately.
Like this:
ECardService.autosave($scope.eCard).then(function (response) {
if($(':focus').length){ //if there is focused element
$(':focus').one('blur', function(){ //
$location.path('/ecard/' + response.id).replace(); //perform once
});
}
else{
$location.path('/ecard/' + response.id).replace();
}
});
Of course this is not the most elegant solution, but it seems to solve your problem.
If you have code that needs to run across multiple view controllers AngularJS provides a root scope for such instances. You can find the documentation here.
However I would recommend against having a tabbing system that is actually multiple views. Having multiple items open means to have them all in your work space.
You might want to consider a single view with Angular directives for your e-cards. That way they could each have their own scope and would be available at an instance without re-rendering the page.
They would also be able to share the functions defined in the controller's $scope, without the need for an app wide root scope. Note that scope has to be enabled on directives. scope: true
Check out the AngularJS site for tutorial and documentation on this.
It seems that the best solution for the problem you're describing would be to use a state machine like ui-router.
With a library like that one, you can have a single page app that has multiple states (that you can also make part of the url), so whenever the state changes, you can save your e-card and you'll never have any visible reloads because you're working on a single page application.
So I understand the path wants to reflect the id of the latest version, so in that case you would need to refresh every save.
But, what about if the path was something like ecard/latest as a alias for the latest version. That way you wouldn't have to refresh your view since you don't have to change your path, and just implement something in the back-end directs the param latest to the id of the latest version.
It turns out there's a way to do exactly what I want, although it's not officially blessed by Angular. Someone opened an Angular ticket for this exact use case: https://github.com/angular/angular.js/issues/1699
The proposed change was submitted as a pull request and rejected: https://github.com/angular/angular.js/pull/2398
Based on the comments in the original ticket, I implemented a workaround that looks like this:
app.factory('patchLocationWithSkipReload', function ($location, $route, $rootScope) {
$location.skipReload = function () {
var prevRoute = $route.current;
var unregister = $rootScope.$on('$locationChangeSuccess', function () {
$route.current = prevRoute;
unregister();
});
return $location;
};
});
I'm then able to basically (error handling omitted for brevity) say
ECardService.autosave($scope.eCard).then(function (response) {
$location.skipReload().path('/ecard/' + response.id).replace();
$scope.resetAutosaveTimeout();
});
Basic testing shows this works great!