Skip to content
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

State transitions similar to Pinterest's overlay. #317

Closed
CMCDragonkai opened this issue Aug 13, 2013 · 23 comments
Closed

State transitions similar to Pinterest's overlay. #317

CMCDragonkai opened this issue Aug 13, 2013 · 23 comments

Comments

@CMCDragonkai
Copy link

What would be the best way to implement Pinterest's overlay mechanism.

If you go to Pinterest and you click on one of the items, it brings up an overlay of the item. It also does a couple other things:

  1. the URL changes to the unique identifier to that item. (thus allowing the ability to share and save the url)
  2. When you hit the back button or close button, it transitions back to the original home state.
  3. Now if you go and just copy that URL straight into the browser and reload. It loads into a unique state that only contains that item, and it is no longer an overlay of the "homepage state".

Obviously I cannot just match the unique item URL, because that would prevent the unique item page from being loaded, only the overlay.

My ideas are currently use ui-bootstrap's dialog service along with 3 states. Home state, item child state of the home that doesn't have a URL parameter. And an item state. The item state would match the URL, so when a user accesses the URL directly it will navigate to the item's page. But whenever I want to bring up the overlay, I'll manually use transitionTo() to get the item child state. I haven't tried just yet, but I feel this may have problems regarding the back button.

@nateabele
Copy link
Contributor

See the ui-bootstrap $dialog example in the FAQ. You can set the type of animation through $dialog.

@CMCDragonkai
Copy link
Author

Hey @nateabele, I did have a look at that example. But would it work with the browser's back button? I would also like to move that logic to a controller not an custom onEnter function. There doesn't seem to be any documentation on the onEnter itself. Furthermore that example did not look into how you would differentiate between the overlay and directly accessing the item.

@nateabele
Copy link
Contributor

But would it work with the browser's back button?

Yes.

I would also like to move that logic to a controller not an custom onEnter function.

Nope. Can't do that without a ui-view. At best you could put all the code in a service.

@nateabele
Copy link
Contributor

Furthermore that example did not look into how you would differentiate between the overlay and directly accessing the item.

Sorry, what item?

@CMCDragonkai
Copy link
Author

That's fine. I can handle a child ui-view on the homepage. But could you elaborate?

Regarding the item. As I said for Pinterest. There is the homepage and the items. When you click on the items, they open up an overlay for that item. But when you access the item's URL directly, it goes to a separate page which only houses the item. The URL of course is shown when the overlay is active.

It's easier to understand if you just go to Pinterest and click on one of the items.

@nateabele
Copy link
Contributor

It's easier to understand if you just go to Pinterest and click on one of the items.

@CMCDragonkai I went to pinterest.com. It wants me to sign up. I am not signing up for Pinterest. Sorry.

But when you access the item's URL directly, it goes to a separate page which only houses the item.

This is up to your server implementation, and unrelated to Angular and UI-Router.

That's fine. I can handle a child ui-view on the homepage. But could you elaborate?

I guess you could put a hidden ui-view on the page that you don't use, then use the controller to launch the $dialog per normal

@braco
Copy link

braco commented Aug 20, 2013

Facing this same challenge.

For

Now if you go and just copy that URL straight into the browser and reload. It loads into a unique state that only contains that item, and it is no longer an overlay of the "homepage state".

I'm thinking I'll probably have to check the state of the page onEnter:... and either pop up a modal or render the entire page there.

If nothing else, there could just be two states.

  1. /quick-details/foo
  2. /details/foo
    and inside of quick-details, redirect:
$state.transitionTo("details")

when it shouldn't be a modal

@braco
Copy link

braco commented Aug 20, 2013

The main problem here is that we need two states on one url. One of the states can only be activated during a click event, the other should be the default. The template layouts may be radically different, so they need their own config tree.

Is there an obvious way to do this that I'm missing?

@nateabele
Copy link
Contributor

Again, I believe this is the responsibility of your server implementation.

@braco
Copy link

braco commented Aug 20, 2013

Why would one state be separated from everything else? I must be missing something. I imagine that there would be exceptions where you want to link to a full details page and not the modal, in which case the logic is still up in the application and state machine, not on the server.

Is there no way to do what I described?

@nateabele
Copy link
Contributor

I've seen other apps that do what you describe. You're not talking about a separate state, you're talking about a separate page-load.

@CMCDragonkai
Copy link
Author

I just went with not using ui-router for the overlay. Seemed difficult. Instead the overlay is manually triggered on a click and the bootstrap dialog service. I then set the url to the full page state and cancel the state transition. But I think ui-router should provide a more elegant capacity for this.

@nateabele
Copy link
Contributor

@CMCDragonkai If you'd like to patch ui-router to create a demo/proof-of-concept, I'd be happy to take a look.

@braco
Copy link

braco commented Aug 21, 2013

@CMCDragonkai Could you share your code for this?:

set the url to the full page state and cancel the state transition

@CMCDragonkai
Copy link
Author

Oh I haven't wrote it yet. I intend to very soon.

@CMCDragonkai
Copy link
Author

Hey @braco I figured it out. And with the help of ui-state too!

Basically all you need is an anchor link with:

  1. href attribute (or ng-href) (to the link of the item)
  2. ng-click="openOverlay()"

The ng-click should be a function that opens up the overlay. This can be done using ui-bootstrap's dialog service. Kind of like this:

                ////////////////////////
                //  OVERLAY HANDLING  //
                ////////////////////////

                //setting up the overlay options
                var dialog = $dialog.dialog({
                    backdrop: false,
                    keyboard: true,
                    dialogClass: 'modal idea_overlay',
                    templateUrl: 'idea_overlay.html',
                    controller: 'IdeaOverlayCtrl'
                });

                //we want to cancel the state change to ideas, and instead launch an overlay
                //however we must keep the URL to the idea
                $scope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
                    //state change is prevented from home to ideas
                    //but the URL is preserved
                    if(fromState.name === 'home' && toState.name === 'ideas'){
                        event.preventDefault();
                    }
                });

                /**
                 * Opens the overlay. This modifies the dialog options just as it is
                 * opening to inject the ideaId and locationParams. This also registers
                 * a closing callback when the overlay closes.
                 * @param  {Number} ideaId Id of the Idea
                 * @return {Void}
                 */
                $scope.openIdeaOverlay = function(ideaId){

                    //ideaId is to be injected to the overlay controller to get the correct idea
                    //locationParamsAndHash is the current URL state before the overlay launches
                    //it will be used when the overlay closes, and we want to return to original URL state
                    dialog.options.resolve = {
                        ideaId: function(){ return ideaId },
                        locationParamsAndHash: function(){ 
                            return {
                                path: $location.path(),
                                search: $location.search(),
                                hash: $location.hash()
                            };
                        }
                    };

                    //closing callback will receive the previous locationParams through the overlay
                    dialog.open().then(function(locationParamsAndHash){

                        $rootScope.viewingOverlay = false;
                        $location.path(locationParamsAndHash.path);
                        $location.search(locationParamsAndHash.search);
                        $location.hash(locationParamsAndHash.hash);

                    });

                };

Now, the trick is '$stateChangeStart' event. Interestingly when this event is broadcasted. The URL in the browser has changed, but the state has not yet been transitioned. So it's possible to do a preventDefault on the state change and persist the URL.

Now the greater trick is closing the overlay. The following is the overlay controller used by the dialog service.

        .controller('IdeaOverlayCtrl', [
            '$scope',
            '$rootScope',
            'dialog',
            'ideaId',
            'locationParamsAndHash',
            'IdeasServ',
            function($scope, $rootScope, dialog, ideaId, locationParamsAndHash, IdeasServ){

                $rootScope.viewingOverlay = true;

                $scope.closeOverlay = function(){
                    //this can only pass in a single param
                    dialog.close(locationParamsAndHash);
                };

                //and load in the appropriate data
                $scope.idea = {};
                IdeasServ.get(
                    {
                        id: ideaId
                    },
                    function(response){

                        //console.log(response.content);
                        $scope.idea = response.content;

                    },
                    function(response){

                        //error message
                        console.log(response.data);

                    }
                );

            }
        ]);

When I close the overlay using ui-bootstrap's dialog service. I pass in the previous URL state as "locationParamsAndHash". This basically restores my original URL's state. So when if I had something like e.com?blah=blah, then opened the overlay which changed to e.com/item/id, then closed it, it would go back to ?blah=blah.

Now this isn't the prettiest solution. I still think if something could expose the URL to be changed without any sideeffects, that would be the best. In fact I believe HTML5 history provides exactly that. The problem is that AngularJS is already using history state. And I believe (I'm not sure) that AngularJS is watching this, so even if I use history state, AngularJS will decide to change the page. But I haven't tested.

For now, have fun using $stateChangeStart.

@CMCDragonkai
Copy link
Author

@nateabele Any chance we could somehow implement what I did above elegantly into ui-router?

@nateabele
Copy link
Contributor

@CMCDragonkai You're welcome to propose a specific solution. However, be aware that this behavior:

The URL in the browser has changed, but the state has not yet been transitioned. So it's possible to do a preventDefault on the state change and persist the URL.

...is probably going to go away, since it is the subject of some open bugs.

@nateabele
Copy link
Contributor

Oh, forgot to mention, in future versions we're planning on exposing a $transition service, which will be active when $state is transitioning from one state to another, and will allow you to manipulate the transition in various different ways. Perhaps if that object offered an interface to continue the transition but suppress the URL change, that would solve your issue?

@CMCDragonkai
Copy link
Author

It'd be the opposite. Basically prevent the transition but allow the URL change. But definitely, more flexibility the better. I would like to be able to do it both ways.

@MrOrz
Copy link

MrOrz commented Oct 24, 2013

I am facing with exactly the same challenge and used a solution similar to @CMCDragonkai 's. I listened to $stateChangeStart event, capturing specific route change (which was implemented as controller function in @CMCDragonkai 's solution) and cancels route change when needed, and finally use $location.path to change the route. However, in my case the pages in the dialog is linked to other pages. I spend a bunch of code in handling the states and it is quite buggy.

I thought this maybe easier if ui-view was allowed to preserve its content after state change. In this way, I can put two named ui-views in HTML, one is the main page and the other is specifically for the dialog. I can just listen to the rout e and show/hide the dialog when needed. The state change do not need to be canceled and everything can be much simpler.

@braco
Copy link

braco commented Oct 29, 2013

Looks like the bug this was exploiting was fixed? Doesn't work now. Any ideas?

Even something like this would be acceptable: angular/angular.js#1699 (comment)

@CMCDragonkai
Copy link
Author

Just use the older version of ui-router.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants