October 10, 2020

How to Build a Single-Page Web App

What is a single page web application? Simply put it's a website at a single, never-changing (while you're using it) URL.

How to Build a Single-Page Web App

But first, what is a single page web application? Simply put it's a website at a single, never-changing (while you're using it) URL. While this type of structure can be used for websites, it's better suited for a site that behaves more like an application than a website. A web-based email client is a good example of this, as well as a live web-feed, a game, etc. As I outlined in my article Consider a Web Application for Your Next Project, there's never been more of a reason to build a web application. While you could build a web application over a number of pages, there are many compelling reasons to build it as a single page app instead.

Why Single Page

  • Efficiency - fewer resources to load over and over (even from cache) means faster loading, and less CPU usage. Lower CPU usage means you're not crushing the battery of your users.
  • Performance - Load once and you're ready. Less screen flickering, faster page transitions, lower lag on actions, and an overall better experience for the user.
  • Reliability - if the first screen works, then the rest most likely will too. The browser compiles the resources it uses up front, so you'll know right away during development if you've missed something.

Why Not Single Page

  • Complexity - single page apps are typically more complex than their multi page cousins, as you always have everything loaded at once. You need to keep track of variable namespace, and maximize code reuse. While a single page app will force you to write better code, it will take longer to create, and will be more difficult to maintain.
  • Complexity 2 - wrapping one's head around the idea of a single webpage that has multiple independent views operating simultaneously, is not always a simple undertaking. If you're someone who's primarily built server-based websites, the transition to single page will be much more difficult.

We're Doing it Anyway

Whether you're intimidated by what you've just read or not, the article is about how to make single page apps, so we're going to learn about them whether you like it or not! Single page apps are better for our good friends the users, so we need to treat them right and give them the experience they deserve.

Task number one is to get yourself some structure. No not in life (although maybe you need that too?), I mean for you to build your app upon. A single page app runs counter to the way the web was originally conceived, so you need to use some tools to bend the browser to your will. Fortunately web technology development has been supportive of this trend, so you don't have to work too hard at ensuring you're not sending your users off someplace else with every click.

View-Controller

There are plenty of web frameworks out there with various pros and cons, and each with a slightly different take on how to accomplish things. We're not going to dive into that never-ending pit, so hit that up on your own time! We're instead going to look at things from a high level, as from that perspective, the concepts are largely the same.

 

You may employ any number of views of all manner, shapes, and sizes, and even views within views; the key takeaway is that each view should operate largely independent of the others. This separation allows for fine-tuned control over how the view functions, while also making maintenance easier; you don't have to worry as much about a change to one area affecting others.

Behind the view system for user interface (UI), are a set of tools powering loading of the views, and any other common features. For example:

  • Persistent data storage
  • Authentication
  • API access
  • Configuration
  • User notifications

The term view-controller is quite literal in that there is an asset for the view, and an asset for the controller. The view contains all the visual elements, structure, and styling that the view would need, where the controller is the smarts that brings that view to life with dynamic content (where needed), and functionality. In a web app the view would be comprised of several assets including: html, css, images, fonts, etc.; the controller is some javascript code.

Bringing Order to Chaos

How do you take all these moving pieces and manage them in a simple and sustainable way? The answer is a framework. A framework is a tool that allows for consistent control and usage of each view-controller pair, so that you the developer does not have to worry about anything other than what you're working on. Think of a framework based on its name; think of frames around each of your features so that they don't spill into each other. Sure you can look over the side of a frame when you need to, but only when you need to. What happens in a frame, stays in the frame. Choosing a framework can be tough as there are many options, so focus your search on what problems you're trying to solve.

For this article we're going to use my view controller framework with the boring name 'VC' for 'view-controller'; you can find it here on Github. We're going to use this as it's the simplest framework I've seen, so it's great for learning and illustrating the view-controller concept.

In order to start using VC to manage your views, you first must initialize it for the view in question. This is done via the 'getView' function.

VC.getView(document.getElementById("mainbody"),"mainbody.htm");

Note that in this case the element has an ID that matches the name of the view; this is not required, but it is good practice as it makes things easier to follow. The URL for the view may be absolute or relative; in this case a relative URL is used. The path is relative to the root window document, with additional optional sub-folder paths prepopulated in VC. The sub-folder path defaults to 'views/' but it may be changed manually by editing the private VC property 'viewsURL', or dynamically using VC's 'setViewsURL' function.

VC.setViewsURL("myviews/");

Once a view is initialized, no controller and no custom syntax are required. VC works by overriding the behaviour of HTML anchors and form elements; instead of affecting the whole page, the response from these element actions will be populated into the view element only. You can even use dynamic content like .Net or PHP to populate your views.

<form href="account/create.php">
    Name <input type="text" name="name"/>
</form>

The key, and really to all view-controller frameworks, is that activities and markup in one view do not affect others. This can even apply to CSS if you prefix view-specific styling with the ID of the view.

#mainbody p {
    text-align: justify;
}

There are cases of course where you will want some cross-functionality between views; the most common form of this is a menu system. If you have a menu view and you would like to cause menu presses to render changes in the 'mainbody' view we've been using, you can give a command to VC through the active element's dataset property.

<a href="account/signup.htm" data-vcelm="mainbody">Sign Up</a>

If VC finds an element with the ID 'mainbody', it will direct the result of the anchor click to the 'mainbody' view and element, instead of the current view.

If you wish to use VC for most of the view anchors and forms but not all, you may instruct VC to skip it by setting the 'target' property on the element.

<!--Opens the link in a new tab or window-->
<a href="https://github.com/offthebricks/VC" target="_blank">VC by OffTheBricks</a>

<!--Does a full page reload with a POST to search.php-->
<form href="search.php" method="POST" target="_self">
  Search<input type="text" name="search"/>
</form>

Note that any view that you load that isn't the first 'single page', should not have complete HTML in it. Each of the above example HTML files loaded by VC should contain only the HTML markup needed for the view. All of your supporting CSS and Javascript assets must be referenced in the initial 'single page' that composes your app; typically this is nearly the only role this file fulfills, as the majority or all of the actual visual aspects are in their respective views.

Give Me Some Control !!!

Ok ok enough about views, time for the controllers. Controllers are often a required companion of a view as it provides additional functionality. Controllers will perform API calls, save local data and preferences, populate dynamic data, do data validation, etc. Controllers are loaded based on the URL of the view being loaded, so a controller for our 'mainbody' element would start out like this.

function mainbody(viewObj){

    this.onload = function(){

        //perform view initialization here

        VC.onviewload(viewObj);
    };

    this.onclose = function(){

        //clean up any loose ends here (listeners, timers, etc)

    };
}

The 'viewObj' passed in on object construction is a useful resource for your view; it contains a reference to the view element, and all URL parameters passed to the view, among other things. It helps keep context and scope in place for your code, and makes your view more portable should it move elements, or become a view within a view or something.

The 'onviewload' call at the end of the 'onload' function is required as it tells VC that the view is done loading, so that it can trigger any applicable listeners. You may set listeners in your controllers so that it may be notified by VC if that view changes. This can be helpful for having menus automatically adjust to changing content, and other dynamic activities. Setting a view listener is easy; just tell VC the element of the view to listen to, the name of your view listener (needed for removing the listener), and the callback function to call when there's a change.

VC.setViewChangeListener(
    document.getElementById('mainbody'),
    "eavesdroppingView",
    myOnChangeCallbackFunction
);

The 'onclose' function in your controller is very important as it helps prevent memory and performance issues caused by orphaned controllers and timers. This function is called whenever a view already exists in an element that is the target of new content; anchors, forms, and calls to 'getView' will all trigger this function call. Every time you're reloading a view, make sure to terminate any timers and listeners that are specific to this instance of the view; all references to those timers and listeners will be lost when the view reloads, and a new controller object is initialized.

I mentioned briefly the concept of a view within a view; a common application of this might be a clock or notification bar within a header. VC supports this naturally due to views being loaded based on the element, but for sub views you must be careful to maintain them in case of changes to the parent view. Any reload of the parent will remove the view element, but not the controller in VC's memory; make sure in the parent view's controller to delete the sub-view when 'onclose' is called.

this.onclose = function(){
    VC.deleteView(document.getElementById("subview"));
};

Just pass the element to the 'deleteView' function, and the controller and content is removed; you may remove just the controller and not the HTML by passing 'true' as a second parameter.

Go Forth and Make Thy Apps Single-Pagers

As mentioned previously, VC is just one of many frameworks available to make your app into a single-page app; the key concept to wrap your head around is that there is just a single URL that the browser address bar ever needs to see. All other page loads and API calls are managed through Javascript, and if done correctly, may operate independently without worry of affecting other views.

In case you missed it here's the link to VC on Github again. There are a lot more features in the VC library than are covered here, most of which are covered in the project's readme.