# Single Page Applications
# What is a SPA?
A Single Page Application (SPA) is a single HTML file that contains a series of <div>
s or <section>
s that are displayed as if they are separate web pages.
We will use CSS to display only one of the "pages" at a time. They will be stacked one on top of the other. Each "page" will be styled to fill the entire screen.
The navigation menu will typically be placed as a bar across the bottom of the screen.
In most cases, when people are building SPAs they will add custom attributes to the "pages" and the navigation area.
<div data-role="page" id="home" class="active">
<!-- the contents of the page would be here -->
</div>
2
3
We can control the appearance of the pages by using CSS attribute selectors.
[data-role='page'] {
/* the CSS to make the section appear as if it is the whole page */
width: 100%;
position: absolute;
top: 0;
min-height: 100vh;
}
2
3
4
5
6
7
More about custom data- attributes
We make the sections use position: absolute;
if the content could be taller than the screen. If the content will never be as tall or taller than the screen then we can use position: fixed;
.
# The History API
One of the HTML5 APIs is the History API. It helps us to manage the history of pages and resources that have been visited. While the concept of page history has been with browsers since the beginning, the new part is the ability to update what is written in the location bar and inject new values into the history array.
# Navigating between "pages"
# Creating Pages
When building SPAs, we are using a single HTML file. However, to the user it needs to appear as if there are multiple pages. The common approach to achieving this is to use a data- property or a CSS class name to indicate which HTML elements are the pages.
<body>
<section data-role="page" id="one" class="active">
<h2>Page One</h2>
</section>
<section data-role="page" id="two" class="">
<h2>Page Two</h2>
</section>
</body>
2
3
4
5
6
7
8
This example shows two pages within the <body>
, using an attribute called data-role
. This attribute is just a made-up attribute like all other data- attributes.
In order to know which page is the currently visible one and which one should be hidden, we will use a CSS class called "active". The presence or absence of this className lets us know which page is being displayed to the user.
[data-role='page'] {
display: none; /* default state */
z-index: 10;
position: absolute;
top: 0;
left: 0;
right: 0;
min-height: 90vh;
}
[data-role='page'].active {
display: block;
z-index: 20;
}
2
3
4
5
6
7
8
9
10
11
12
13
# Navigation
There are two ways to create navigation menus in an SPA. First is to add a navigation area inside each of the sections. Second is to create a navigation area outside of all the pages.
<section data-role="nav">
<ul>
<li><a href="#one" class="active">One</a></li>
<li><a href="#two" class="">Two</a></li>
</ul>
</section>
2
3
4
5
6
Whether or not you put this section inside the page or outside the page, we need to be able to visually indicate which page is the current one. We can use the same className
as the one that we use for the pages.
[data-role="nav"]{
position: absolute;
bottom: 0;
width: 100%;
height: 10vh;
z-index: 30; /* higher than the pages */
}
[data-role="nav"] .active{
background-color: /* something with more contrast */
font-weight: bold;
}
2
3
4
5
6
7
8
9
10
11
We are positioning the nav section at the bottom of the page, with a z-index
that is higher than any of the pages.
# Scripting the Pages
After the DOMContentLoaded event (or the deviceready event when using Cordova) we can start scripting with all the pages. We will loop through all the pages
which are saved in a global variable.
var pages = document.querySelectorAll('[data-role="pages"]');
var links = document.querySelectorAll('[data-role="nav"] a]');
2
By default, all the pages will be hidden. We will use JavaScript to add and remove the active
classNames.
pages[0].className = 'active';
links[0].className = 'active';
2
First page element in the array will be set to active by adding a CSS class. This CSS class will determine whether pages are visible or hidden.
Clicking on a tab or main navigation button will call a function. The function will read the href from the tab or link. All pages will have the active
class removed. The page that matches the href from the anchor will have the active class added.
[].forEach.call(links, function(item) {
item.addEventListener('click', navigate);
});
function navigate(ev) {
//user clicked on one of the tabs
ev.preventDefault();
let url = ev.currentTarget.href;
//the href will end with something like "#one"
let id = url.split('#')[1];
[].forEach.call(pages, function(item) {
if (item.id == id) {
page.className = 'active';
} else {
page.className = '';
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
We need to call the preventDefault
method to stop the anchor click from making the html file being reloaded.
We loop through the pages looking for one whose id matches the href from the anchor tags that was clicked. When there is a match, we make that <section>
the one with the class active
. We also remove the class active from ALL other <section>
s.
Keep in mind that this example only shows two pages. However, this code will work for any number of pages
.
Here is a CodePen sample showing this at work:
Here is a more advanced version that uses the History API. This is where we are headed (but the code is more advanced than we need at this point. The version above will NOT handle the back button properly.