Multiple pages in your app are just an illusion. Make one HTML document and put as many of your apps main states (pages, modals, etc) in it. Use CSS to hide and show them.
The idea here is to “statically allocate” as much of the view as possible, because it’s much easier to do that than to dynamically generate a hierarchical DOM and then keep track of what has and has not been generated. For something like a “detail view” of, say, bowties, just use one element and replace its contents every time your user looks at a new bowtie.
Besides, unless you’re building a desktop UI or something, most web apps have a limited number of states and pages anyways.
A router mostly just has to hide and show elements to give the illusion of multiple pages. One simple way to do this is just and and remove a one class at a time on a container or body element (e.g. “page-intro” for /intro, “page-about” for /about).
If you use pushState routes your users will be unable to tell that they’re not actually going to multiple pages. For links, have some way to mark up in-app links differently from external links, and override in-app links to call “history.pushState”. In the event that someone goes to a page directly (e.g. /about), use a small piece of server-side programming to return the same HTML document for each of your app’s pages.
Sometimes though you’ll want something more to happen when you arrive at a URL. Do this by firing an event for each route. For example, “/products/5/comments/2” could trigger an event like “product-comment” with a payload of { productId: 5, commentId: 2 }. More on this in the next secion.
Set up a global event bus. The event bus will isolate your view code from having to know anything about HTTP, LocalStorage, Cookies, Websockets, etc. Use a few constraints to make things easier on yourself:
- If code works with the DOM, it’s view code. Router events are also view code.
- If code works with HTTP, LocalStorage, Cookies, or Websockets, it’s “service” code.
- A unit of code (function, object, etc) should touch the DOM or the services (or neither) but not both.
- View code cannot directly call service code and vice-versa.
- View code can trigger events on the bus, but those events should not be listened to by other view code. Similarly, service code can trigger events for the view, but not for other service code.
Keep your events as specific to your application as possible, and pass simple values or JSON-like objects as payloads. (Avoid passing, say, Backbone models or collections, because then all of your application is coupled to knowledge of Backbone.)
With the above partitioning of code and isolation with an event bus, you’ll wind up with two kinds of events. User events that describe what a user did or wants to see. Service events that describe content from the network or the reduced state of a part of your app. Rule 5 helps to steer you away from complex or circular graphs of events triggering other events.
The event-bus works as a low-tech core of your application, and you can use whatever JavaScript libraries you want to implement views or services. Long term though, the catalog of events should be relatively stable in the face of UI and technology changes, and might even be worth documenting.
If you want to test with mocks you can test view code by firing service events and inspecting the resulting HTML, and similarly you can test service code by firing user events, mocking (or recording) your HTTP API and inspecting the resulting service events.
However, you can also get a lot of mileage by just unit testing the logic or transformations that happen within service or view code, and having just a handful of integration tests to show that everything is hooked up correctly.
Sometimes you will want to have fine-grained validation for each field in a form that a user is filling out. Somewhat counter-intuitively, this validation does not usually have to involve the service layer. For simple rules, consider encapsulating them in a unit of code that touches neither the DOM nor the network, and then using that module in a form view. For rules that do need to be “looked up”, do it with events from the view (e.g. VALIDATE_EMAIL, “foo@“) and results back from the services (e.g. EMAIL_VALIDATION, “foo@“, false).
excellent writeup, thanks!
So there's "view" functions (that mess with the DOM), and "service" functions (that wrangle data or I/O). Okay.