Given that

  1. AngularJS enforces a Single Page Application structure;
  2. a web application can easily have two or more layouts;

I understood that I needed to setup a Multi Page Application in AngularJS to be able to have multiple layouts with standard routing, i.e. without installing, understanding, and learning special solutions like ui-router. How did I do it? In a few words,

  1. independent SPAs share the same routes
  2. routes reference the SPA they belong to
  3. when a user crosses one’s SPA boundaries, the other SPA is navigated to

Application Structure

(click on bold text to expand / collapse)

{[ .app-structure | =client/apps/= | =client/shared/= | array(2) | 1.directory(2) ]}

This MPA allows to host its SPAs into their own folder in ´client/apps/´ and functionalities shared among them (like the authentication service) into ´client/shared/´.

Each of those SPAs allows to host its components into their own folders in ´client/apps/<app>/components/´ and functionalities shared among them (like the user service) into ´client/apps/<app>/shared/´.

Thus the organization of specific and shared stuff between the MPA and its SPAs is analogous to that between each SPA and its components.

client/shared/routes.js

Routes belong to the MPA, i.e. they are shared among all the SPAs. This allows me to look at one file and know exactly which URL translates to which component of which app.

{[ =https://raw.githubusercontent.com/aercolino/mean-app/0dac346e70b44d964e734e639cb76a29f491d4ab/client/shared/routes.js= | 1.get-url(1) | =javascript= | 1.hl(2) ]}

Based on the article Dynamically Loading Controllers and Views… by Dan Wahlin, I wrote my own route provider, which sets up routes for components, using the Folders-by-Feature Structure shown in the Application Structure section above. A few things to notice:

  • The ´routeProvider.route.forComponent(options)´ method creates a standard ´route´ argument for the ´$routeProvider.when(path, route)´ method.
  • The ´options´ argument can be a string or an object literal.
  • If ´options´ is a string, then its format must be: ´'<app>: <filepath>[ as <alias>]’´.
  • If ´options´ is an object, then its properties are the same as the ´route´ argument (documentation),
    • PLUS:
      • ´app´: required
      • ´filepath´: required
      • ´controllerUrl´: optional
  • Set ´options.controller´ to specify a special controller name. By default, a controller name is built by taking the last part of ´filepath´, making it camel case, and adding ´Controller´. It must be the registration name.
  • Set ´options.controller´ to ´”´ to indicate that the view has no controller.
  • Set ´options.templateUrl´ and / or ´options.controllerUrl´ to specify a special view and / or controller file. By default, view and controller files are built by taking the last part of ´filepath´ and adding ´.html´ and ´.js´. They must be the path part of a valid URL, i.e. based off ´client/´, like ´/shared/modules/my-empty-controller.js´.
  • A ´filepath´ is always based off ´client/ apps/ <app>/´ (reduced to ´…/´ below here).
    • Use a ´filepath´ like ´/ path/ to/ file´ (with a leading slash)
      for addressing files like ´…/ path/ to/ file.*´.
    • Use a ´filepath´ like ´path/ to/ file´ (without a leading slash, with middle slashes)
      for addressing files like ´…/ components/ path/ to/ file.*´.
    • Use a ´filepath´ like ´file´ (without a leading slash, without middle slashes)
      for addressing files like ´…/ components/ file/ file.*´.

The ´forComponent(options)´ method returns a route object with a ´resolve´ property which is a map of dependencies to be injected into the controller. Given that the ´load´ dependency returns a promise while loading the controller, the router will wait for it to be resolved. That is all explained in the documentation. The nice paradox is that, when the router gets the promise of the dependency to inject into the controller, there is no controller yet, i.e. the dependency is the controller itself. It’s a bit weird, but works like a charm, and all under the hood.

Compare the routes with the application structure above to understand how they collaborate. For example, when the router matches the ´’/login’´ path, it applies the route returned by ´forComponent(‘auth: login as vm’)´. In particular,

  • if the current app is ´auth´ then the ´client/ apps/ auth/ components/ login/ login.js´ controller is required before being bound to the ´client/ apps/ auth/ components/ login/ login.html´ view that Angular automatically downloads. Instead,
  • if the current app is not ´auth´ then the browser is redirected to the ´/login´ path of the ´auth´ app, which will cause the ´client/ apps/ auth/ index.html´ layout to be loaded. That will set the current app to ´auth´, and allow the routing flow to proceed like above.

Remember that the routes file is loaded (and ´forComponent´ is run) at bootstrap time, but each controller is lazily loaded only when the router matches its path, if ever.

client/shared/modules/my-route.js

{[ =https://raw.githubusercontent.com/aercolino/mean-app/3516e4b95270b6ec77d9baabe7ab18a3ff67751d/client/shared/modules/my-route.js= | 1.get-url(1) | =javascript= | 1.hl(2) ]}