Skip to content
This repository has been archived by the owner on Oct 8, 2021. It is now read-only.

Navigation Refactor - First Pass #5091

Closed
johnbender opened this issue Sep 26, 2012 · 24 comments
Closed

Navigation Refactor - First Pass #5091

johnbender opened this issue Sep 26, 2012 · 24 comments
Assignees
Milestone

Comments

@johnbender
Copy link
Contributor

Goals

Ordered by priority.

Abstract pushstate/hashchange handling

I'd like to unify these two events into one internally ("navigate"). Additionally I was considering providing namespaced events "forward.navigate", and "back.navigate" where appropriate but that will require review since it's tightly coupled to our hashchange handling. Again, these events will initially be internal until we decide that the module is ready for general consumption.

This likely includes the following deliverables:

  1. "navigate" triggered on window
  2. HistoryManager
  3. HistoryEntry
  4. "back.navigate", "forward.navigate" triggered on window
  5. Dependency only on jQuery

Container

I'd like to provide a container widget that manages its history and content transitions. The idea is to have this respond to navigation events and possibly delegate to changePage. This is important as we make a run at multiple container documents.

This likely includes the following deliverables:

  1. ContentContainer
  2. changePage alterations to account for a container
  3. Dependency on jQuery and some change content function (eg, changePage )
@ghost ghost assigned johnbender Sep 26, 2012
@frequent
Copy link
Contributor

@Wilto: if you want, I can share some stuff on what I did in multiview. The current version is tapping into JQM history (no container history mgt). In the previous version every container had it's own history. I eventually got both to work, although I'm finding container history inside JQM easier to manage. Let me know if you want some code samples with comments.

@johnbender
Copy link
Contributor Author

@frequent

I'll definitely ping you for review, after I get the basic navigation event structure in place. I'm really looking forward to getting this cleaned up and I your help could be invaluable.

@frequent
Copy link
Contributor

frequent commented Oct 1, 2012

@johnbender:
I could do a quick write-up of the current navigation model I'm using (storing container transitions in JQM history). Although I'm still not happy with and it has it's bugs (spell multipe pages in the DOM each with containers), the logic is pretty simple, which... I like.

@johnbender
Copy link
Contributor Author

@frequent

Let me sort out the navigate event first and think about page containers a bit, and then we'll talk. Fresh eyes :)

@frequent
Copy link
Contributor

frequent commented Oct 2, 2012

@johnbender - you have a point :-)

@toddparker
Copy link
Contributor

This will be really great, especially the ability to surface a consistent way to track an action in the history stack because we already have dialogs and popups that need this and I can see the list of potential widgets that may need history support - hide/show panels, menus, etc.

@frequent
Copy link
Contributor

frequent commented Oct 3, 2012

aka "The history-dump"

@gabrielschulhof
Copy link

Make sure you take into account what happens should the user refresh the page at any point in history. Currently, we get all kinds of funky effects when the user refreshes mid-history. See #5122 for example. Also, it seems a refresh on a given location is not the same as starting with a deep link, because, if you refresh and then hit "Back", jqm is not reloaded, but the same pagestate is kept alive.

What I mean is this:

  1. Open a URL containing jQM
  2. Navigate using AJAX to another page
  3. Refresh. At this point, jQM is re-instantiated with the second page as the starting URL.
  4. Click "Back". At this point, the second instance of jQM becomes associated with the first URL.

@frequent
Copy link
Contributor

frequent commented Oct 4, 2012

@gabrielschulhof - do you know if double entries are allowed/possible/wanted in the URL history? After a transition, I'm running a cleanser, that clears out double entries in the URL history when they are last in stack, so:

page 1
page 2
page 3
page 3 refresh

would clear out the last page 3 entry, which "maintains" the user on the current page and entry.

@gabrielschulhof
Copy link

O_o ... how do you reach such a state? I'm talking about browser history, not $.mobile.urlHistory.

@frequent
Copy link
Contributor

frequent commented Oct 4, 2012

@gabrielschulhof: Can't do browser history, only $.mobile.urlHistory as well....

Hhaven't looked at this in a while...

var activePagefromUrl = $.mobile.urlHistory.stack[$.mobile.urlHistory.activeIndex].pageUrl;

for (var i = 0; i < $.mobile.urlHistory.stack.length; i++) {                        

    // last backwards transition on a panel (always overwritedata-url with full-url, no #page-thingies)
    if ( o._backFix == true ){
        if ( $.mobile.urlHistory.stack[i].url == data.toPage.attr('data-url') ) {
            $.mobile.urlHistory.stack.splice(i,1);
            $.mobile.urlHistory.activeIndex = $.mobile.urlHistory.activeIndex-1;

            // clear fwd
            $.mobile.urlHistory.clearForward();
            }
    // regular panel transitions
    } else if ( todo == "blockdoubles" ){                               

        // check if the current page is already in the history
        if ( i != 0 && i < $.mobile.urlHistory.stack.length-1 && $.mobile.urlHistory.stack[i].url == activePagefromUrl ) {
            $.mobile.urlHistory.stack.splice(i,1);
            $.mobile.urlHistory.activeIndex = $.mobile.urlHistory.activeIndex -1;

            // clear fwd
            $.mobile.urlHistory.clearForward();
            }
    }                       
} // end for -loop

@frequent
Copy link
Contributor

@johnbender:
have a look here. Don't click documentation, the rest should stay in the containers/page boundaries. I could only test on iPad and PC but this sure is in sync with browser history.

In the source, the js file includes jquery, jqm and url.js (almost at the bottom - search for url.js v1.0.0 ). I haven't really looked into the code, but it performs nicely (probably a little to customized on the specific page content, but I guess this can be generalized). I can hook you up with the author if you like - sitting in the same room this week.

@johnbender:
repo can be found here

@johnbender
Copy link
Contributor Author

Update:

Here's what I have so far, all event/method names are up for discussion.

First, I've wrapped both hashchange and popstate and created a new event navigate:

https://github.com/jquery/jquery-mobile/blob/simple-nav/js/navigation/events/navigate.js

This by itself isn't super exciting though it does normalize the "shape" of the data and API so that folks who want to build on it down the stack have a reduced set of concerns. Importantly, this by itself doesn't allow us to track history. The key to tracking history within jQuery Mobile is that new history states are created and controlled by the library.

Second, I've created the $.navigate method, which sets the hash, does the replaceState with the hash (necessary for ios 4 support), and tracks history.

https://github.com/jquery/jquery-mobile/blob/simple-nav/js/navigation/navigate.js

Along side the navigate method are two event bindings (hashchange and popstate) that live "before" the event bindings in the normalized navigate event. When those events are triggered from something other than a call to $.navigate they do our basic history calculation to attached the history entry to the event and attach directional information to the event.

All of this taken together means that if users are willing to use $.navigate along with the navigate event they can manage page state with either hashchange or popstate without having to care which one is being used.

Let's look at an example similar to something that jQuery Mobile does though we'll assume that the content associated with each page state can be loaded directly without concern for scripts, etc:

$( "a" ).click(function( event ){
  event.preventDefault();

  // Here we set the hash, call replaceState where supported
  // and also track a new history entry with our extra data from the 
  // anchor "data-foo"
  $.navigate( this.attr( "href" ), { foo: this.attr("data-foo") });

  // ostensibly this method call takes the href and loads the content from the server
  // altering the document to reflect the proper page state
  MyApp.loadContent( this.attr("href") );
});

Here we've done a link hijack that participates in our history tracking by using the $.navigate method to track state and do the url alteration in the application. Let's look at series of events and the result of something like this:

First some markup:

<a href="/dogs" data-foo="bar">Dogs</a>
<a href="/cats" data-foo="baz">Cats</a>

When the first link is clicked, navigate is called and the url is updated:

hashchange support:  / => /#/dogs
popstate support:    / => /#/dogs => /dogs

Assuming those links are still available and the second link is then clicked:

hashchange support:  /#/dogs => /#/cats
popstate support:    /dogs => /dogs#/cats => /cats

Now assuming an event binding like:

$( window ).on( "navigate", function( event, data ){

  // NOTE I need to address namespacing in the data so that it doesn't collide with the
  //      attached history information
  console.log( data.foo );
  console.log( data.url );
  console.log( data.direction );

  MyApp.loadContent( data.url );
});

Hitting the back button, or calling window.history.back() will result in the following output:

bar
/dogs
back

And with the call to loadContent provided the url the visual state of the application should change as well. Note the directional information which is useful for things like transitions.

If the user were to then hit the forward button the output would then be:

baz
/cats
forward

And the call to `loadContent with the url could handle the visual state of the application.

Hopefully this illustrates what I've come up with so far. There are a lot of details to sort, but I wanted to get some feedback before I proceed to rework the navigation internals based on this model. I think this will remove alot of the complexity from the core navigation code.

@scottjehl
Copy link

Nice work, John!

I really like this approach so far and am happy to chip-in as things progress. It's definitely easier to follow than our first pass.

I'm not entirely sure whether triggering the navigate events on the window object makes more sense than triggering them on the page container...

Anyway, looking great at first glimpse.

@johnbender
Copy link
Contributor Author

@scottjehl

You raise an important issue, and it's something I've considered while working on this.

I'm primarily addressing/normalizing/improving the browser's concept of navigation because at some level in the library and for other developers at large it all has to boil down to that anyhow. That is, when we get to the point that we have many page containers, they will have to derive their "navigation state" from a single url change event even if it's associated with complex state in the background.

I think that dealing with what a page container does when it get's a "navigate" event is a bit further down the stack and later in the refactor, but I have a branch that started out allowing arbitrary objects as the trigger point for the nav event and I ran into a complexity wall when considering the issue with page containers.

Hopefully we'll be able to derive the correct information with these tools at the page container level, and provide a second derivative event that applies only to a given container. Or at least that's my current thinking.

@scottjehl
Copy link

okay. Well, keep going and let's see how it shakes out, then! :)

@frequent
Copy link
Contributor

@johnbender: Nice work!

@scottjehl, @toddparker

Some late night thinking on using $(window) as global object with pageContainers:

(1)
I'm using two layers of JQM pages (wrappers and nested), so a page with containers would look like this.

<!--- wrapper page --->
<div data-role="page">
   <!--- containers--->
   <div data-role="panel"></div>
   <div data-role="panel">
         <!--- nested page--->
         <div data-role="page"></div>
   </div>
</div>

While I figured out a way to use JQMs current navigation to work with this setup, I like the idea of using $( window ) as top-level object "managing" different types of pages (single page, multipage, pageContainers) vs having wrappers and nested pages. Much less confusion.

(2)
Also, I ran into a lot of trouble when going from a page with pageContainers to another page with pageContainers.

Say you have:

 wrapper-page:  base.html
    pageContainer-A
    pageContainer-B
         nested-page:  two.html

 wrapper-page:   next.html
    pageContainer-C
    pageContainer-D
         nested-page:  three.html

If you navigate:

1)  base.html => two.html    URL = base.html/#two.html
2)  base.html => next.html   URL = next.html   vs.  base.html/#next.html 
3)  next.html => three.html   URL = .... base.html/#next.html/#three.html 

This can be fixed by exposing documentUrl and documentBase and resetting both when pulling in a new wrapper page with different data-url than the current documentUrl.

Again, I think running the navigation from the $(window) object would spare a lot of complexity here as you would always works from a global object.

(3)
Your approach feels like delegating from $(window) to a target container, which also sounds right.

Say you'd use the <body> and pageContainers as "viewports", you should get a trackable path through the application as pages will either be loaded into the <body> or a pageContainer. There are a few caveats in reverting this in the history, but I tried with up to 4 panels and the same set of rules always apply.

(4)
The only drawback I see is you would loose the option of having a global header/footer if widgets can't be initialized outside of a page.

Say you are using a wrapper page vs. running things from $(window):

 body                                                  body
   wrapper-page
       header                                              (header)                          
       pageContainer                vs.               pageContainer
       pageContainer                               pageContainer
           nested-page                                       nested-page
       footer                                                (footer)

I guess once popups will be available globally, global toolbars won't be far behind, so I don't think this is really a problem down the road.

Therefore: $(window) would get my vote!

@johnbender
Copy link
Contributor Author

@frequent

Thanks for the notes and the vote of confidence, this is super useful stuff to know up front 👍

@frequent
Copy link
Contributor

@johnbender: thx.

One important point I want to add:

(5)
Best reason for using $(window) is if you can manage to handle the last backwards transition on a container, which currently is a nightmare of excepctions for me.

Using above example:

wrapper-page:  base.html
    pageContainer-A
    pageContainer-B
        nested-page:  two.html

Loading this page will create an entry for base.html in the urlHistory. If you do this:

 two.html => three.html       

you will get a 2nd entry in the urlHistory, which will be three.html.

Now go back. You don't have two.html in the urlHistory.

What happens is inside $.mobile._handleHashChange you will end up with no to defined, so JQM will try to reload the first page base.html vs. reverting the last panel transition, which blanks the pageContainer in question.

I spend forever on this and I spare you the details, but you need a fwd/backwards transition counter, have a terrible check for the last panel backwards transition, hijack the trailing hashchange (it trails nothing, cause there is no to ), convert the trailing hashchange object to a string, have it pass so you get a transition, block everything it triggers and clean up the mess it makes in the urlHistory afterwards.

This handler is about half of my panel navigation, so if using $(window) can catch the last backwards transition on a container and if the navigation has a handle for "no to defined", you should save yourself a ton of headaches.

@johnbender
Copy link
Contributor Author

@frequent

We'll have to revisit that issue once I get this merged so I can fully understand it. It sounds like what we need in that case is a history stack per page container, which is something I had planned to push down to the container widget (1.4)

I could be way off there though.

@frequent
Copy link
Contributor

@johnbender.

ok. I did my first multiview version like this = with every container keeping it's own urlHistory. I later switched to using JQMs urlHistory, which only required to add the container to the urlHistory like so (from almost latest):

// addNew is used whenever a new page is added
// XXX: add pageContainer param
addNew: function( url, transition, title, pageUrl, role, pageContainer ) {              
    //if there's forward history, wipe it
    if ( urlHistory.getNext() ) {
        urlHistory.clearForward();
    }

// XXX: storing pageContainer in url-history
urlHistory.stack.push( {url : url, transition: transition, title: title, pageUrl: pageUrl, role: role, pageContainer: pageContainer } );
urlHistory.activeIndex = urlHistory.stack.length - 1;
},

Both variants have their pros and cons :-)

@johnbender
Copy link
Contributor Author

@toddparker

Per your request an executive summary of the nav work:

There are two new additions to the navigation functionality in jQuery Mobile. A navigate event that normalizes the URL alteration events hashchange/popstate, and a $.mobile.navigate method that allows users to receive extended traversal information in navigate bindings (eg, directionality).

The navigate event is a light weight attempt to unify bindings to a browser's URL alteration events hashchange and popstate. It also handles differences in the way setting the hash on the location object interacts with the two browser events (ie, stop the world popstate triggering) , and provides event hooks to control the event lifecycle.

The $.mobile.navigate method forms the bulk of the new functionality. By using the $.mobile.navigate method to do url manipulation instead of doing it directly with the location object, replaceState, or pushState you get history management and support for both modes of URL state tracking. The history management provides a state object to navigate event bindings whether the browser supports the new history API or not. In addition it contains the same logic used in jQuery Mobile to determine what direction the browser history is moving.

Both the event and the method are available as modules apart from other navigation functionality in jQuery Mobile, with minor dependencies on other parts of the library.

@gabrielschulhof
Copy link

Nav sequence regressions wrt. master (I'll maintain a list in this comment):

Dialog hash key:
[ ]External -> Start -> Click "A dialog" <- Click "Back" on the browser
Bad: You're back to External, not to the start page.
Good: If you click the dialog's close button, it works.

[ ]External -> Start -> Click "Another page" <- Click "Back" on the browser
Bad: You're back to External, not the start page.

[ ]External -> Start -> Click "Popup 1" <- Click "Back" on the browser
Bad: You're back to External, not to the start page.

Page 1/Page 2&dialog hash key:
[ ]External -> Start -> Click "A dialog" <- Click dialog's close button
Bad: Dialog won't close
Bad: If you click "Back", you're back to External

@arschmitz
Copy link
Contributor

This is merged into master so closing as complete for the first pass per @johnbender

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

No branches or pull requests

6 participants