azicchetti / jquerymobile-router

A router/controller for jquery mobile. Also adds support for client-side parameters in the hash part of the url. The routes handles regexp based routes. This plugin can be used alone or (better) with Backbone.js or Spine.js, because it's originally meant to replace their router with something integrated with jQM.
GNU General Public License v2.0
402 stars 69 forks source link

Loading the first page via bc handler #78

Open rbu opened 11 years ago

rbu commented 11 years ago

It does not seem to be possible to load the first page via a JS handler when no page with that id already exists in the DOM. If it is, it's not documented well.

Here's how I have this set up:

  1. include jQM Router
  2. include jQM with $.mobile.autoInitializePage = false
  3. instantiate the Router object
  4. call $.mobile.initializePage() by hand

I have any number of pages, that are generated completely in JS. The router looks like this:

myRouter = new $.mobile.Router([
    { "#index": { handler: "renderIndex", events: 'bC', step: 'url'} },
    { "#detail\\?id=(.+)$": { handler: "renderDetail", events: 'bC', step: 'url'} }
], controller);

Ideally, I want my HTML body to be empty, the index rendered by default and the detail page should be loaded when the site is accessed with /#detail?id=23

Unfortunately, this does not work because jQM requires at least one page to be present in the DOM when initializePage is called. If it is not, the body will be made into a page. This page could be the index (which is a sane default), however when I add the index page to the DOM, jQM will set a wrong data-url to this page (...detail... instead of index) and call changePage to this page. jQM Router will not call its JS handlers to render a page at all, because jQM already hands over a page (albeit a wrong one). The relevant code path in jQM is this (from initializePage):

            // if hashchange listening is disabled, there's no hash deeplink,
            // the hash is not valid (contains more than one # or does not start with #)
            // or there is no page with that hash, change to the first page in the DOM
            // Remember, however, that the hash can also be a path!
            if ( ! ( $.mobile.hashListeningEnabled &&
                $.mobile.path.isHashValid( location.hash ) &&
                ( $( hashPage ).is( ':jqmData(role="page")' ) ||
                    $.mobile.path.isPath( hash ) ||
                    hash === $.mobile.dialogHashKey ) ) ) {
...
                $.mobile.changePage( $.mobile.firstPage, {
                    transition: "none",
                    reverse: true,
                    changeHash: false,
                    fromHashChange: true
                });

This code path is triggered because hashPage is null -- there is no page with that id yet. It surely must be possible not to have dummy HTML elements for every single path that I support. And if it's not -- jQM Router should create them for me.

azicchetti commented 11 years ago

Hi, I think this use case should be handled with the pagebeforeload event.

You just have to disable push state ($.mobile.pushStateEnabled=false) and install a route for the beforeload event. Remember to strip the "#" from your internal links (for instance, your "#foo" page will become "foo" or "foo.html". Don't worry if the foo.html file doesn't exist, you've disabled push state for this exact reason).

Once you've injected the (full) page into the DOM, just resolve the deferred provided by jQuery Mobile itself. There's no need to play with autoInitializePage since jQM already knows what to do when the deferred is resolved.

rbu commented 11 years ago

With 'beforeload', do you mean pagebeforeload (bl)? When I create an empty jQM site with a router and $.mobile.autoInitializePage = false; $.mobile.pushStateEnabled = false;, I do not get such an event and the page stays empty.

What do you mean by "the deferred provided by jQuery Mobile itself"? My idea was to call changePage with the dom fragment that I generated, but there may be a better way?

Taking a step back, am I doing something completely unintended with the router here? My understanding was that it allows for dynamic content generation. What I want is this scenario:

http://mysite/app -- renders the index http://mysite/app#index -- renders the index (explicitly / backlink) http://mysite/app#detail?id=23 -- renders a detail editor for item number 23

Once one page is loaded, the router will trigger navigation just fine -- I can go from the index to a detail editor and back. I just want the router to handle the load case as well. If this should already be possible, maybe it would help to have an example that comes with no initial page and just a few render functions.

I have a very minimized example at http://jsfiddle.net/pGe3s/6/ Note, to escape the iframe surrounding the fiddle and actually experiment with hashtags, you can directly open http://fiddle.jshell.net/pGe3s/6/show/light/

rbu commented 11 years ago

In the example, note that http://fiddle.jshell.net/pGe3s/6/show/light/#edit does not load the edit page.

azicchetti commented 11 years ago

I think you might be overcomplicating things with your current approach (which is perfectly fine, but it's quite messy in jQM. I guess you've already realized that :D).

When you want to dynamically inject pages into my jQM applications, there are basically two ways to achieve such a thing:

1) using the pagebeforechange event. This one may be tough, it's been somehow buggy in the past and not consistent across different jQM versions. However, you retain "hash" pages / urls. With this approach, your internal links will look like #foo?bar=baz

2) using the pagebeforeload event. This is the "official" way to inject ajax things in jQM. If you're content is not really ajax but comes from a template in a js file, it's absolutely the same

I've played with both approaches and I prefer the latter, because I get a minimal cache management which may be fine in most cases (and when it needs to be tweaked, jQM provides a couple of useful events that are not available with the former), because I don't want my DOM to be bloated with a lot of pages.

Back to your example, I would simply keep a "skeleton" page in my DOM for the first page only (and add the content during the pagebeforechange event at startup), then inject the details using the pagebeforeload event and its deferred magic (http://api.jquerymobile.com/pagebeforeload/).

Urls would be in the form:

If you really want to use pagebeforechange, I suggest you put at least the first <div data-role="page"> ... </div> skeleton into the DOM to avoid this jQM madness. Then you can inject whatever you want using the router deferred mechanism.

activelogic commented 11 years ago

Hello, I am having a similar problem as rbu, but do not understand how to implement your option 2. Could you explain in more detail via an example?

To give you some background, I originally coded my app with Backbone and jQM by disabling the jQM page navigation by disabling linkBindingEnabled, ajaxEnabled, hashListeningEnabled, and pushStateEnabled. In doing so, as part of calling Backbone.View.Render() I would append 'pages' to the DOM dynamically.

I would like to implement your router in my app, but also maintain how I dynamically inject pages into the DOM.

activelogic commented 11 years ago

Regardless of the above, I'm also having an issue with the routing. I am initializing the router like so in my custom AppRouter class:

    __init__: function() {
        var self = this
        new $.mobile.Router(self.routes, self.handlers.self.options);
    }

    routes: {
        "" : { handler: "dashboard", events: "bs" }
        "#sessions" : { handler: "sessions", events: "bs" }
    }

    dashboard: function(type, match, ui, page, evt) {
        require(['views/pages/DashboardView'], function(DashboardView) {
            (new DashboardView()).render()  // dynamically attaches to the body tag
        })
    },

    sessions: function(type, match, ui, page, evt) {
        require(['views/pages/SessionsView'], function(SessionsView) {
            (new SessionsView()).render()  // dynamically attaches to the body tag   
        })
    }

The problem I'm having is that no matter what url I put in the browser, it always goes to 'dashboard'. If I change the first route from "" to #dashboard no pages will load at all.