Refreshing page removes non-URL $stateParam (ui-router) - angularjs

Im using UI-Router in my application and I have a state parameter where it doesn't really make sense for it to be part of the url. According to the documentation I am creating the parameter without specifying it in the state url, as such:
.state('contact', {
url: "/:contactId/contact",
params: {
otherParam: null
},
templateUrl: 'contacts.html'
})
This works as intended, but I noticed that if I manually refresh the page, the parameter gets reset to the default null.
For example, if I redirect to the state as such:
$state.go('contact', {contactId: 42, otherParam: 11});
Everything works as expected ($stateParams.contactId is 42, $stateParams.otherParam is 11)
However, once I refresh the page, $stateParams.contactId is still 42, but $stateParams.otherParam has been set back to null. Is it possible to persist this non-URL parameter across a browser refresh?

URLs in a SPA are meant for managing the UI state, i.e.: which components to display and with which info (ex. detail component with id). The router resolves the state from this. A good example is to think of the url as something to be sent by email (or whatever) to someone else and expect for their router to resolve the state. So, briefly put I would say no to your question.
Your problem is that you want to persist state of the application that is specific to the browser session, right? Browsers are equipped with mechanisms for that and I'd recommend taking a look into local storage or session storage to solve your problem and retrieve the info you need on the resolve method in your state declaration.

It's sort of possible, but you shouldn't do it. If it is something that should persist on refresh, that means that by definition it SHOULD be in the URL, since you expect it to be a component of the resource.
If you really really want to break convention and do this, you'll need to do something that saves the data in window.name and retrieves it on reload. This is definitely an anti-pattern, and anyone could get access to that data, but like I said, it's possible.

Related

How does react-router persist location state over page refreshes?

I am just so very confused.
I have a workflow where someone can start to fill out a form for a product. It is a long form and I want to save progress to the server as they type (but not until they fill it out for a bit). So we start at a url for creating the form, after they've typed for a bit, we POST to create a resource on the server, and after the request finishes, we update the url to the edit route with the new id.
In other words, you start filling out the form at url /product then after you've filled it out for a bit the url shifts to /product/123. After that, loading that URL gives you your form.
So basically I have
<Route path={`/product`} exact component={CreateProduct} />
and
<Route exact={true} path="/product/:productId" render={({
match: {params: {productId}},
location: {state: {data}={}}
}) => (
<EditProduct productId={productId} initialData={data}
)} />
See that state? That's because the way I do the switch over from create to edit mode is something like this
const id = await apiFetch(`/api/product`, data, {method: `POST`})
this.props.history.push({pathname: `/product/${id}`, state: {data} })
in the constructor of my <EditProduct> component I have
constructor({productId, initialData}) {
this.super()
this.state = {}
if(initialData)
this.setState({data: initialData})
else
getProduct(productId).then(({data}) => this.setState({data}))
}
By doing that, the initial data into the <EditProduct> is seeded from the <CreateProduct> component and I don't need to reload it from the server or anything.
This works, the transition is smooth, the url updates, and everything is hunky dory.
I can now continue editing the <EditProduct> component and it saves properly. I can open a new tab to the same url and it loads everything up and I can continue. This happens because in that situation initialData is undefined so it's loaded from the server. yay!
BUT
If I instead refresh the original tab things get weird. Any changes that have accumulated since the save are lost. Drilling down in the debugger I see the issue is that initialData passed from the location.state.data object is not empty - it is the initial object from when the product was first created.
So where on earth does it come from? I just did a full page refresh (even a "hard" refresh with no cache and devtools open). That data isn't in the URL (and in fact copy pasting the url into another tab in the same window doesn't have this issue).
The only mechanism I'm aware of that can persist data across refreshes but not to new tabs like this is sessionStorage, yet when I check it in the console, I am told
> sessionStorage
< Storage {length: 0}
I've even thought that maybe react-router is manipulating session storage just before the page unloads and just after it loads, but breaking on the first line of my javascript bundle shows the exact same thing.
So how on earth is this persistence happening!?
I believe the asker already resolve this problem, the answer is buried in the comment though.
The question is actually down to this:
Where the state come from when the user reloads the page? And state refers to props.location.state provided by react-router
TLDR; the state is not a plain javascript implementation, it is bound to the browser environment.
The BroswerRouter of react-router use the underlying native broswer history API directly so the history API is bound to the platform, you can not predict its behavior based on the normal rule.
Here is the special part:
The state object can be anything that can be serialized. Because Firefox saves state objects to the user's disk so they can be restored after the user restarts the browser
Most of the users treat the state as a plain javascript, so there is a problem
I had a very similar problem, and the same confusion.
Solved it with window.history.replaceState()
I had a simple search form which redirected to a second page, and used the location state from the router to repopulate the search input on the second page.
In myse case, this happened:
Search for "foo" on the first page -> Get redirected to the second page, and see search+results for "foo".
Search for "bar" on the second page. -> See results for "bar".
Hit refresh. Expectation? Either an empty search bar, or search+results for "bar". -> Instead, see search+results for "foo" (??)
I solved this by making it so that every time the user does a search on the second page, I replace the state using window.history.replaceState with the correct search term. This way a refresh gives the user the expected search. Replacing the state with an empty object on each search from the second page worked fine as well, giving the user an empty search on each refresh.

Check if url leads to a defined state in AngularJS

I use UI-Router and if the requested state doesn't exist in my config I want the app go to a certain state depending on whether the user is authenticated or not. Either 'home' or 'dashboard'.
I tried to use $urlRouterProvider.otherwise but the authenticated is not available there (in app.config). Then I tried in app.run to use 'stateChangeError' and 'stateNotFound' which interestingly have no effect at all here.
So my idea to work around this is to check if the state exists by using the URL - which I can get with $location.path(). But a $state.get() with this always delivers null because it expects the name of a state, not the URL.
So how can I find out, based on what the user enters, whether there is an according state?

in ui-router, how to re-resolve only the most local state?

I have the following state tree, using ui-router,
1 login
2 root (abstract, resolves app-prefs, user-prefs)
2.1 home (builds a refresh button, should refresh whatever is being shown)
2.1.1 dashboard (resolves dashboard-prefs)
2.1.2 search (resolves search-prefs)
2.1.3 etc
2.2 etc
From home when user presses refresh button while in XYZ state, I would like to have the XYZ re-entered in such a way that it re-resolves its own XYZ-prefs but not things above in hierarchy. Something like
$state.go("dashboard", dashboardParams, {please-resolve-only-dashboard})
When I try, from home
$state.go("dashboard", dashboardParams, {reload:true})
that causes everything from root downwards to get re-resolved, which is problematic, and expensive, as I need to re-resolve only dashboard-prefs. I can setup a more elaborate scheme in some resolvers to not re-resolve themselves but that might become a task by itself I'm afraid. Is there another, more idiomatic way?
Thanks
There is a wokring plunker
There is a native way how to do that - just change the parameter of the state you want to reload.
To reproduce the above state definition let's have dashboard defined like this:
.state('dashboard', {
parnet: 'home',
url: "^/dashboard",
params: { updater : 1, },
...
})
What we can see, that we do not touch url at all. It will always be without any change just /dashboard
But we introduce really cool feature of the latest version - params: {}. It defines some parameter - updater in our case. Whenever this parameter is sent, and does differ form its current value, this child state (and only this child state) is re-init
Check it more about state params: {} here: http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider
Now, we can create this reload link:
<a ui-sref="dashboard({updater : ($stateParams.updater + 1) })">reload</a>
And with this incrementation, we can be sure, reload will reload this state
Check it here

Set URL query parameters without state change using Angular ui-router

How should I update the address bar URL with a changing query parameter using AngularJS' ui-router to maintain state when refreshing the page?
Currently, I am using $state.transitionTo('search', {q: 'updated search term'}) whenever input changes, but the problem is that this reloads the controller, redraws the window and loses any text input focus.
Is there a way to update stateParams and sync it to the window URL?
I was having trouble with .transitionTo until I updated to ui-router 0.2.14. 0.2.14 properly changes the location bar (without reloading the controller) using a call like this:
$state.transitionTo('search', {q: 'updated search term'}, { notify: false });
edit: Played around with this some more today, and realized that angular ui-router has a similar option as the native routerProvider: "reloadOnSearch".
https://github.com/angular-ui/ui-router/wiki/Quick-Reference#options-1
It's set to true by default, but if you set it to false on your state, then the state change won't happen when the query parameters are changed. You can then call $location.search(params); or $location.search('param', value); and the URL will change, but ui-router won't re-build the entire controller/view. You'll probably also need to listen for the $locationChangeStart event on the root scope to handle back and forward history actions within your app, as these also won't cause state changes.
I'm also listening for the $stateChangeSuccess event on my controller's scope to capture the initial load of the page/route.
There is some discussion on github for using this feature with path changes (not just URL changes): https://github.com/angular-ui/ui-router/issues/125 but I haven't tested that at all since my use case was specific to query string parameters.
The previous version of my answer mentioned this github issue:
https://github.com/angular-ui/ui-router/issues/562
But that's a slightly separate issue, specifically showing a modal of one state over another state without the non-modal state changing. I tried the patch in that issue, but it's clear that it isn't meant for preventing the entire state from reloading on URL change.
Update May 19, 2015
As of ui-router 0.2.15, the issue of reloading the state on query parameter changes has been resolved. However, the new update broke the history API back and forward capabilities with query parameters. I have not been able to find a workaround for that.
Original
Jay's answer didn't work for me, and neither did a ton of other answers. Trying to listen to $locationChangeStart caused problems when trying to go back and forth in the browser history as it would cause me to run code twice: once when the new state changed and another because $loationChangeStart fired.
I tried using reloadOnSearch=false, but that prevented state changes even when the url path changed. So I finally got it to work by doing the following:
When you change $location.search() to update the query parameters, use a "hack" to temporarily disable reloading on search, set query parameters, then re-enable reloading.
$state.current.reloadOnSearch = false;
$location.search('query', [1,2]);
$timeout(function () {
$state.current.reloadOnSearch = undefined;
});
This will ensure that query parameter changes do not reload the state and that url path changes will reload the state properly.
However, this didn't get the browsers history to change the state (needed for knowing when a query parameter changes to re-read the URL) when a query parameter was part of the url. So I also had to add each query parameter's name to the url property of the state.
$locationProvider.html5Mode(true);
$stateProvider
.state('home', {
url: '/?param1&param2&param3',
templateUrl: 'home.html',
controller: 'homeCtrl as home',
});
Any parameter names on the url are optional when listed this way, but any changes to those parameter names will reload the state when hitting the back and forward buttons on the browser.
Hopefully others find this useful and it doesn't take them multiple days to figure out how to do it (like I did).

Why is it considered bad practice to call trigger: true in the navigate function of backbone.js?

I have read in several places that calling the Backbone.history.navigate function is considered bad practice.
For example Addy Osmani sais in his book "Developing Backbone.js Applications"
It is also possible for Router.navigate() to trigger the route along
with updating the URL fragment by passing the trigger:true option.
Note: This usage is discouraged...
http://addyosmani.github.io/backbone-fundamentals/#backbone.history
Or Derick Bailey in his blog post even sais:
You shouldn’t be executing the route’s handler from within your application, most of the time.
But I don't really understand the reasoning behind it and what would be a better solution.
In my opinion it is not really bad to call the navigate function with the trigger:true option. The route function could upon calling always check if the considered data is already loaded and show this loaded data instead of doing the whole work all over again...
There seems to be some confusion about what Router#navigate does exactly, I think.
Without any options set it will update the URL to the fragment provided.
E.g. router.navigate('todo/4/edit') will update the URL to #todo/4 AND will create a browser history entry for that URL. No route handlers are run.
However, setting trigger:true will update the URL, but it will also run the handler that was specified for that route (In Addy's example it will call the routers editTodo function) and create a browser history entry.
When passing replace:true the url will be updated, no handler will be called, but it will NOT create a browser history entry.
Then, what I think the answer is:
the reason why the usage of trigger:true is discouraged is simple, navigating from application state to application state to application state requires most of the time different code to be run than when navigating to a specific application state directly.
Let's say you have states A, B and C in your application. But state B builds upon state A and state C builds upon B.
In that case when you navigate from B to C only a specific part of code will need to be executed, while when hitting state C directly will probably execute some state checking and preparation:
has that data been loaded? If not, load it.
is the user logged in? If not redirect.
etc.
Let's take an example: State A (#list) shows a list of songs. State B (#login) is about user authentication and state C (#list/edit) allows for editing of the list of songs.
So, when the user lands on state A the list of songs is loaded and stored in a collection. He clicks on a login-button and is redirected to a login form. He successfully authenticates and is redirected back to the song list, but this time with delete-buttons next to the songs.
He bookmarks the last state (#list/edit).
Now, what needs to happen when the user clicks on the bookmark a few days later?
The application needs to load the songs, needs to verify the user is (still) logged in and react accordingly, stuff that in the state transition flow had already been done in the other states.
Now for a note of my own:
I'd never recommend the above approach in a real application as in the example. You should check whether the collection is loaded when going from B to C and not just assume it already is. Likewise you should check whether the user really is logged in. It's just an example.
IMO the router really is a special kind of view (think about it, it displays application state and translates user input into application state/events) and should always be treated as such. You should never ever rely on the router to transition between states, but rather let the router reflect the state transitions.
I have to disagree with #Stephen's answer here. And the main reason why is because the use of router.navigate({trigger : true}) gives the router responsibility to handle the application's state. It should only reflect application state, not control it.
Also, it is not a View's responsibility to change the hash of the window, this is the router's only job! Don't take it away from it! Good modularity and separation of concerns makes for a scalable and maintainable application.
Forwarding a person to a new section within your application
Backbone is an event driven framework, use events to communicate. There is absolutely no need to call router.navigate({ trigger : true }) since functionality should not be in the router. Here is an example of how I use the router and I think promotes good modularity and separation of concerns.
var Router = Backbone.Router.extend({
initialize: function(app) {
this.app = app;
},
routes: {
'videoLibrary' : function() { this.app.videoLibrary(); }
}
});
var Application = _.extend({}, Backbone.Events, {
initialize: function() {
this.router = new Router( this );
this.listenTo( Backbone, 'video:uploaded', function() {
this.router.navigate('/videoLibrary');
this.videoLibrary();
});
},
videoLibrary: function() {
//do useful stuff
}
});
var uploadView = Backbone.View.extend({
//...
uploadVideo: function() {
$.ajax({
//...
success: function() { Backbone.trigger('video:uploaded'); }
});
}
});
Your view does not need or want to know what to do when the user is done uploading, this is somebody else's responsibility. In this example, the router is just an entry point for the application's functionality, an event generated by the uploadView is another. The router always reflects the application state through hash changes and history but does not implement any functionality.
Testability
By separating concerns, you are enhancing the testability of your application. It's easy to have a spy on Backbone.trigger and make sure the view is working properly. It's less easy to mock a router.
Modules management
Also, if you use some module management like AMD or CommonJS, you will have to pass around the router's instance everywhere in the application in order to call it. Thus having close coupling in your application an this is not something you want.
In my opinion it's considered bad practice because you should imagine a Backbone application not like a Ruby On Rails application but rather like a Desktop application.
When I say RoR, I'm just saying a framework supporting routing in sense that a route brings you to a specific call to the controller to run a specific action (imagine a CRUD operation).
Backbone.history is intended just as a bookmark for the user so he can, for example, save a specific url, and run it again later. In this case he will find the same situation he left before.
When you say:
In my opinion it is not really bad to call the navigate function with
the trigger:true option. The route function could upon calling always
check if the considered data is already loaded and show this loaded
data instead of doing the whole work all over again...
That to me sounds smelly. If you are triggering a route and you are checking for the data to see if you have it, it means that you actually already had them so you should change your view accordingly without loading again the entire DOM with the same data.
That said trigger:true is there so do we have reason use it? In my opinion it is possible to use it if you are completely swapping a view.
Let's say I have an application with two tabs, one allows me to create a single resource, the other one let me see the list of the created resources. In the second tabs you are actually loading a Collection so data is different between the two. In this case I would use trigger:true.
That said I've been using Backbone for 2 weeks so I'm pretty new to this world but to me it sounds reasonable to discourage the use of this option.
It depends on your context.
If you have done something in your current view that might affect the view you are about to navigate to, for example creating for deleting a customer record, then setting trigger to true is the right thing to do.
Think about it. If you delete a customer record don't to want to refresh the list of customers to reflect that deletion?

Resources