angular-ui / ui-router

The de-facto solution to flexible routing with nested views in AngularJS
http://ui-router.github.io/
MIT License
13.53k stars 3k forks source link

Changing URL without changing state #64

Closed iliakan closed 10 years ago

iliakan commented 11 years ago

Here's the use case to prove my point.

I'm editing a Model. On save I want to change URL to /model/:id (id comes form server), but calling $state.transitionTo('model', {id: ...}) causes the state change and hence the model is RELOADED. As if it were not on client!

I'm trying to pass the existing model to the state, so that may keep it (by reusing in resolve): state.transitionTo('model', {model: modelObject}). But the normalization code stringifies the "model", so that's not an object any more.

Is what I'm doing conceptually wrong? Is there any really good reason to keep normalization which prevents it from working?

mariendries commented 10 years ago

+1

gregberge commented 10 years ago

+1

kevinlambert commented 10 years ago

@jjaburke91 Your solution worked perfectly for me. The abstract prevents the reload. Only difference is I used $state.transitionTo

jmwolfe commented 10 years ago

Is there any way dynamic params could work so that if you have a link to an URL with a dynamic param declared in its matching state, it will change the state params (maybe emit an event) but maybe not initiate a transition?

My issue is that we have a directive which creates a very long scrolling list of items, and we have avoided using angular in this directive's output so that we could avoid a painfully long $compile. So now each link in this list is a simple href URL which maps vi ui-route to a new state. However, that state transition is causing a complete refresh of some nested controllers with complex items in them, causing unneeded churn in the UI among other things. It would be way preferable to keep those controllers intact and maybe receive some sort of event noting the change.

The intercept feature seems most likely to help out here if I am not mistaken (just emit my own event if the url matches the state where I don't want to reload everything), but I could also see maybe dynamic params being woven in there somehow as well. Like maybe emitting the $stateChangeSuccess event or something, but not causing a rebinding cycle to happen.

If this isn't the correct place for this, please let me know the gitHub friendly way to approach this.

Great work on these features!!!

jjaburke91 commented 10 years ago

@jmwolfe If you have a look further up for my comment made on Aug 11, I found a solution / workaround to achieve a behavior very similar (if not the same) to the one you define.

jmwolfe commented 10 years ago

@jjaburke91 Thanks. I am looking to see if I can avoid switching to using search params, though. It would probably cause more change in my code base then I would like. However, if that's the only way, I may have to go there.

As an update: I was able to plunk this out and it's pretty clear routing doesn't have anything with our issue, and one of our other dev's pointed out that at some point we will be changing states and our app would have some issue at that point. So this is probably not a good pattern to follow!

lenardg commented 9 years ago

@nateabele any news on the dynamic-params branch getting merged into master?

AlJohri commented 9 years ago

to anyone else coming to this thread, this SO answer helped me a ton: http://stackoverflow.com/questions/23585065/angularjs-ui-router-change-url-without-reloading-state

angular.module('app').config(['$urlRouterProvider', function ($urlRouterProvider) {
    $urlRouterProvider.deferIntercept();
}]);
angular.module('app').run(['$rootScope', '$urlRouter', '$location', '$state', function ($rootScope, $urlRouter, $location, $state) {
    $rootScope.$on('$locationChangeSuccess', function(e, newUrl, oldUrl) {
      e.preventDefault();
      if ($state.current.name !== 'MYSTATE') {
        $urlRouter.sync();
      }
    });
    $urlRouter.listen();
}]);
mrzepinski commented 9 years ago

@AlJohri But when sync() method is invoked all queued logic (including controllers) is also invoked.

For example I have smth like this: 1) I am on URL: /act/123456 and controller ActCtrl 2) I click on smth and URL change to /act/123456?unit=1 without sync() 3) I click on anchor with URL to different state and state reloads view but invoke at first ActCtrl and then his own controller.

mightyguava commented 9 years ago

@nateabele Do you have an updated timeframe on when dynamic-params might get merged?

dynamic-params would be really helpful to a few projects we are working on as well. I'm currently using @jjaburke91's workaround but that has its own quirks.

In general, I think dynamic-params would be a big help for any view that tries to represent a resource with multiple nested resources on a single page.

kelp404 commented 9 years ago

When will it merge into master?

bahadirbb commented 9 years ago

@nateabele is there a reason for not merging dynamic-parameters to master? if so maybe we can help?

gentledepp commented 9 years ago

@jjaburke91 thank you for your solution - works like a charm! :+1:

jpmckearin commented 9 years ago

@nateabele I'm here to contribute to this if you need help.

btm1 commented 9 years ago

This is very confusing. All I want to do is update the params in the url without reloading the controller to the current state and also have that updated params be reflected in $state.params. It sounds like there are two methods in the works that could possibly fix this issue.

$urlRouterProvider.deferIntercept(); -> how does this solve my problem? I get the concept: you call this in the config function for your app and you define some logic for when to sync and not sync the url BUT how does only have access to the new url and old url help me determine an appropriate line of logic?

@AlJohri in your example your showing:

  if ($state.current.name !== 'MYSTATE') {
    $urlRouter.sync();
  }

This doesn't look scalable to me. It's specific to a state name not a situation where a method is being used and in that method i specify that i don't want the controller to reload.

Can someone please give me an example that shows me how i could use this to update params on a url without reloading the controller from soup to nuts?

thanks

btm1 commented 9 years ago

So my solution for now is:

• reloadOnSearch: false ( to keep the page from reloading with params change ) • $location.search ( to change params ) which isn't ideal because it's not a $state method there is a loss of communication between the two systems • sync then doesn't work to let $state know about the new params so in order to keep the params and $state.params in sync I extend $state.params at the same time I apply the new params to the url. The issue with this is that you could end up with something in $state.params that's not a valid param based on the $state defenition

I think dynamic params is the solution I am actually looking for an I'm sure I could probably hack $urlRouterProvider.deferIntercept(); to be something similar to what I'm doing right now.

Mirodil commented 9 years ago

Does someone has solution?

sfakir commented 9 years ago

+1

cheahkhing commented 9 years ago

+1, i digged up a lot of old issues, and it seems like a lot of them didn't get merged back to the master for release. Not sure what's the reason, but somehow all these issues related to dynamic state parameters are dead. :(

hazzik commented 9 years ago

Please reopen this issue as the branch still has not been merged

zaggino commented 9 years ago

2 years from the initial issue and this is still not easily achievable?

My apologies, notify:false mentioned above works fine.

elimydlarz commented 9 years ago

Thanks zaggino, I saw and used 'notify: false' literally 15 minutes after you posted that here and I had missed the earlier mention =O You may have saved me lots of time.

spiffy2man commented 9 years ago

I have been doing this and it has been updating the URL just fine and has not been changing my state: $state.go('my.state', {id:data.id}, {notify:false, reload:false}); And to remove the id from the url: $state.go('my.state', {id:undefined}, {notify:false, reload:false});

I believe that the same thing could be applied to whatever is needed, though I may be missing something or misunderstanding something in this issue. (ui-router version 0.2.15)

l-liava-l commented 9 years ago
$state.go('my.state', {id:data.id}, {notify:false, reload:false});
//And to remove the id from the url:
$state.go('my.state', {id:undefined}, {notify:false, reload:false});

//or

$state.params.id = '123'
$state.transitionTo($state.current, $state.params, { 
       inherit: true, 
       notify: true, 
});
NorthMcCormick commented 9 years ago

I have kind of an odd case where I am building a highly responsive app for a company. Basically it has to determine whether to only show the list or the item based on the routed url. If you go to the url directly it has to know what to load, but if you're already in that state, it can't reload the state again because it cases lag and flicker just because the amount of data. I ended up doing a crazy mash up of some of the previous answers, and what is amazing is that the events for going into the state still fired!

So now using:

$state.transitionTo('newnav.view', { code : ing.code });

then listening for

$scope.$on('$stateChangeSuccess', function(event) {
});

I'm able to achieve this awesome no-reload but data syncing structure in my split view. I hope this little insight helps someone! Thank you all for your insightful comments previous of this, it saved my life.

JohnBernardsson commented 9 years ago

@l-liava-l Lot of thaks, that was exactly what I needed :)

robertu7 commented 9 years ago

@l-liava-l thanks.

estvmachine commented 9 years ago

@l-liava-l thanks x 10000

btm1 commented 9 years ago

This is absolutely not fixed can we please reopen this issue?

The original requirements were to be able to change the url without the state reloading:

deferIntercept sort of allows you to do this:

$urlRouterProvider.deferIntercept();

if you're willing to use a method to change the url that's outside of the API i.e. $location.search

what it does allow you to do: • it allows you to change the params without reloading the state • If done correctly you can use $location.search without having the use reloadOnSearch:false which creates a crappy experience

what it doesn't do: • the updated params are then not reflected in $state.params so it becomes a manual process. You have to keep track of and then merge the params back into the $state.params

If you go with an apposing solution which is a hack of $state.go which is:

$state.go($state.current.name,params,{
    // prevent the events onStart and onSuccess from firing
    notify:false,
    // prevent reload of the current state
    reload:false, 
    // replace the last record when changing the params so you don't hit the back button and get old params
    location:'replace', 
    // inherit the current params on the url
    inherit:true
});

On first glance this looks like a good solution and it seems to work for 5 minutes until you try to navigate to another state.... the router then reloads the same state you're on including the controller again and doesn't navigate to the other state.

It sounds like dynamic params is the solution to this problem and I see what looks like the beginning of dynamic params here https://github.com/gGonz/ui-router/commit/b43ece41d3837ca8f5461f418a53818457dea1a4 but it's not being talked about or as far as I can tell it's not merged in. Even if it is there is no documentation about it. Also from the example given on the top of that page that only looks like a url param /myState/:myparam how do you use the params object to declare a dynamic query param i.e. /myState?myparam

Could someone please clear all of this up.

matheusdavidson commented 9 years ago

@btm1 solution worked for me! Changing the url without reloading the controller and without the { notify: false } state reload bug.

tysonnero commented 9 years ago

@l-liava-l and @btm1 The solutions worked for me to not reload the Controller, however, my resolve methods are still firing with each url change. Any ideas?

btm1 commented 9 years ago

@tysonnero @l-liava-l honestly the best way to do this right now is to use $urlRouterProvider.deferIntercept();

i.e.

app.run(function($rootScope, $urlRouter) {
       $rootScope.someVariableThatYouToggle = true;
    //------------------------ this keeps the current controller form reloading every time you change a query param on the url ------------------------//
    $rootScope.$on('$locationChangeSuccess', function(e) {
        // Prevent $urlRouter's default handler from firing
        e.preventDefault();

        if($rootScope.someVariableThatYouToggle) {
            $urlRouter.sync();
        }
    });
});

where right before you change the url you set $rootScope.someVariableThatYouToggle to false and then change your param and then change it back again. It's ugly I know but it works:

this.queryString = function(params) {
        //------------------------ tell the router to stop listening to url changes: top of run function application.js ------------------------//
        $rootScope.someVariableThatYouToggle = false;

        $timeout(function(){
            var search = $location.search();

            //------------------------ merge new params into search params ------------------------//
            if (angular.isObject(params)) {
              angular.extend(search, params);
            }

            //------------------------ replace them on the url ------------------------//
            $location.search(search).replace();

            //------------------------ sync $state.params with new params ------------------------//
            angular.extend($state.params,search);
            angular.extend($stateParams,search);

           $rootScope.someVariableThatYouToggle = true;
      },0);
};
spiffy2man commented 9 years ago

So I am a little bothered that I-lava-I gets credit for what I posted earlier that day, but I had many issues with this as well. I found a better solution on stackoverflow, though I don't know from exactly where.

$state.current.reloadOnSearch = false;
$location.search('query', paramValues);
$timeout(function () {
  $state.current.reloadOnSearch = undefined;
});
btm1 commented 9 years ago

@spiffy2man i'm pretty sure that's the same thing I posted. Either way who really cares who gets credit for what. We're all just trying to solve a problem here. Also it looks as thought they've fixed the issue with the current state's controller getting reloaded when using the $state.go hack so all of this may be irrelevant with the next release.

danielabar commented 9 years ago

Does anyone know this is resolved in v 0.2.15?

I'm trying to make use of this feature with client side pagination. I have a state with a resolve, that fetches all users from the server. Then a client side pagination control. On each click of page number, I'd like to change the url query parameter to append, for example "?page=5" , however, currently it reloads the state, making the server api call again.

app.js

$stateProvider
      .state('users', {
        url: '/users?page',
        templateUrl: 'views/user/users.html',
        controller: 'UsersController as users',
        resolve: {
          allUsers: function(ApiService) {
            return ApiService.allUsers();
          }
        }
      })

users.html

<div class="col-md-4">
  <ul class="well nav nav-pills nav-stacked">
    <li data-dir-paginate="user in users.userList | itemsPerPage: users.pageSize"
      current-page="users.currentPage"
      ui-sref-active="active">
        <a ui-sref="users.detail({id:user.links[0].href})">{{user.firstName}} {{user.lastName}}</a>
    </li>
  </ul>
  <dir-pagination-controls max-size="6" on-page-change="users.pageChangeHandler(newPageNumber)"></dir-pagination-controls>
</div>

UserController.js

 this.pageChangeHandler = function(pageNum) {
      $state.go('users', {page: pageNum});
 };

This works, however, ApiService.allUsers() is being called every time user clicks a page number.

btm1 commented 9 years ago

@danielabar see my answer where I describe the the various methods for changing a query param without the state reloading the controller... it doesn't appear that 2.16 was release yet and I think the fix so this can be accomplished by just using $state.go will be in this release. I'm not sure when it will happen.

danielabar commented 9 years ago

@btm1 I got a little confused with all the various code snippets in this thread, which one(s) formed the total solution. So just to confirm, is it the code posted in this comment?

btm1 commented 9 years ago

@danielabar that's correct yes .... the first part is put in the run function and the second part is an example of executing a param change. It's not ideal by any means but it does work for the time being.

christianmalek commented 9 years ago

@btm1 But this still triggers the onEnter event, doesn't it?

btm1 commented 9 years ago

@Phisherman no if you do the $urlRouterProvider.deferIntercept(); method and pause listening to change the url it will not cause any events to be fired.

davidcunha commented 9 years ago

I wanted to refresh the query parameters based on some form change. This is working for me:

$state.go('items', { value: value }, {notify:false, reload:true})
cseils commented 9 years ago

@davidcunha This works for me too, until I need to go to another state. In your example, once I go to another state, then the controller for 'items' is reloaded before the new state is loaded.

btm1 commented 9 years ago

@davidcunha @cseils yes this method doesn't really work. You'll need to use $urlRouterProvider.deferIntercept();

squadwuschel commented 8 years ago

+1

the

   $state.go('items', { value: value }, {notify:false, reload:true})

is still not working right. When you navigate to the next view, the current view is also loaded again.

tachyon-ops commented 8 years ago

Is there a way to just change the url? Idea: change the language/locale after which a 'translateChangeSuccess' (from angular translate) is issued and from there just change the url. State is the same, so no need to actually reload it or execute the controller again.

I've used a number of combinations from here but it doesn't seam to work, and I suspect it's because I actually have to change the $stateParams to their localized values.

Any idea?

cseils commented 8 years ago

I'm using this inelegant solution of changing the url and not causing the controller to fully reload when leaving the state.

I put this at the top of the controller:

app.controller('TestController', function($state) {
   if ($state.transition) return;
   ...
});

Then just call:

$state.go('items', { value: value },
  {
    // prevent the events onStart and onSuccess from firing
    notify: false,
    // prevent reload of the current state
    reload: false,
    // replace the last record when changing the params so you don't hit the back button and get old params
    location: 'replace'
  });

The controller still reloads when the state has changed, but it exits straight away. Not a great solution, but so far it works for me.

tachyon-ops commented 8 years ago

Yes. Got it to work now. The concept is as stated throughout here. I translate my url like this:

//on app.run()
$rootScope.$on('$translateChangeSuccess', function(){
      $rootScope.routerTranslate();
})
$rootScope.routerTranslate = function(){
   if($state.$current.name){ //if there's an actual state
      //newParams => iterate through params and translate them
      $state.go($state.$current.name, newParams, {notify: false});
   }
};

It's fine now. No reload (I was not creating a proper 'newParams' object), and only url change as intended.

squadwuschel commented 8 years ago

@ nmpribeiro yes that works, but when you navigate to a other url/state the current (old) state is also completly reloaded and this can create some troubles like wrong url, ....

tachyon-ops commented 8 years ago

@squadwuschel it dependes. I am only scratching the surface since I only know angular for a couple of months. Buy here is my method:

  1. I have a 'routes' array in the translations .json files from angular translation that I actually load before the angular is bootstraped so I can know of all my routes in advance.
  2. My states are constructed from native parameters.
  3. that function I showed will iterate all parameters and check if there is a native key for each, if such then translate it. If not, use the native key and translate that native key.

That means, if I have a new state it will basically re-write the process once a translation is triggered. Of course, if I change state I also do this 'routerTranslate' function.

So, that means when I reload to a new state, that new state will get translated as well. All the ways I've thought about it these one seams the best. This translation is done on rootScope, so no need to bother about it anymore. If there are translations, awesome, if there aren't, not a problem at all.

averas commented 8 years ago

Is there any change in behaviour in the new UI router 1.0 here? I've tried most, if not all, of the suggested solutions in this thread but I am not able to achieve what I want. Basically I want to show a modal (an image light box) with a specific URL, exactly like facebook does it when you look at images. The image light box modal should show up, the URL change, and when you close the modal the URL is changed back. All this so that you can copy-paste or deep-link the URL to the image if you want to.

I actually have achieved the described behaviour, but not without my application reloading the controllers once I get back from my light box and start navigating again.