Making a Single Page App Without a Framework

Demo Download

The idea behind single page applications (SPA) is to create a smooth browsing experience like the one found in native desktop apps. All of the necessary code for the page is loaded only once and its content gets changed dynamically through JavaScript. If everything is done right the page shouldn't ever reload, unless the user refreshes it manually.

There are many frameworks for single page applications out there. First we had Backbone, then Angular, now React. It takes a lot of work to constantly learn and re-learn things (not to mention having to support old code you've written in a long forgotten framework). In some situations, like when your app idea isn't too complex, it is actually not that hard to create a single page app without using any external frameworks. Here is how to do it.

Note: To run this example after downloading it, you need a locally running webserver like Apache. Our demo uses AJAX so it will not work if you simply double-click index.html for security reasons.

The Idea

We will not be using a framework, but we will be using two libraries - jQuery for DOM manipulation and event handling, and Handlebars for templates. You can easily omit these if you wish to be even more minimal, but we will use them for the productivity gains they provide. They will be here long after the hip client-side framework of the day is forgotten.

The app that we will be building fetches product data from a JSON file, and displays it by rendering a grid of products with Handlebars. After the initial load, our app will stay on the same URL and listen for changes to the hash part with the hashchange event. To navigate around the app, we will simply change the hash. This has the added benefit that browser history will just work without extra effort on our part.

The Setup

SAP_tree1.png
Our project's folder

As you can see there isn't much in our project folder. We have the regular web app setup - HTML, JavaScript and CSS files, accompanied by a products.json containing data about the products in our shop and a folder with images of the products.

The Products JSON

The .json file is used to store data about each product for our SPA. This file can easily be replaced by a server-side script to fetch data from a real database.

products.json

[
  {
    "id": 1,
    "name": "Sony Xperia Z3",
    "price": 899,
    "specs": {
      "manufacturer": "Sony",
      "storage": 16,
      "os": "Android",
      "camera": 15
    },
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
    "rating": 4,
    "image": {
      "small": "/images/sony-xperia-z3.jpg",
      "large": "/images/sony-xperia-z3-large.jpg"
    }
  },
  {
    "id": 2,
    "name": "Iphone 6",
    "price": 899,
    "specs": {
      "manufacturer": "Apple",
      "storage": 16,
      "os": "iOS",
      "camera": 8
    },
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
    "rating": 4,
    "image": {
      "small": "/images/iphone6.jpg",
      "large": "/images/iphone6-large.jpg"
    }
  }
]

The HTML

In our html file we have several divs sharing the same class "page". Those are the different pages (or as they are called in SPA - states) our app can show. However, on page load all of these are hidden via CSS and need the JavaScript to show them. The idea is that only one page can be visible at a time and our script is the one to decide which one it is.

index.html

<div class="main-content">

    <div class="all-products page">

        <h3>Our products</h3>

        <div class="filters">
            <form>
                Checkboxes here
            </form>
        </div>

    <ul class="products-list">
      <script id="products-template" type="x-handlebars-template">​
        {{#each this}}
          <li data-index="{{id}}">
            <a href="#" class="product-photo"><img src="{{image.small}}" height="130" alt="{{name}}"/></a>
            <h2><a href="#"> {{name}} </a></h2>
            <ul class="product-description">
              <li><span>Manufacturer: </span>{{specs.manufacturer}}</li>
              <li><span>Storage: </span>{{specs.storage}} GB</li>
              <li><span>OS: </span>{{specs.os}}</li>
              <li><span>Camera: </span>{{specs.camera}} Mpx</li>
            </ul>
            <button>Buy Now!</button>
            <p class="product-price">{{price}}$</p>
            <div class="highlight"></div>
          </li>
        {{/each}}
      </script>

    </ul>

    </div>

    <div class="single-product page">

        <div class="overlay"></div>

        <div class="preview-large">
            <h3>Single product view</h3>
            <img src=""/>
            <p></p>

            <span class="close">&times;</span>
        </div>

    </div>

    <div class="error page">
        <h3>Sorry, something went wrong :(</h3>
    </div>

</div>

We have three pages: all-products (the product listing), single-product (the individual product page) and error.

The all-products page consists of a title, a form containing checkboxes for filtering and a <ul> tag with the class "products-list". This list is generated with handlebars using the data stored in products.json, creating a <li> for each entry in the json. Here is the result:

rsz_screenshot_-_13012015_-_145617.jpg
The Products

Single-product is used to show information about only one product. It is empty and hidden on page load. When the appropriate hash address is reached, it is populated with product data and shown.

The error page consist of only an error message to let you know when you've reached a faulty address.

The JavaScript Code

First, lets make a quick preview of the functions and what they do.

script.js

$(function () {

    checkboxes.click(function () {
        // The checkboxes in our app serve the purpose of filters.
        // Here on every click we add or remove filtering criteria from a filters object.

        // Then we call this function which writes the filtering criteria in the url hash.
        createQueryHash(filters);
    });

    $.getJSON( "products.json", function( data ) {
        // Get data about our products from products.json.

        // Call a function that will turn that data into HTML.
        generateAllProductsHTML(data);

        // Manually trigger a hashchange to start the app.
        $(window).trigger('hashchange');
    });

    $(window).on('hashchange', function(){
        // On every hash change the render function is called with the new hash.
        // This is how the navigation of our app happens.
        render(decodeURI(window.location.hash));
    });

    function render(url) {
        // This function decides what type of page to show 
        // depending on the current url hash value.
    }

    function generateAllProductsHTML(data){
        // Uses Handlebars to create a list of products using the provided data.
        // This function is called only once on page load.
    }

    function renderProductsPage(data){
        // Hides and shows products in the All Products Page depending on the data it recieves.
    }

    function renderSingleProductPage(index, data){
        // Shows the Single Product Page with appropriate data.
    }

    function renderFilterResults(filters, products){
        // Crates an object with filtered products and passes it to renderProductsPage.
        renderProductsPage(results);
    }

    function renderErrorPage(){
        // Shows the error page.
    }

    function createQueryHash(filters){
        // Get the filters object, turn it into a string and write it into the hash.
    }

});

Remember that the concept of SPA is to not have any loads going on while the app is running. That's why after the initial page load we want to stay on the same page, where everything we need has already been fetched by the server.

However, we still want to be able to go somewhere in the app and, for example, copy the url and send it to a friend. If we never change the app's address they will just get the app the way it looks in the beginning, not what you wanted to share with them. To solve this problem we write information about the state of the app in the url as #hash. Hashes don't cause the page to reload and are easily accessible and manipulated.

On every hashchange we call this:

function render(url) {

        // Get the keyword from the url.
        var temp = url.split('/')[0];

        // Hide whatever page is currently shown.
        $('.main-content .page').removeClass('visible');

        var map = {

            // The Homepage.
            '': function() {

                // Clear the filters object, uncheck all checkboxes, show all the products
                filters = {};
                checkboxes.prop('checked',false);

                renderProductsPage(products);
            },

            // Single Products page.
            '#product': function() {

                // Get the index of which product we want to show and call the appropriate function.
                var index = url.split('#product/')[1].trim();

                renderSingleProductPage(index, products);
            },

            // Page with filtered products
            '#filter': function() {

                // Grab the string after the '#filter/' keyword. Call the filtering function.
                url = url.split('#filter/')[1].trim();

                // Try and parse the filters object from the query string.
                try {
                    filters = JSON.parse(url);
                }
                // If it isn't a valid json, go back to homepage ( the rest of the code won't be executed ).
                catch(err) {
                    window.location.hash = '#';
                }

                renderFilterResults(filters, products);
            }

        };

        // Execute the needed function depending on the url keyword (stored in temp).
        if(map[temp]){
            map[temp]();
        }
        // If the keyword isn't listed in the above - render the error page.
        else {
            renderErrorPage();
        }

    }

This function takes into consideration the beginning string of our hash, decides what page needs to be shown and calls the according functions.

For example if the hash is '#filter/{"storage":["16"],"camera":["5"]}', our codeword is '#filter'. Now the render function knows we want to see a page with the filtered products list and will navigate us to it. The rest of the hash will be parsed into an object and a page with the filtered products will be shown, changing the state of the app.

This is called only once on start up and turns our JSON into actual HTML5 content via handlebars.

function generateAllProductsHTML(data){

    var list = $('.all-products .products-list');

    var theTemplateScript = $("#products-template").html();
    //Compile the template​
    var theTemplate = Handlebars.compile (theTemplateScript);
    list.append (theTemplate(data));

    // Each products has a data-index attribute.
    // On click change the url hash to open up a preview for this product only.
    // Remember: every hashchange triggers the render function.
    list.find('li').on('click', function (e) {
      e.preventDefault();

      var productIndex = $(this).data('index');

      window.location.hash = 'product/' + productIndex;
    })
  }

This function receives an object containing only those products we want to show and displays them.

function renderProductsPage(data){

    var page = $('.all-products'),
      allProducts = $('.all-products .products-list > li');

    // Hide all the products in the products list.
    allProducts.addClass('hidden');

    // Iterate over all of the products.
    // If their ID is somewhere in the data object remove the hidden class to reveal them.
    allProducts.each(function () {

      var that = $(this);

      data.forEach(function (item) {
        if(that.data('index') == item.id){
          that.removeClass('hidden');
        }
      });
    });

    // Show the page itself.
    // (the render function hides all pages so we need to show the one we want).
    page.addClass('visible');

  }

Shows the single product preview page:

function renderSingleProductPage(index, data){

    var page = $('.single-product'),
      container = $('.preview-large');

    // Find the wanted product by iterating the data object and searching for the chosen index.
    if(data.length){
      data.forEach(function (item) {
        if(item.id == index){
          // Populate '.preview-large' with the chosen product's data.
          container.find('h3').text(item.name);
          container.find('img').attr('src', item.image.large);
          container.find('p').text(item.description);
        }
      });
    }

    // Show the page.
    page.addClass('visible');

  }

Takes all the products, filters them based on our query and returns an object with the results.

function renderFilterResults(filters, products){

      // This array contains all the possible filter criteria.
    var criteria = ['manufacturer','storage','os','camera'],
      results = [],
      isFiltered = false;

    // Uncheck all the checkboxes.
    // We will be checking them again one by one.
    checkboxes.prop('checked', false);

    criteria.forEach(function (c) {

      // Check if each of the possible filter criteria is actually in the filters object.
      if(filters[c] && filters[c].length){

        // After we've filtered the products once, we want to keep filtering them.
        // That's why we make the object we search in (products) to equal the one with the results.
        // Then the results array is cleared, so it can be filled with the newly filtered data.
        if(isFiltered){
          products = results;
          results = [];
        }

        // In these nested 'for loops' we will iterate over the filters and the products
        // and check if they contain the same values (the ones we are filtering by).

        // Iterate over the entries inside filters.criteria (remember each criteria contains an array).
        filters[c].forEach(function (filter) {

          // Iterate over the products.
          products.forEach(function (item){

            // If the product has the same specification value as the one in the filter
            // push it inside the results array and mark the isFiltered flag true.

            if(typeof item.specs[c] == 'number'){
              if(item.specs[c] == filter){
                results.push(item);
                isFiltered = true;
              }
            }

            if(typeof item.specs[c] == 'string'){
              if(item.specs[c].toLowerCase().indexOf(filter) != -1){
                results.push(item);
                isFiltered = true;
              }
            }

          });

          // Here we can make the checkboxes representing the filters true,
          // keeping the app up to date.
          if(c && filter){
            $('input[name='+c+'][value='+filter+']').prop('checked',true);
          }
        });
      }

    });

    // Call the renderProductsPage.
    // As it's argument give the object with filtered products.
    renderProductsPage(results);
  }

Shows the error state:

function renderErrorPage(){
    var page = $('.error');
    page.addClass('visible');
  }

Stringifies the filters object and writes it into the hash.

function createQueryHash(filters){

    // Here we check if filters isn't empty.
    if(!$.isEmptyObject(filters)){
      // Stringify the object via JSON.stringify and write it after the '#filter' keyword.
      window.location.hash = '#filter/' + JSON.stringify(filters);
    }
    else{
      // If it's empty change the hash to '#' (the homepage).
      window.location.hash = '#';
    }

  }

Conclusion

Single page applications are perfect when you want give your project a more dynamic and fluid feel, and with the help of some clever design choices you can offer your visitors a polished, pleasant experience.

Bootstrap Studio

The revolutionary web design tool for creating responsive websites and apps.

Learn more

Related Articles

Danny Markov

Thank you for reading our tutorial! How do you usually create single page apps? Would you try the hashchange technique we outlined here, and go for a framework-less approach?

Kanishk Kunal

Hi Danny! Another great post.
After having tried AngularJS, EmberJS, React and few other frameworks, I agree with you about spending time in constantly learning and re-learning new things instead of getting actual work done.
Recently, I also decided to take the approach of going framework-less and used just jQuery, Handlebars & Charts.js for one of my single page app which I did as side hobby project. The app pulls uptime status data for all the websites that I manage from UptimeRobot and displays it along with graphs of response times. The code is available at GitHub under MIT License in case anyone is interested.

However, I was not aware of this hashchange technique and this will surely enable me to take my single page apps to next level without relying on large frameworks for routing. Thanks.

Really a nice article Danny. Have you tried writing a SPA using HTML injection (leveraging the innerhtml property) to bring all new content into the page. I have been doing this for over 10 years and have found it is a much faster and simpler way to develop than using json. It still retains much of the state on the browser, but allows you to keep the security on the server. The developer also gets the advantage of only having to render the data one time with the layout server side, full page refresh style.

I have put together a framework based on what I've learned that wraps the async call to take care of all the mundane things one needs to do when pulling in content this way. It is a super super light framework that only makes the content delivery standard and gives the developer a javascript stack of DOM views. I am giving it away for free so you can download it and play around with it if you like. It's called WebRocketx. You can find it by searching for it by name. Thanks.

Panos R.

I ve recently desided to start moving my new wep applications from server to client side with SPA in mind, not only for the users sake, but also because it would help me write less overall code. I write in MS stack for more that a decade, so WebApi and jQuery come to the rescue.
After lots of personal RnD, I ve concluded that well structured JavaScript + jquery is sufficient for small-mid projects (at least) and I get to keep the control without any reading.
So here is what I do. I use prototypes for each presentation part that use ui loaded from "hidden" script templates. These classes are my viewModels and are equipped with all methods that get and send data to the server. I also have methods like databind, beginEdit etc that load and bind views accordanly. I use modals and append templates constantly. I do not use a template system yet, but I follow the modular and pubsub techniques. The code is match less on client or server and after the core is authored and organized, development really speeds up. Except from the manual set/get of elements with data that is kind of boring, I find that my skills and experience with web is enough to get me where I want. I believe that Frameworks come to organize teams but if you work alone, they can be left aside
Nice article.
Regards.

Can you point us to your framework, thank you very much.

John Donavan

Thanks Danny that was a different technique from the usual one. Would definitely try this new hashchange technique. The approach is good but need to know the implementation and the result for the same.

Awesome. But it doesn't work on my local PC :(

Danny Markov

You should open the application through a locally running web server.
The app won't work if you just double click the html file because of AJAX security restrictions.

How do we do that please? I'm using Brackets IDE but its not running the Handlebars part

Cristian Muscalu

Try this ( https://www.npmjs.com/package/http-server) . You can install it very easy with npm install -g http-server

Great article! Thanks :D
I have been thought about web page without framework in last month, same with your combination here, especially the use of handlebars, and then your article appears. It's just like a de javu for me. Very helpful.

So jQuery is not a framework ?

Martin Angelov

Nope, it is a library. Larger beasts like AngularJS are frameworks. Here is a rule of thumb - if a project only gives you functions that you call, it is a library. If it dictates how you should structure your application, it is a framework. The main problem with frameworks is that you have to rewrite your entire application if you decide to use a different one. This is especially problematic in web development, where we get new popular client-side frameworks every year.

Spot on! I have also been going backwards now after endless new learning, the one thing that got stuck is jQuery, just for being such an amazing library.

These days I rather get things done quicker with simple AJAX calls than having to learn the new geekiest way to do so for the time being. I feel like I've wasted a fair time on the Angular docs now only to go back to basics.

Although when I really want to make something special, it's hard to look away from all of the new amazing frameworks.. but for simple client pages, I've had enough. I'm just kicking out wordpress pages or simple vanilla JS onepagers so I can manage time better

I think it was the ejs author who said it best. Something like, "We already have JavaScript. We don't need yet another Turing complete language." I don't know where Angular is going, but at Angular 1.0, it seemed to be using HTML, and overloading that mess with a whole new world of unnecessary pain! Over the years, I have started to think the whole HTML CSS thing has morphed into the current browser enterprises building a moat of side effects around their empires. Here is a great tutorial for cutting to the quick as much as possible.

Good luck with SEO. Google will never index your pages. I'd only use this for the behind-the-login-wall pages like admin part of the site. Angular has similar problems, React though has an option of server-side rendering, so you may have content available both through json api and statically.

Martin Angelov

Yes, SEO is a problem with every client-side framework. At least for now, you shouldn't use this approach for content-driven sites.

This sounds interesting. I have been working on SPAs using Durandal Knockout and Breeze Js. But the learning curve for them hasn't been smooth. Can I adapt this demo into a commercial project I am working on of similar nature. Thanks again

Danny Markov

Go ahead, we would be happy to see our demo turn into a fully functional project.

License

Chris Love

Danny thanks for posting this article. This is close to the architecture I have been recommending for a few years now. I wrote a book a little over a year ago going into many details, http://amzn.to/1mNTP00

A modular approach with small libraries is the best way to build modern web applications. It gives the most flexibility and best performance.

SomeNoobStranger

I also have this feeling since I was starting the AngularJS route since about 8 months ago. I still haven't finished the project, because there are too many AngularJS specific problems on way and debugging them is like hell. However, one question that arose to my mind is: How do you test your javascript without a proper framework? I mean, Karma and the likes dó help a bunch with that.

Eager to hear you response!

When I'm developing something like this, the fastest resource is console along with Dev Tools. You'd be surprised how far those 2 things alone can get you.

majid mohammadnejad

thank you for your tutorial.you are big man

Miguel Mota

React is not a framework. It's just the view layer. And if you're not using a framework, you'll just end up creating your own. It's inevitable.

"And if you're not using a framework, you'll just end up creating your own. It's inevitable"...

TRUE. But in that case you own the framework, you can customize it, extend it as needed.

"But in that case you own the framework, you can customize it, extend it as needed."
True, but then you are one/few developers while popular framework improved by hundreds or thousands.

mproving

The example demo is a good example of a website that should not be a single page app.

Making a store like that a single page app makes it impossible to do things like opening up multiple products in browser tabs when comparing, or booking marking items. Web browsers convenient and familiar interfaces and that has a lot to do with what makes the web work so well. It's nice to have tabs and back buttons and unique URLs, and you lose all that if you make an app that only exists on one single page.

Single page type apps work well if you're making something highly dynamic like a game, a drawing program, or an online word processor. But not for an online store. Don't hijack the web browsing experience just to make a website more flashy.

it's just a starting example. you could implement opening products in a new tab.

I like it over flavor-of-the-day frameworks. I do like frameworks for what they provide, but most are too primitive, and some such as AngularJS too complex for the value addition they provide.

gjcarrow

Is this technique something that you might see some of the current frameworks using "under the hood" to do their routing?

OMG...Thank you! This was everything I wanted and needed. It's frankly difficult to find tutorials (or just simply a small walk-through with images or code to get the gist) of creating a SPA without use of frameworks or libraries. I'm planning to create one for a simple task and I had a hard time concretely wrapping my head around the hierarchy (probably not the best word) of how it's set up concerning not only the folder architecture, but also in the HTML. This was beyond helpful! I just want to use bare JS, no JQuery or Handlebars, would it be wise to split up the JS into multiple files? Maybe like single-goal files (so all the functions used to render an error, all the functions used to render the filters, etc.?) or should I stay away from that idea since it's bad practice, etc.?

Either way, finally, a great, thorough tutorial!!!

thanks a lot.

the local file works well on Firefox (only Chrome blocks local ajax operations). just need to add "http" to the JS lib files.

Sheo Narayan

I saw discussion about making it SEO friendly.

This tutorials shows how to create SEO friendly single page application development. You can directly copy-paste the link and you would get the respective view. It is built with AngularJS and ASP.NET Web API and the downloadable source code is available.

http://techfunda.com/Howto/angularjs/single-page-application

Greetings,
Just to let you know that the design and the code are beautiful.
The app runs fine on Chrome, but fails in IE11 and Firefox. When you click on any of the filters the page refreshes and so nothing is seleced?

Thanks,
Shaka

Danny Markov

Thanks for the heads up, Shaka!

The cause of this bug was hidden in the way window.location.hash is read. For some reason Firefox escapes the special characters from the url, while Chrome doesn't. This causes the hash to appear as invalid and the app is returned to it's initial state (not actually refreshing the page, just everything from the hash removed).

The solution was to decode the url, before rendering: render(decodeURI(window.location.hash)); (line 116 from script.js).

The demo is updated and should work properly now :)

Great one! I think for the filter function will be quite convenient to use underscore.js

Nice tutorial, gr8 work and good comments from members. It cleared a lot of doubts and suspicions I had about popular notions.
Frameworks were never friendly and none of them accomplished the objective of a framework 2 reduce complexity and increase ease and lure new less skilled/new developers into the web/app building arena.
Pls do continue the good work, it is really appreciated.

I like this approach and will try it out on a site I'm building today. THANKS for taking the time to lay it out so clearly. If all tutorials were this well done the web would be a better place.

Parkash Kumar

Largely, these SPA frameworks and techniques are handy for only content driven application with some of limitations. However, if someone wants to implement any of these in their project, they will require to change server side logic as well. That's the main headache.

intersel

For all these issues given in this article and comments, I ended to develop my own library/stack/framework based on jquery with the idea to be able to have an SPA without having to change my way of programming website with a normal CMS and php... working on html blocks that would be automatically updated with ajax from page to page simply using html syntax and standard js libraries...

I would be please to have your feedback on this approach that you could find on github (https://github.com/intersel/Blapy)...

This is quite interesting. I will have a play around with your application and get back. For a start it looks quite cool as I had done something similar in the past but yours is way more advanced.

Can I pull data from a database using php and let Blapy handle the rest?

Thank you for sharing.

intersel

Thanks for your feedback.
Yes sure you can get your data from your database with php, and send them to be handled by blapy on the frontend side.

Any idea why it doesn't work on IE 11? It works initially but crashes when you open single product view and close it down.

if you replace "products.json" with "https://api.myjson.com/bins/tnr3&quot; it will work on all browsers.

Aamir Ghanchi

Thanks for dispelling the myth for newbies that frameworks are required to develop SPA. They are not.

So if I build a single page app for my band that lists upcoming shows and shows old pictures, and plays songs and such. How can I make a "landing page" to share on social media that directs people to a specific photo or a specific event reservation part of the site? Is it as simple as grabbing the url hash and all?

Excellent article and thank you.

I was having difficulty getting this to work and discovered that on our version of IIS, a mime type for .json was not defined. Once I defined and restarted IIS, everything worked like a charm. Instructions: http://stackoverflow.com/questions/8158193/how-to-allow-download-of-json-file-with-asp-net ( pay no mind to the .asp stuff )

How can I eliminate all the text that shows up in the address bar as filters are clicked? If someone that is using this link that isn'tcomputer savvy they can potentially get a massive link full of javascript/json info and I can't have that. Is there anything I can add or remove the script that changes how the hash change or json.stringify works? Any help would be a serious life saver!!!

Maybe instead could I use Bit.ly API to shorten the link in the address bar? Does anyone know how I can do this? Please help