I think we have/use something similar to what you're describing -- it's in CanJS, but I think the same pattern would work fine in javascriptMVC. This is a fairly recent change in our app, so we're still trying it out, but so far it's given us some nice results.
Here's what we did:
1) We created 3 separate control classes which each handle a different scope of the page:
- "AppLoader", which ensures the user is logged in and then renders the header, footer, and an empty div#content where the main page will go.
- "AppPage", which loads the high-level data for a section of the app (e.g., a list of all profiles), and then renders a left menu and an empty div#profile into div#content.
- "AppWorkspace", which loads the specific resource being viewed (e.g., all the deep info on profile 1), and then renders it into that div#profile.
(We extend AppPage and AppWorkspace -- so our actual code has a "DashboardPage" and "ProfileWorkspace", etc.)
2) We added a param to each route which identifies its page and workspace. (We use the window.history api and pushstate, but I think the same thing would apply to hashchange-based routing.)
- can.route('account/:accountid', { pageName: 'account-detail', workspaceName: 'account-overview' });
- can.route('account/:accountid/users', { pageName: 'account-detail', workspaceName: 'users-list' });
- can.route('account/:accountid/user/:userid', { pageName: 'account-detail', workspaceName: 'user-detail' });
- can.route('domain/:domainid/:workspaceName', { pageName: 'domain-overview' });
- //etc
This let us specify the the specific AppPage (i.e., the left menu and other 'outside' stuff) which needs to go around the thing being viewed, so that even if we arrive at "users/profile/1", we know that pageName "dashboard" needs to be active.
3) Whenever we show a particular UI (i.e., when the page first loads, and every time the route changes after that), we walk down the whole chain:
- AppLoader ensures we're still logged in. If it hasn't initialized before, we render the header, footer, and div#content.
- AppLoader examples params.pageName. If the corresponding AppPage control has is not active/initialized, we remove the old AppPage and instantiate a new one; else we just leave the existing one since it's already set up.
- AppPage loads whatever data it needs. If it hasn't initialized before, we render the left menu and div#profile.
- AppPage examples params.workspaceName. If the corresponding AppWorkspace control is not active/initialized, we remove the old AppWorkspace and instantiate the new one; else we just leave the existing one since it's already set up.
- AppWorkspace loads whatever data it needs, and renders its template.
We use can.Deferred (which is just $.Deferred) to make each step in this process asynchronous, so the actual code looks more-or-less like:
- // inside AppLoader
- showPage: function(config) {
- var appPageClass = this.determineAppPageForConfig(config);
- var operation = new can.Deferred();
- var successFn = function() { operation.resolve(); };
- var errorFn = function() { operation.reject(); };
- if (!appPageClass) {
- // 404: show an error
- this.currentAppPage.destroy();
- this.currentAppPage = null;
- } else if ( !(this.currentAppPage instanceof appPageClass) ) {
- // we have a new page to display
- this.currentAppPage.destroy();
- this.currentAppPage = new appPageClass('#content', { ... });
- }
- if (this.currentAppPage) {
- // regardless of whether this page is new or not, it probably needs to
- // go load some data based on the ids in our route
- this.currentAppPage.loadDataForConfig(config)
- .done(_.bind(function() {
- // Only after all AppPage data has been loaded successfully
- // do we start to think about AppWorkspace
- this.currentAppPage.showWorkspace(config)
- .done(successFn)
- .fail(errorFn);
- }, this))
- .fail(errorFn)
- ;
- }
- // And by returning a deferred/promise which only gets resolved/rejected
- // once the entire operation is complete, we can do things like:
- // this.loadPage({ ... }).done(function() { ... });
- return operation.promise();
- }
and then AppPage.showWorkspace() does more-or-less the exact same thing as AppLoader.showPage() -- it just deals with 'workspace' controls instead of 'page' controls.