Skip to content

1.0 URL change without changing state #2679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
orangesoup opened this issue Apr 6, 2016 · 21 comments
Closed

1.0 URL change without changing state #2679

orangesoup opened this issue Apr 6, 2016 · 21 comments

Comments

@orangesoup
Copy link

I have a state, let's call it x. I can open many modals (one at the same time) on this page/state. What I would like to do is when I open a modal I change the "state" to y (with $state.go). I say "state", because I only want to change the url, nothing else. In the old version I could do a simple $state.go(..., ..., {location: 'replace', notify: false}) (I know it wasn't intended for this, and it wasn't even perfect since it always re-resolved the stuff...).

Also when I close the modal I want to change the url back to the original one. I don't want to re-render, re-resolve anything. Just simply change the url.

I know there's plenty of new hooks in 1.0 (I'm using alpha.4), but I simply can't find a way to do this.

Any help would be appreciated!

@orangesoup orangesoup changed the title 1:0 URL change without changing state 1.0 URL change without changing state Apr 6, 2016
@shmool
Copy link

shmool commented Apr 6, 2016

@orangesoup did you try using dynamic parameters? I think it may be a good solution for you.
See how it works in the plunker @christopherthielen prepared: http://plnkr.co/edit/MkGSGfPXVC8b6qYqKKjP?p=preview (Thanks again Chris! :) )
Define your parameters this way:

$stateProvider.state({
    name: 'home',
    url: '/home/:modalName',
    template: template, 
    controller: controller,
    params: { 
      modalName: { 
        dynamic: true 
      }
    }
  });

Then transition to the same state, changing only the modalName param:
ui-sref=".({ modalName: 'modal1' })" or $state.go('.', { modalName: 'modal1' })
You can use uiOnParamsChanged hook to do stuff when the change occurs.

Tell me if it works out for you :)

@orangesoup
Copy link
Author

I'm not quite sure it works for me (but I might be missing something).

Let's say I have a state:

$stateProvider.state({
    name: 'profile',
    url: '/profile/:id',
    template: template, 
    controller: controller,
    resolve: { ... }
  });

And another state:

$stateProvider.state({
    name: 'whatever',
    url: '/whatever/:id',
    template: template, 
    controller: controller,
    resolve: { ... }
  });

When I'm on the profile page I click on a button and then the modal opens. At this point I want to do something like $state.go('whatever', {id: 123}, {location: 'replace'}). Basically I just want to change the URL, I don't want to resolve anything from the state whatever. When I close the modal, I would change the URL back to profile/456.

The point is, I want the url to be what is defined for the state whatever so when someone links it (and opens it directly), it points to that state and loads the template and everything else. With your method I can only have my urls be like profile/id/modalID if I'm right (but please correct me if I'm wrong.)

@shmool
Copy link

shmool commented Apr 6, 2016

@orangesoup My Idea is that the param is the indication of whether and which modal is open. So you'll have one state with url: '/:modalName/:id' where the base view is what you present in profile, and it opens a modal in case the param is whatever. In this case you can't have whatever defined as a state with its own controller, resolve etc. (You can use UI-Bootstrap's Modal which adds these abilities.)

Routing to /profile/123 will show only the base view, and routing to /whatever/456 will route to the same view and open the modal. To return to the base view, just route to the same state with {modalName: 'profile'}.

Another method you should consider is having the modal states as children of the base view. This way the base will not be exited when you open the modal or re-entered when you close it, so its resolved data will be remained. And you'll be able to attach different controllers and resolves to the modal via their states. But then you can't have a different URL path for the two situations: either have profile/:id1/whatever/:id2 or give the parent an empty path.

If anyone has another idea, I'll be happy to hear. (I'm investigating UI-Router thoroughly these days preparing for a lecture about routing in Angular...)

@orangesoup
Copy link
Author

@shmool Thanks for the suggestions! The first one is good'ish, but still it's not quite what I want. Still, the idea is good, thanks for the explanation!

However, I'm wondering whether it is such an edge use case really. There is a very similiar use case on Instagram for example. When someone clicks on an image, the url changes, after closing the modal the url goes back to the original one. When you reload the page when the modal is open, then the actual state gets loaded fully, not the modal. Basically this is what I would like to get.

@orangesoup
Copy link
Author

For now I decided to use $location.path('/w/' + id).replace() and $location.path('/profile/' + uid).replace(). This gets the job done, since there is no state registered with url: '/w/:id' so effectively it never gets changed from profile, but the url still changes. Also I set up a redirect, so whenever a user opens a link /w/123 it's redirected to /whatever/123.

I still think it's a hacky way to do the job, but until there is a better solution I'm gonna stick to this I guess.

@christopherthielen
Copy link
Contributor

Wow, this a great discussion!

@orangesoup your use case can be solved using "sticky states" from ui-router-extras.

$stateProvider.state({
    name: 'profile',
    url: '/profile/:id',
    sticky: true,
    resolve: { ... }
  });
And another state:

$stateProvider.state({
    name: 'whatever',
    url: '/whatever/:id',
    resolve: { ... }
  });

A transition from profile to whatever would inactivate the profile state, enter the whatever state, and update the url to whatever/:id. Your view could detect this and render as a modal. Transitioning back to profile would exit the whatever state and reactivate the profile state.

Refreshing the browser on the modal would enter whatever, and render as a non-modal.


Unfortunately, sticky states is not yet compatible with the 1.0 alpha. I have plans to make it compatible, but I am currently devoting time to finalizing 1.0, and haven't spent much on extras.

@orangesoup
Copy link
Author

@christopherthielen Heh, thanks! I just upgraded to 1.0 ~2 days ago to see if I can solve this problem with it. Unlucky, lol. The hacky way I mentioned above is kinda buggy when using the back button and such, so I might just revert back to 0.2.x and use ui-router-extras then!

Is there any ETA for the 1.0 compatibility?

@christopherthielen
Copy link
Contributor

@orangesoup there is no ETA currently. I will probably have to wait until after 1.0 is in release candidate, and after ui-router-ng2 (angular 2 version) is in beta.

@eddiemonge eddiemonge added the 1.0 label Apr 20, 2016
@ShakurOo
Copy link

ShakurOo commented Jun 24, 2016

Hello do you have find a solution ?

I can't trust the users input (Who can write space between words, apostrophes, uppercases, etc). I want redefined it like a great url SEO friendly when the resolve function returns me success. Why ? because my webservice send me the trust slug that I can use.

Example :

The user writes : http//mydomain.com/city/Newcastle upon Tyne .
The API returns me the correct slug : newcastle-upon-tyne-0191

I want reverse the user inputs with this slug like this before the templates load : http//mydomain.com/city/newcastle-upon-tyne-0191.

I'm using UI router, it is a way for do this ($state, $stateparams) ?

Here is the part of my router code which work with :

 .state('villes.ville', {
        url: "/:ville",
        parent: 'villes',
        templateUrl: "partials/ville.tpl.html",
        metaTags: {
            title: '{{city.label}} ({{city.zip_code}})',
        },
        resolve: {
            city: function(City, $state, $stateParams){

                return City.get({
                    slug: $stateParams.ville,
                    shape: true,
                    details: true

                }).$promise.then(
                    function(data){ return data },
                    function(error){ $state.go('/404') });
            }
        },
        controller : 'villeCtrl'               
    })

I tried to inject $location.path('villes/' + data.slug).replace(); in promise callback of the resolve function but the state is reactivate a second time and the resolve function called a second time too ! I don't want this because I don't need it and because it keep the view on standby even if I use directive ng-cloak. Like @orangesoup I want just update last part of the URL just before the controller is loaded.

@christopherthielen
Copy link
Contributor

@ShakurOo are you using ionic? Noticed cache: false in your state def.

@ShakurOo
Copy link

ShakurOo commented Jun 24, 2016

Oups I forgot to take off it, no I don't use ionic @christopherthielen .

@christopherthielen
Copy link
Contributor

christopherthielen commented Jun 24, 2016

@ShakurOo I think you can use ui-router 1.0 hook to validate the parameter before allowing the transition.

$transitions.onBefore({ to: 'villes.ville' }, function($transition$, $injector) {
  let slugService = $injector.get('SlugService');
  let ville = $transition$.params().ville;

  // user provided arbitrary input and we don't recognize it as a the canonical param
  if (!slugService.isCanonical(ville)) {
    // return a promise from the hook.  the promise fetches the canonical slug
    return slugService.loadCanonicalSlug(ville).then(function (canonicalSlug) {
      // after the slug is loaded, redirect to the same state with the canonical param value
      return $injector.get('$state').target($transition$.to(), { ville: canonicalSlug }, { location: 'replace' });
    });
  }
});

Then you just need to implement SlugService

@ShakurOo
Copy link

How thank's @christopherthielen I never used this before. I using the latest version of UI Router (V0.2.18) but I can migrate to the V1. I'll be back when I have succeeded integrating this hook :)

@ShakurOo
Copy link

ShakurOo commented Jun 25, 2016

@christopherthielen I tried to implement your hook in the run() but I have a infinite loop :

I ask myself if the return $transition don't reload the entiere state ?

  angular.module('app.config', [])
    .run(function($rootScope, $state, $stateParams, $transitions){

        $rootScope.$state       =  $state;
        $rootScope.$stateParams = $stateParams;

        $transitions.onBefore({ to: 'villes.ville' }, function($transition$, $injector) {

            var ville           = $transition$.params().ville;
            var slugService     = $injector.get('Cities');

            return slugService.getSlug(ville).then(function (canonicalSlug) {

                // The console.log loop
                console.log(canonicalSlug)

                return $injector.get('$state').target($transition$.to(), { ville: canonicalSlug }, { location: 'replace' });
            });
        });
    });`

@adamreisnz
Copy link

adamreisnz commented May 3, 2017

Hello! A year has passed almost since the last comment, and it seems I have stumbled onto this issue as well. My use case is as follows.

I am displaying a bookings page, which has a date picker on it. The currently active date is always present in the URL as a state parameter, so that people can copy paste the URL easily or bookmark certain dates etc.

The problem is that when a date changes, the controller's $onInit method is fired again, which is unnecessary.

I used to tackle this with the {notify: false, location: 'replace'} options, which worked in 0.x.x, but now after having upgraded to 1.0.0 it doesn't appear to work anymore. Furthermore, the docs specify that notify has been deprecated.

I tried to use {reload: false, location: true} instead, but to no avail. It still seems to reload the route and call $onInit on my controller.

Any thoughts on what I can do to prevent the $onInit calls? Why isn't reload: false working? According to the docs it seems to be exactly what we need:

Re-render the views (controllers and templates)

So assuming when false, it won't re-render the view.

This is quite crucial to our application, and I would prefer not to resort to having to update the URL manually using the $location service.

Edit: actually event just changing the path with the $location service does not work, because ui-router picks up on that and then proceeds to reload the route. And while I'm at it, window.history.pushState() doesn't work either due to the same reason... :(

Looks like ui-routers states are really persistently linked to the URL. Is changing it without changing state not possible at all?

@adamreisnz
Copy link

adamreisnz commented May 3, 2017

Upon yet further digging and deep analysis of the docs, I've stumbled upon the dynamic flag for a parameter, which seems to help!

https://ui-router.github.io/ng1/docs/latest/interfaces/params.paramdeclaration.html#dynamic

I think that probably closes this issue as well!

It might be good to mention the existence of this parameter in the deprecated notify option.

@alidzen
Copy link

alidzen commented Nov 23, 2017

@adamreisnz thank you very much for your solution and saved my time! I spent several hours to fix this problem and read the documentation. But I would have spent more time if not for you.

@adamreisnz
Copy link

Glad I could help!

@sztrzask
Copy link

sztrzask commented Dec 5, 2017

Hi,

I can't get it to work. Given this state declaration

searchState: Ng1StateDeclaration = {
            name: 'search',
            url: '/search/:searchType/:id/',
            templateUrl: 'Template/SearchObjects/Index',
            params: {
                searchType: {
                    dynamic: true,
                    value: ''
                },
                id: {
                    dynamic: true,
                    value: ''
                }
            }
        };

when trying to update the id and searchType params the URL doesn't change.

        this.$state.go('.', { searchType: type, id: id });

I've checked the output of the .href with the same values and it does gives back the correct url - but it just not update. I am in html5 mode (where the urls don't have the #!).

@alidzen
Copy link

alidzen commented Dec 5, 2017

@sztrzask according to this doc: https://github.com/angular-ui/ui-router/wiki/Quick-Reference#stategoto--toparams--options
try to change your state code to:
this.$state.go('search', { searchType: type, id: id });

@stale
Copy link

stale bot commented Jan 24, 2020

This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.

This does not mean that the issue is invalid. Valid issues
may be reopened.

Thank you for your contributions.

@stale stale bot added the stale label Jan 24, 2020
@stale stale bot closed this as completed Feb 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants