Open jods4 opened 9 years ago
@bryanrsmith I think this is similar to the other issue we had where we proposed something like a force
option to be added to the options for the navigate
method. I'm seeing this issue come up more and more, so I think we definitely want to find a clean way to address it.
@EisenbergEffect Yes, a new option force: true
that would skip the check on line 191 I referenced in my suggestion would certainly do the trick.
The method reload
would still be a nice convenience wrapper to call navigate(this.fragment, { replace: true, force: true })
and is certainly more discoverable.
That sounds good. @bryanrsmith At your earliest convenience, let's work on adding the force
parameters and then creating the reload
or maybe refresh
method that is a convenience as stated above.
Hi,
After some time looking for a response I couldn't find any way I can refresh the current view. I'd like to have a way to reload the current view and forcing all the cycle to run
+1 to this feature
It's surprising there's no way to reload/refresh current view/view-model... I'd just expect activate method to be called again
+1 Is there any way to achieve that ?
This is tricky. What would it mean to refresh if there were child routers? Would you expect the entire hierarchy to be refreshed? or would you expect only the deepest child to be refreshed? Perhaps this would just be controlled with navigation strategies, but what if you want different strategies for the same view model based on whether or not there's an internal refresh?
A simple way to achieve the use case above, is to extract all state into its own class and simply set that as a property on the screen. Then, whenever you want to "refresh" you simply replace the property with a new instance of the state class. You could make this really simple by manually calling the screen's own activate
callback. Here's some pseudo code:
@inject(Server)
export class MyScreen {
constructor(server) {
this.server = server;
this.params = null;
this.model = null;
}
activate(params) {
this.params = params;
return this.configureModelFromParams(params);
}
reset() {
this.activate(this.params);
}
configureModelFromParams(params) {
return this.server.loadModel(params.id).then(model => this.model = model);
}
}
In this case I'm showing loading a model from the server, but it could be some other more complex aggregate model derived from anything. The point is that the state doesn't reside directly in the screen. It resides in some other object that you can easily dispose of and replace. The screen then acts more like a controller, automating the process of creating that object and replacing it as needed. Additionally, this is going to be much more efficient because the screen object and its view don't need to be destroyed, re-created, rebound, etc. Only the underlying data of the screen is destroyed and recreated. The binding system will handle the rest.
Our use case is changing permissions for the current user. Imagine the admin user who is going to the user management and change his own roles and permissions. We need the full system refresh. Everything depends on the user permissions from the very beginning, including the navigation. May be with the new permissions user will not even be able to stay on the page where he currently is. The easiest solution is user re login, but I think we can do better then this.
Another use case is back end system ask front end to refresh because last user activity was too long ago and the system state is inconsistent now. Again, we need the complete refresh.
I use this (with a ugly hack: add a dummy random parameter to the current route) quite a bit in our app.
One reason (not the only one) is that we use one-time
bindings where we can, so when we want a full refresh we need to re-evaluate those bindings, too.
It's not the only reason, another being that @EisenbergEffect's example is a basic model. We have complex UI with multiple independent parts and not everything can easily be reduced to a single loadModel
call. Plus there might be some UI state we want to reset, which is not always tied to a ViewModel property (see the motivations of #173, notably my comments at the bottom).
As we don't use child routers, I am not sure if refreshing child routers is the right thing to do or not... I would lean towards yes? Could it simply be an option to the new api, with a true
default value?
Maybe: router.reload({ childRouters: false })
?
@EisenbergEffect @bryanrsmith I think that in the case of child routers - if you call reload()
on a parent router, it would reload itself and all of its children (unless you specifically wanted to opt-out of that behavior for some reason). The most logical solution for reloading only the deepest child, to me, is to call reload()
on that specific child. (The code would have to have a reference to it somewhere, anyway.)
This would ensure that there's only a one-way flow at all times, and a predictable one, at that. A router either only reloads itself or its entire hierarchy, but never a parent router or an arbitrary child router.
Finally, I think the reload function could take a NavigationStrategy as an optional parameter, if you wanted to override the configured strategy. Or, this may not be necessary at all.
If there are use cases where this would not work, let's explore them. I'd like to nail this down before we begin work on it.
That sounds reasonable to me. It might be nice to get feedback from someone who wants this feature, though. The simplest thing to do (in terms of implementation on our end) is to reload the entire router tree, respecting activation strategies. I think that would work now, there's just no helper method for making it happen.
Is there still a plan to add the force
parameter to the router/navigateToRoute's option?
@bryanrsmith "reload the entire router tree" will work for us. We have two use-cases for this feature. Both will be solved. Both of them is "permission based". Current user permissions changed and it pottentially affect everything on the app.
@bryanrsmith We need this and reload all child routers would work for us as well, mostly because we don't have child routers ;)
+1 @bryanrsmith i was able to hack the old Alpha history-browser previously to reload same path with new dynamically loaded routes
// if (this.fragment === fragment) return;
aurelia/history-browser#4
Also similar use case to @kiryaka. Unauthorised routes are hardcoded into app. Once authorised, we need to reset router and navigate to same blank route, now configured with authorised routes. Current call sequence was/is:
router.reset();
router.configure((config) => { ... });
router.navigate(path || '', { replace: true });
We are also desperately waiting on a fix for this. Is there an ETA ? Thanks.
+1
also seems that this issue is closely related #237 (after router.reset() and router.configure() the view isn't updated)
I am having a similar issue in which some pages are not being updated when the route changes. I see some mention of (with an ugly hack: add a dummy random parameter to the current route). Does anyone have a sample of how this hack is implemented? Thanks.
The concern with this issue is that we we have to do a page refresh on the browser every time a user authenticates (which is when we generate the navigation menu's).
The result of this is that page load feels like it takes 15sec when in actual fact it would take 3sec.
yawn
For authentication, one solution is to route the user to a "logged in page" or profile page but I want the user to stay on the current page (but refresh it based on the new authenticated status). I solved this scenario by sending an event when the user authenticates. Each view is then responsible to subscribe to this event and react accordingly. Some views does nothing, some update backing fields in the view model which in turn will update the bound elements in the view.
I do however agree with this suggestion and use case.
We also have the same problem in my company with the same usecases =>
When doing so this results in wrong translated views or in wrongly shown views. There is currently no possibility to refresh a page you always have to use observers or something like this in order to get the views refreshed. This is a pain and should be nothing that you have to do on your own. I would like to see this feature in following releases.
BTW you're doing a great job with aurelia - thanks :)
Update In case you missed it, it seems some changes have been made that impact this.
The culprit used to be the following line in history-browser
if (this.fragment === fragment) return;
Which was changed at some point to:
if (this.fragment === fragment && !replace)
It's not clear to me why this was changed and replace
is a bit orthogonal to "force reload".
But the net result is that if you pass { replace: true }
when navigating it will perform a reload even when you go to exactly the same route. Conveniently, using replace
when reloading a page is highly likely as you probably don't want to add an history entry for that.
Note: don't forget to use
determineActivationStrategy() {
return activationStrategy.invokeLifecycle;
}
or otherwise the lifecycle events of your VM won't be called by default.
@jods4, this is still an issue for us...
this.router.reset();
this.router.configure(function (config) {
config.title = conf.title;
config.map(conf.menus);
}).then(() => {
this.router.navigate(path || '', { replace: true });
});
am I doing anything wrong to dynamically load the new routes?
ERROR [app-router] Error: There was no router-view found in the view for ./dashboard.
@ochart2 sorry, I really don't know what you're trying to do with the dynamic router configuration. Maybe @EisenbergEffect can help.
I am just using this.router.navigateToRoute(currentRoute, currentParams, { replace: true })
in our ViewModels and with the current release it seems to correctly reload the page.
@jods4 The idea is to reload the router with brand new routes, and then navigate to the emtpy
route again, which should activate a different viewmodel from the one currently loaded. Does that make sense?
@jods4 ps getting the following when using navigateToRoute
Error: A route with name '' could not be found. Check that `name: ''` was specified in the route's config
with the names object in aurelia-route-recognizer
empty
@jods4, got it working with navigateToRoute!
Question, it is now manipulating the actual url by appending ?replace=true
to it.
Is there not a cleaner solution for this as I am now manually removing ?replace=true
out of the url afterwards?
@ochart2 I think you messed up the parameters. navigateToRoute(routeName, routeParams, options)
Note that there is one additional parameter compared to navigate()
.
That's it! navigateToRoute
with named routes fixed it!
Thanks @jods4!
@jods4, just realised i have another issue... if i call router.reset()
, navigateToRoute
does not work, but my routes get duplicated each time within the router if i don't call it. Is there a way for me to clear current routes inside the router, before reconfiguring?
@ochart2 I don't know as I never use dynamic routes / router.reset()
myself. Maybe ask in the gitter channel!
I have a workaround for the menu reload: Hope it helps someone down the line.
`import {bindable} from "aurelia-framework"; import {Context} from './Context';
export class NavBar { @bindable router = null; routes = [];
constructor() {
Context.Register("ActiveChanged", this, this.setRoutes);
}
setRoutes(page, user) {
page.changeRoutes();
}
attached() {
this.changeRoutes();
}
private changeRoutes() {
this.routes = [];
this.routes = this.router.navigation;
}
get isLoggedIn() {
var token = localStorage.getItem("auth_token");
return !(token === null || token === undefined);
}
}`
I have an Context object that holds the security of a user, part of this Context is also an event system. I bound the Navbar to an array, in this case this.router.navigation, and when the User context switch I fire an event and reload the Navbar array. This force the authFilter to reload and revaluate security. For our application, this works perfectly.
I hope this helps someone.
Here is a simple solution to refresh the page:
in aurelia-history-browser.js, added a check in updateHash()
and if the new _href is equal to the old then reload the page from the cache (not the server).
Then, to refresh the page you just need to use the existing options:
router.navigateToRoute('watch', {}, **{ replace: true, trigger: true }**);
The updated code is copied below:
function updateHash(location, fragment, replace) {
if (replace) {
var _href = location.href.replace(/(javascript:|#).*$/, '') + '#' + fragment;
if (_href == location.href)
location.reload(false);
else
location.replace(_href);
} else {
location.hash = '#' + fragment;
}
}
Is it possible to allow the activationStrategy
to be set when navigating? Something like:
this.router.navigate("/url/here", {activationStrategy: activationStrategy.replace});
+1 to get a simple way to reload current view with or without the complete child/parent router tree.
The idea is to get something faster than window.location.reload(true);
for the end-user.
My use-case is also about changes everywhere on user login.
I didn't want to patch aurelia-history-browser.js as suggested by @gravsten. And I wasn't ready to add an event-handler in all modules too, like @mikeesouth did. Here is my dirty hack :
1/ define file reload.js :
export class Reload { activate() { window.history.back(); } }
2/ define file reload.html :
<template></template>
3/ in your app.routerConfigure add this route :
{ route: 'reload', name: 'reload', moduleId: 'reload' },
Then when you need to reload the page without reloading all the app, just call the reload module : window.location = '/#reload'
This is far to be perfect, because :
history.replaceState()
but it didn't work.Despite of those issues, this workaround is good enough for my use case. And I hope the changes will be easy to bring when ( if ) a route.reload() method appears somewhere in the future.
+1 for this feature and refreshing the entire hierarchy
Is there a solution to the original problem?
I believe that there are several workaround in the comments above. This feature is waiting a community contribution for implementation.
What does activationStrategy.invokeLifecycle
do versus the default?
@carusology activationStrategy has three modes: invoke lifecycle, replace, and no change. Invoke lifecycle is used when you're navigating to the same route with different parameters, which will use the can/Activate and can/Deactivate callbacks. Replace is used when navigating to a completely different route with a different module. No change is used when the incoming url represents no change to the route, which is important specifically in the case where you're navigating from /parent/1/child/1 to /parent/1/child/2, where the child has a invoke lifecycle and the parent is no change.
This is one of the highest rated feature requests on the router, however, even after reading all of the above, I don't fully understand the use case. There was some talk of changing Authentication which made sense to me. Are there any other use cases?
I'd happy to look at working on this feature request, but I don't feel like I fully understand the use case. I'm concerned I might solve the problems of some in this thread to the dissatisfaction of others.
My use case is when the user logs out of the app, thus the app needs to reload the page... this time without any user authentication (same route, but obviously the result is different). A similar use case is when the application needs to "reboot" due to user inactivity or other server-side decision, with the same consequence of the user being logged out. The above solution that I contributed on August 24, 2016 works great with the latest version of Aurelia.
@davismj Thanks for describing how invokeLifecycle
is intended to work. I'll give you a specific example of our use case.
Our product involves the management of data across several clinical trials. Users will only be managing data for one clinical trial at a time. However, once they are done for that trial, he or she will move on to the next one. We have done this by creating a widget that allows a user to select a different trial. When that occurs, we reload the current route with a different trial's contextual data.
We want the page to act as if the user is navigating to it anew. This is because we have a small amount of animation that takes place during the loading of all of our routes that signals to the user that the page's content is being refreshed even if the visual representation of the data did not.
I have pulled this off today by setting activationStrategy.invokeLifecycle
on all of my routes as I described in my StackOverflow answer. It sounds like I am using it correctly. However, I think a better, iterative improvement would be to set activationStrategy
during trials's context widget's navigate()
invocation instead of in the route config, as @glen-84 suggested here.
@carusology What do your routes look like? You probably want something like this:
config.map([
{ route: 'clinical-trial/:id', moduleId: 'pages/clinical-trial' }
]);
Then, when you navigate from clinical-trial/1
to clinical-trial/2
, the activation strategy will be invokeLifecycle
.
@gravsten Cool, thank you for the info. It seems like the one and only use case for the reload()
is a full on reload of the entire route tree. That is, it seems like no one really wants to reload the current module and not its parent. I'll take a look at your suggested change above.
About the clinical-trial/1
routes, it may seem a good idea here but often you don't want the user to know the ID for security and anonymity reasons. Stateful information is best kept outside of the URL.
@davismj
No, that is not what we want. Our routes, in a contrived example, are like this:
config.map([
{
route: 'subjects',
moduleId: 'pages/subjects',
activationStrategy: activationStrategy.invokeLifecycle
},
{
route: 'investigators',
moduleId: 'pages/investigators',
activationStrategy: activationStrategy.invokeLifecycle
},
{
route: 'sites',
moduleId: 'pages/sites',
activationStrategy: activationStrategy.invokeLifecycle
}
]);
A trial is very much like an account for most applications. It is a required, global piece of state that is verified in a PipelineStep
within the aurelia-router
and consumed by the view models. Most of our users have access to one at a time. Our data API certainly requires that level of sophistication (in a similar vein of routing as you described), but it is abstracted away outside of setting a global context in the GUI.
Think of an email account. If you're on your inbox (let's say foo.com/inbox
) and you have no mail messages, and you swap to a different account, your route will still be (foo.com/inbox
), but you want the user to understand that you loaded the new account's content even if both inboxes render identically because they are both empty.
Reinvoking the lifecycle causes an identical user experience to take place that occurs during all other navigation scenarios.
In case anyone's looking for a workaround, I tried this one
this.router.navigateToRoute(
YOUR_ROUTE_NAME,
{ replace: true }
);
and in the router configuration
configureRouter() {
config.map([
{
route: 'YOUR_ROUTE_URL',
name: YOUR_ROUTE_NAME,
activationStrategy: 'replace',
...
},
...
}
Feature description
It would be nice to have an API
router.reload()
or similar. Basically it would navigate to the current route.Respecting activation strategies means that it could do nothing, create a new VM or just invoke the lifecycle again. I am unsure if passing options to force a different activation strategy (e.g.
invokeLifecycle
instead of default) is useful or not.As I am not using child routers or viewports, am I unsure what behavior would make sense for those.
Use cases
Use cases could be that you've performed an operation on the server (e.g. save) and the impact on your VM is complicated and you prefer to load it from scratch than try to update it.
Current solutions
Trying to use
navigate
ornavigateToRoute
is currently doomed to fail, because of this check I think: https://github.com/aurelia/history-browser/blob/master/src/index.js#L191A workaround is to pass an additional "forcing" parameter with a random value or something that changes such as time.