DEV Community

Vinay NP
Vinay NP

Posted on • Edited on • Originally published at askvinay.com

Building a single page application with vanilla js

Often times I've come across this framework vs that framework debate. Many times as an observer, some times as a participant and occasionally as the person who *cough* started the debate *cough*. Most frequent arguments in these debates are around the comparatively easy ways to do stuff or the lesser code needs to be written points. What I don't see people talking about is writing vanilla js and structuring your project better instead of using a framework. What comes across as a shock to me is that many developers don't know anything more outside of their favorite frameworks and are scared to write plain js code. If you decide to code for a living, how can you not know the building blocks of the plugin you use??

A lot of good folks have talked about this in the past. Some of my favorite reads are zero framework manifesto and Look ma, no frameworks. Many of these frameworks have a wonderful way of marketing themselves by presenting their top features or perceived benefits of usage on their websites or through developer's blogs, however, I don't see as many folks showing ways of building SPA with vanilla js. I therefore decided to refactor my personal website as a SPA without using any framework. I hope that this post will serve as a good first step when you are building an app on your own without using any frameworks. All code referenced here is available at vinay20045.github.io repo and this website itself acts as a live demo.

Design
Prior to refactoring my website was a typical blog written in PHP. Every page request used to do a round trip to a server for all html content and assets, it had a management console etc. During refactoring some of my considerations were...

  • No page loads for every post i.e. it should be an SPA
  • Posts to be written using markdown syntax.
  • The blog should be written only in HTML+CSS+JS
  • Hosting to be done on github pages or AWS S3
  • It had to be mobile friendly

with these things in mind, the high level design of the blog looks like this...

askvinay.com SPA design

Basic Structure
One of the primary things that you should be looking at while developing any application is the organization of the code. This includes everything right from your folder structure and naming conventions to declarations and definitions. A lot of developers I've seen argue over 2 line breaks vs 1 line break but are ok with having business logic in the views or templates. Anyways, once you do this for one project, it sort of acts like a boilerplate and will be very easy to replicate and extend for your future projects.

The basic structure of the blog application looks like this...

|-- assets
|   |-- css -- All site styles go here
|   |-- images -- All images used in the templates or page shell go here
|   `-- js
|       |-- config.js -- Environment specific config file
|       |-- init.js -- Contains all instructions on load
|       |-- controllers -- Business logic and view manipulation functions
|       |-- templates -- context based reusable snippets of HTML
|       |-- utils -- All internal and 3rd party libraries
|       `-- views -- Views exposed to the user
|-- index.html -- Page shell. Acts like a container. Actual content is populated based on route
|-- posts -- All posts markdown files go here
`-- uploads -- All assets used in posts go here
Enter fullscreen mode Exit fullscreen mode

Routing
It becomes very important to have proper routing in place to facilitate deep linking, book marking and better SEO. Many techniques can be used for routing but hash based routing works really well and is easy to implement. On load of the application a routing function is registered against the hashchange event.

The routing function, part of utils library, looks like this...

router: function(route, data){
    route = route || location.hash.slice(1) || 'home';

    var temp = route.split('?');
    var route_split = temp.length;
    var function_to_invoke = temp[0] || false;

    if(route_split > 1){
        var params  = extract_params(temp[1]);
    }

    //fire away...
    if(function_to_invoke){
        views[function_to_invoke](data, params);
    }
}
Enter fullscreen mode Exit fullscreen mode

extract_params function looks like this...

var extract_params = function(params_string){
    var params = {};
    var raw_params = params_string.split('&');

    var j = 0;
    for(var i = raw_params.length - 1; i >= 0; i--){
        var url_params = raw_params[i].split('=');
        if(url_params.length == 2){
            params[url_params[0]] = url_params[1];
        }
        else if(url_params.length == 1){
            params[j] = url_params[0];
            j += 1;
        }
        else{
            //param not readable. pass.
        }
    }

    return params;
};
Enter fullscreen mode Exit fullscreen mode

The event listener is registered in init.js...

window.addEventListener(
    "hashchange", 
    function(){utils.router()}  // the router is part of the utils library
);
Enter fullscreen mode Exit fullscreen mode

Dissecting controllers
Controllers hold the business logic. You can use the functions in here to manipulate your views. These functions are not exposed to the user directly. They can access only templates and the libraries available in utils. They can be invoked by a view or another controller.

The controller taking care of the home page looks like this...

controllers.home_page = function(data, params){
    var all_posts = JSON.parse(data);

    var posts_to_show = 3;
    var template_context = [];
    for (var i = 0; i < posts_to_show; i++){
        var post = all_posts[i];
        var item = {
            'link': '#post?'+post.post,
            'title': post.post.replace(/-/g, ' '),
            'snippet': post.snippet,
            'published_on': post.added_on,
        };
        template_context.push(item);
    }

    //get recent posts
    var recent_posts = templates.recent_posts(template_context);

    //get hello text
    var hello_text = templates.hello_text();

    var final_content = hello_text + recent_posts;
    utils.render(
        'page-content',
        final_content
    );    
};
Enter fullscreen mode Exit fullscreen mode

Dissecting templates
Templates hold HTML markup for the actual page content. It helps in reusability when you can have functions generating the HTML you want based on some context passed. All functionality for the templates have to be provided by the controller invoking it by using data binding and event registration techniques. The only exception that I've allowed are the hrefs.

The template for the hello section of the home page is...

templates.hello_text = function(data){
    var content = `
        <div id="hello_text">
            <h2>Hello...</h2>
            <img src="assets/images/Vinay.jpg" align="left" style="width:70px;">
            <p>
                Thank you for visiting my blog. I am Vinay Kumar NP. I am a passionate techie...
            </p>
            <p>
                I am currently working on a <a href="http://www.int.ai/" target = "_BLANK">startup</a> of my own. I have previously worked in various engineering leadership positions at...
            </p>
        </div>
    `;

    return content;
};
Enter fullscreen mode Exit fullscreen mode

Dissecting views
Views are the functions that are directly exposed to user. i.e. they are invoked by the router and are part of the url. There is no other difference between view functions and controllers. You could expose controllers too, but that might hurt modularity.

The view for all posts page looks like this. It simply passes the request to load show_posts controller after making an ajax call to get the posts index file.

views.all_posts = function(data, params){
    var api_stub = 'posts/index.json';

    utils.request(
        api_stub,
        'show_all_posts',
        'show_all_posts_error'
    );
};
Enter fullscreen mode Exit fullscreen mode

Making API Requests
This is the holy grail of any SPA (sic). Though my blog does not need a mechanism to make outside api calls as all my posts are hosted within, I've written it to illustrate the concept. The request method takes the api stub, call back functions, params and fires the request. This is also part of the utils library. (Please be careful of CORS here).

The function to make api calls looks like this...

request: function(api_stub, success_callback, error_callback, callback_params){
    api_stub = api_stub || '';
    callback_params = callback_params || {};

    controllers.show_loader('page-content');

    var url = config.api_server + api_stub;

    var x = new XMLHttpRequest();
    x.onreadystatechange = function(){
        if (x.readyState == XMLHttpRequest.DONE) {
            if(x.status == 200){
                controllers[success_callback](
                    x.responseText, 
                    callback_params
                );
            }
            else{
                controllers[error_callback](
                    x.status, 
                    callback_params
                );
            }
        }
    };
    //other methods can be implemented here
    x.open('GET', url, true);
    x.send();
}
Enter fullscreen mode Exit fullscreen mode

I haven't gotten a chance to do a comparative bench marking but at first glance all my repaints are done very fast with little or no jank. For the question of too many network calls on first load, I am planning to build a python based site packager for one of my other projects, I will post it once done.

There you go, wasn't that easy?? If you're still not convinced, fire up a browser, clone my repo, make the necessary changes (config, templates etc.) and play around. I'm pretty sure that not only will you come around to start building your own js applications, framework free; you will also be contributing more libraries to the open source world... The world needs more people who share :)

I've tested the code on all modern browsers (except IE) and it seems to work without any glitches. Watch out for JS api compatability while building your own applications (For ex, I've used back ticks which are not compatible with older browsers). Let me know if you find any bugs or issues with the code.

This post was first published on my blog

Top comments (8)

Collapse
 
slmyers profile image
Steven Myers

Yeah, this works well for a blog which is more-or-less rendering static content. However, when you start to write applications, or sites with many components and disparate state, it's better to reach for a well established framework with a strong ecosystem. I think the takeaway is that a framework is not always needed and I agree with that.

Collapse
 
tra profile image
Tariq Ali

Yeah, it usually isn't a good idea to turn a blog into a SPA for the reason you mention. If you want interactivity for a site, you may have to use JS, but if you just want people to consume your content, you might be better sticking with CSS and HTML.

Collapse
 
antonfrattaroli profile image
Anton Frattaroli

Agree but needs pushstate over hashchange

Collapse
 
rafaelbadan profile image
Rafael

Awesome post Vinary. I've just started learning web development, and one of my doubts was related to this gap between learning the building blocks of web apps and learning how to use a framework to get the job done. This post helps a lot in that matter, this structure looks like a great tool for learning fundamentals ;)

Collapse
 
xtrasmal profile image
Xander

I like the thought of an approach in plain javascript. At some point you might need a framework, but until then, you will have code that lasts longer and is more accessible for other javascripters(framework agnostic, so you don't need to learn more then you already know) then most frameworks. Like think of all the Angular 1 code that needs to be maintained until a massive refactor or migration.

Collapse
 
seinopsys profile image
David Joseph Guzsik

I've personally been using jQuery for the longest time, not because "I don't know the building blocks" but because if I choose not to I'd be spending time essentially rewriting 75% of jQuery as individual functions just to make my code easier to manage (+ the bragging rights that I was so tough I avoided it) instead of getting started on whatever I wanted to do straight away.

Collapse
 
wimdebok profile image
wimdebok

Great article and it stands out in simplicity. Very welcome for a newcomer in the wonderful world of JS libs, tools and frameworks.

Collapse
 
courier10pt profile image
Bob van Hoove

I like your approach. You show how to make some of the common building blocks. It's easy to further customize. There may be situations where this is beneficial. Food for thought :)