aurelia / router

A powerful client-side router.
MIT License
120 stars 115 forks source link

Suggestion: router.reload() #201

Open jods4 opened 9 years ago

jods4 commented 9 years ago

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 or navigateToRoute is currently doomed to fail, because of this check I think: https://github.com/aurelia/history-browser/blob/master/src/index.js#L191

A workaround is to pass an additional "forcing" parameter with a random value or something that changes such as time.

EisenbergEffect commented 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.

jods4 commented 9 years ago

@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.

EisenbergEffect commented 9 years ago

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.

dpinart commented 9 years ago

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

kiryaka commented 9 years ago

+1 to this feature

dpinart commented 9 years ago

It's surprising there's no way to reload/refresh current view/view-model... I'd just expect activate method to be called again

jvkassi commented 8 years ago

+1 Is there any way to achieve that ?

EisenbergEffect commented 8 years ago

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.

kiryaka commented 8 years ago

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.

jods4 commented 8 years ago

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 }) ?

jwahyoung commented 8 years ago

@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.

bryanrsmith commented 8 years ago

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.

xiaoweil commented 8 years ago

Is there still a plan to add the force parameter to the router/navigateToRoute's option?

kiryaka commented 8 years ago

@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.

jods4 commented 8 years ago

@bryanrsmith We need this and reload all child routers would work for us as well, mostly because we don't have child routers ;)

ochart2 commented 8 years ago

+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 });
ashbrener commented 8 years ago

We are also desperately waiting on a fix for this. Is there an ETA ? Thanks.

joelclimbsthings commented 8 years ago

+1

ochart2 commented 8 years ago

also seems that this issue is closely related #237 (after router.reset() and router.configure() the view isn't updated)

DanielRNichols commented 8 years ago

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.

ashbrener commented 8 years ago

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

mikeesouth commented 8 years ago

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.

nporcu commented 8 years ago

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 :)

jods4 commented 8 years ago

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.

ochart2 commented 8 years ago

@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.
jods4 commented 8 years ago

@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.

ochart2 commented 8 years ago

@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?

ochart2 commented 8 years ago

@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

ochart2 commented 8 years ago

@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?

jods4 commented 8 years ago

@ochart2 I think you messed up the parameters. navigateToRoute(routeName, routeParams, options) Note that there is one additional parameter compared to navigate().

ochart2 commented 8 years ago

That's it! navigateToRoute with named routes fixed it! Thanks @jods4!

ochart2 commented 8 years ago

@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?

jods4 commented 8 years ago

@ochart2 I don't know as I never use dynamic routes / router.reset() myself. Maybe ask in the gitter channel!

Ronmenator commented 8 years ago

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.

gravsten commented 8 years ago

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;
    }
  }
glen-84 commented 8 years ago

Is it possible to allow the activationStrategy to be set when navigating? Something like:

this.router.navigate("/url/here", {activationStrategy: activationStrategy.replace});
Stopi commented 8 years ago

+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 :

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.

alexculea commented 8 years ago

+1 for this feature and refreshing the entire hierarchy

krostkowski commented 8 years ago

Is there a solution to the original problem?

EisenbergEffect commented 8 years ago

I believe that there are several workaround in the comments above. This feature is waiting a community contribution for implementation.

carusology commented 7 years ago

What does activationStrategy.invokeLifecycle do versus the default?

davismj commented 7 years ago

@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.

gravsten commented 7 years ago

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.

carusology commented 7 years ago

@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.

davismj commented 7 years ago

@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.

davismj commented 7 years ago

@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.

gravsten commented 7 years ago

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.

carusology commented 7 years ago

@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.

maryprazyan commented 3 years ago

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',
        ...
      },
      ...
  }