There are many frameworks such as Angular and React that help you develop client-side applications in a very structured manner. They help you create Single Page Applications (SPA), perform data binding, and provide many other services. The purpose of this blog post is not to replace these frameworks, but to show you how you might accomplish the same services using JavaScript and jQuery. There are many developers that have a huge investment in JavaScript and jQuery and can't, or maybe don't want to, convert to these new frameworks. They may also want to create a SPA and have a nice structured approach to their jQuery applications. In this blog post, I present a method to create a SPA using HTML, Bootstrap and jQuery and how to structure your application files.
In this post, you will learn the basics of downloading partial HTML pages and insert them into another HTML page at a specified location. This is similar to the SPA functionality that Angular and React supply. You are going to create small files for style sheets and JavaScript for each page. You are also going to learn to handle the back button, so your user can move back through your partial pages just as if they were normal pages. All of this is a good start on building a SPA using just jQuery, and a model for structuring your different application artifacts.
In the next blog post, I will show you how to build a CRUD page using HTML, Bootstrap, jQuery, and a Web API using the techniques presented in this blog post.
Build a Starting Page
Create a new web project using the tool of your choice. Add a new folder named \src. Add an HTML page named index.html into the \src folder. Add Bootstrap & jQuery to your project. If you are using Visual Studio 2017, your solution might look like Figure 1.
Figure 1: A sample project structure
In your index.html page, modify the <head>
section to look like the following:
<head><br> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SPA Sample</title>
<link href="../content/bootstrap.min.css" rel="stylesheet" />
</head>
Just after the <body>
tag, add Bootstrap navigation and menu items with their href attributes filled in with hashtag values as shown below:
<nav class="navbar navbar-fixed navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<a href="#home" class="navbar-brand">Home</a>
</div>
<ul class="nav navbar-nav">
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</div>
</nav>
Just below the navigation area, add a new <div>
and use the Bootstrap class of "container
". It is within this area that we want our partial pages to be displayed. Add a new element named called "contentarea
". This is an element name you are just making up.
<div class="container">
<contentarea></contentarea>
</div>
Below this code, add <script>
tags for jQuery and Bootstrap.
<script src="../scripts/jquery-3.2.1.min.js"></script>
<script src="../scripts/bootstrap.min.js"></script>
Add another <script>
tag below these into which you add code to retrieve a piece of HTML from a file.
<script type="text/javascript">
'use strict';
$(document).ready(function () {
window.location.hash = "#home";
window.onhashchange = function () {
if (window.location.hash) {
var pageName = window.location.hash.substr(1);
$.ajax({
url: pageName + ".html",
dataType: "html",
success: function (html) {
$("contentarea").html(html);
},
error: function (error) {
console.log(error);
}
});
}
}
});
</script>
Home Page
Create a new page called home.html within the \src folder. This is going to be one of the partial pages that will be loaded and placed in between the <contentarea>
and the </contentarea>
tags.
<div class="row">
<div class="col-xs-12">
<h1>Home Page</h1>
</div>
</div>
About Page
Create a new page called about.html within the \src folder that will also be loaded into the content area.
<div class="row">
<div class="col-xs-12">
<h1>About Page</h1>
</div>
</div>
Contact Page
Create a new page called contact.html within the \src folder that will also be loaded into the content area.
<div class="row">
<div class="col-xs-12">
<h1>Contact Us Page</h1>
</div>
</div>
Run the page to and click on the About and Contact menu items to see them displayed. If they are not displayed, check the console window.
Fix Back Button
If you run the sample and click on the About menu, then the Contact menu, you will see both those pages appear just fine. Now, click on the back button and you go back to the About page. Click the back button once more and you are back at the Home page. Check the address bar in your browser and you will see that there is a "#home
" after the name of the .html page. Click the back button one more time and you will see this "#home
" go away.
This means that there was one additional item in the history. The reason for this is when the page is loaded, that is the first item put into history. In the JavaScript you wrote, you set the window.location.hash
equal to "#home
" which then triggered the onhashchange
event. This event loads the home.html page and adds this page to the history. Let's fix this problem, and break up our code so we don't have so many things happening in the onhashchange
event.
The loadPage Function
The process of loading a partial HTML page and displaying it within a specific area on a page can be made very generic. Let's break this part of the previous code into its own separate function named loadPage()
.
function loadPage(contentArea, pageName) {
$.ajax({
url: pageName + ".html",
dataType: "html",
success: function (html) {
$(contentArea).html(html);
},
error: function (error) {
console.log(error);
}
});
}
The changePage Function
The process of taking the hash name, stripping out the leading hash symbol, and calling the loadPage()
function can be made into another function called changePage()
.
function changePage(contentArea, hashValue) {
var pageName = hashValue.substr(1);
loadPage(contentArea, pageName);
}
Fix Back Button
In order to fix the back button problem, you don't want to set the hash property of the location object. Instead, you can use the replace()
method to rewrite the URL and supply the "#home
" hash. The replace()
method will replace the current history, which is the first page load, with the page with the hash appended to it.
window.location.replace(window.location.href + "#home");
From the onhashchange
event, you now only need to call the changePage()
function. Below is the complete document.ready
function you replace with the code that is currently in your page.
$(document).ready(function () {
window.location.replace(window.location.href + "#home");
window.onhashchange = function () {
changePage("contentarea", window.location.hash);
}
});
Run the page again, and you should now see that when you land on the page, the back button is no longer enabled.
Add Custom CSS and JS to Partial Page
Having custom style sheets and JavaScript files particular to one page is always a best practice. Even though you are using partial pages, there is no reason you can't add styles and JavaScript to those pages. Add a new style sheet named about.css within the \src folder and add the following code into this new file.
h1 { color: blue; }
Now add a JavaScript file named about.js within the \src folder and add the following code into this file.
'use strict';
function testing() {
alert("Hello from About Page");
}
Open the about.html and modify the HTML to match what is shown below:
<link href="about.css" rel="stylesheet" />
<div class="row">
<div class="col-xs-12">
<h1>About Page</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button onclick="testing()">Click Me</button>
</div>
</div>
<script src="about.js"></script>
Run your page, click on the About menu and you should now see the title is in a different color. Click on the button and you should see an alert pop-up on your browser.
Wrap loadPage() into a Closure
Since the loadPage()
function is very generic, let's move that off into its own .js file You are going to also wrap this function into a closure. Create a file called spa-common.js in your \scripts folder. Add the following code:
'use strict';
var spaController = (function () {
function loadPage(contentArea, pageName) {
$.ajax({
url: pageName + ".html",
dataType: "html",
success: function (html) {
$(contentArea).html(html);
},
error: function (error) {
console.log(error);
}
});
}
return {
loadPage: function (contentArea, pageName) {
loadPage(contentArea, pageName);
}
};
})();
The above code creates a closure and assigns that to a variable named spaController
. You may now access the loadPage()
function through this variable. Open your index.html page and add a link to the spa-common.js file.
<script src="../scripts/spa-common.js"></script>
Remove the loadPage()
function from your index.html page and modify the changePage()
function by adding 'spaController.
' in front of the loadPage()
function.
spaController.loadPage(contentArea, pageName);
Run the page and ensure everything is still working like it was before.
Add Title for Each Page
As you click on each menu, it would be good if the title of each partial page updated the tab in your browser. Let's use the 'title
' attribute to specify the name of each page. Modify each anchor tag on your page to add a 'title
' attribute as shown below:
<a href="#home" title="Home" class="navbar-brand">
Home
</a>
<a href="#about" title="About">About</a>
<a href="#contact" title="Contact Us">Contact</a>
Add code at the bottom of the changePage()
function to set the document.title
property with the value from the 'title
' attribute just clicked upon.
function changePage(contentArea, hashValue) {
window.document.title =
$("a[href='" + hashValue + "']").attr("title");
}
Run the application and click on each menu item and watch the title change in the tab of your browser.
Move Pages to Different Folder
Most likely, you will want to separate many of your pages into separate folders. Create a \common folder under the \src folder and move the home.html, about.html and the contact.html pages into the new common folder. After moving them, you need to somehow specify the path to find these pages. You don't want to modify the href
attribute, so add a new 'data-page-path
' attribute to specify the path as shown in the following code:
<span style="background-color: transparent;"><a href="#home" </span>title="Home"
data-page-path="./common/"
class="navbar-brand">
Home
</a>
<a href="#about" title="About" data-page-path="./common/">
About
</a>
<a href="#contact" title="Contact Us" data-page-path="./common/">
Contact
</a>
Retrieve this path from the 'data-page-path
' attribute using jQuery. Not every anchor tag has a data-page-path
attribute so set the path to an empty string
if the attribute is not found. Concatenate the path
and the pageName
together when passing the file to the loadPage()
function.
function changePage(contentArea, hashValue) {
var path = $("a[href='" + hashValue + "']")
.data("page-path") || "";
var pageName = hashValue.substr(1);
spaController.loadPage(contentArea, path + pageName);
window.document.title = $("a[href='" + hashValue + "']").attr("title");
}
One note here; if you reference .CSS or .JS files from your partial pages within the \common folder, you will need to fully qualify those references. For instance, in the about.html page, add the "./common/" in front of each file name as shown below:
<link href="./common/about.css" rel="stylesheet" />
<script src="./common/about.js"></script>
Reset Menu Focus
Run your index.html page and again click on the About menu, then click on the Contact menu. Now click the back button. Pay attention to where the focus on the menu bar is located. It is still on the Contact menu. Click the back button again so you are back at the home page. The menu focus is still on the Contact menu. This is obviously something we need to fix.
Add a function named resetMenu()
. You will pass the current hash value like #home
or #about
to this function. You use this value to locate the anchor tag contained in the Bootstrap menu system. Once located, you set focus to that menu. Unfortunately, this also draws a dotted outline around that menu item. You can remove that outline by setting the outline style to 0
. Create the following function on your index.html page.
function resetMenu(hashValue) {
$("a[href='" + hashValue + "']").focus();
$("a[href='" + hashValue + "']").css("outline", "0");
}
Call this new resetMenu()
function from the bottom of the changePage()
function.
function changePage(contentArea, hashValue) {
resetMenu(hashValue);
}
Run the index.html page and ensure that everything still works, and that the back button now resets the menu focus to the current partial page in your SPA.
Move All JavaScript into Separate Files
The changePage()
and resetMenu()
functions can be used from any of your pages. Remove these two functions from your HTML page and move them into your spa-common.js file. After you move the changePage()
function, change "spaController.loadPage(…)
" in the function to simply "loadPage(…)
".
Modify the return
statement at the bottom of your closure. You only need to expose the changePage()
function and not the loadPage()
function.
return {
changePage: function (contentArea, hashValue) {
changePage(contentArea, hashValue);
}
};
In the onhashchange
event in your index.html page, add 'spaController.
' in front of the call to changePage()
.
window.onhashchange = function () {
spaController.changePage("contentarea",
window.location.hash);
}
Run your page and ensure everything is still working.
Get Rid of Start Up Code on SPA Page
Instead of having to have a document.ready on the SPA home page, you can make a couple of minor changes and make each page self-contained. First, modify the <contentarea>
tag to include a 'data-page-start
' attribute. Set that attribute to the first partial page you want to display when this page loads.
<contentarea data-page-start="#home"></contentarea>
Next, cut the document.ready
function from your index.html page and move it to the top of the spa-common.js file, just below the 'use strict
' command. Modify this function to look like the following:
$(document).ready(function () {
var content = $("[data-page-start]")[0].nodeName;
var start = $(content).data('page-start');
window.location.replace(window.location.href + start);
window.onhashchange = function () {
spaController.changePage(content, window.location.hash);
}
});
The first line locates the element with the 'data-spa-start
' attribute and retrieves the nodeName
property. The nodeName
property contains the name of the element, which in our case is "contentarea
".
The second line then locates that content area and retrieves the value in the data-spa-start
attribute. That value, in our case, is "#home
". This value is then passed to the replace()
method. Finally, in the call to the spaController.changePage()
method, you pass the value in the content variable.
Remove the complete <script>
tag where you had the document.ready
method from the index.html page. Run the page and ensure your application still works as expected.
Summary
In this blog post, you learned how to create your own Single Page Application (SPA) structure using just jQuery, Bootstrap and HTML. With very little code, you can create a nice separation of your HTML, CSS and JavaScript. Sure, they are frameworks that do all this for you, but sometimes those frameworks come with a price. Sometimes, keeping it simple is a very good way to code. In my next blog post, I show you how to create a CRUD page using the techniques presented here.