The web as a platform is constantly growing. Naturally, the complexity of applications built for the web is constantly on the rise as well. For years, we have gotten along just fine with building traditional web apps with full-page reloads whenever making a transition from one page to another. But the web is more demanding now, They always seem to be a gap between the kind of user experience web applications offers as opposed to one offered by native desktop applications. When building a web application, there are generally two types of applications that we can build. Single Page Applications (SPA), and Multi-Page Applications (MPA). Everyone is familiar with the idea of MPA, it’s the traditional web app we all grew up in the building. SPA is the new kid on the block. It has the ultimate goal of providing a user experience similar to that of desktop/mobile applications. Building A Single Page Application: The idea behind a SPA-based website is to load all the necessary code with a single page load. After that, it dynamically updates that page through JavaScript as the user interacts with your application. With this kind of system in place, the page should never have to reload unless the user does so manually. To build such an application, we will be using only jQuery. We won’t be bothering much with the server end of the application. The majority of our application logic will be residing in the front end in the form of JavaScript code. Let’s take a look at the index.html file. This will be the only .html file we will be using in our application. All the contents of our web app will be directed through this file. <!DOCTYPE html> <html> <head> <title>My Application</title> </head> <body> <div class="app-container"> <h1>Hello World!</h1> </div> </body> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="spa.js"></script> <script src="app.js"></script> </html> 1234567891011121314 <!DOCTYPE html><html> <head> <title>My Application</title> </head> <body> <div class="app-container"> <h1>Hello World!</h1> </div> </body> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="spa.js"></script> <script src="app.js"></script></html> The HTML document consists of three important parts, so let’s quickly glance over them: spa.js: JavaScript code residing in this script will act as our SPA framework for building our application app.js: JavaScript code residing in this script will contain the main logic for our application .app-container: This will be the document node in our HTML document in which we will be injecting the view for our application Let’s take a look over at spa.js: function AppRouter() { this.routeConfig = {}; this.routeListing = { static: {}, // For Static Route Paths }; this.getAllRoutes = function() { return this.routeConfig; }; this.getStaticPathRoute = function(pathInformation) { var matchedRouteName = null; $.each(this.routeListing.static, function(routeName, routePath) { if (routePath == pathInformation) { matchedRouteName = routeName; return false; } }); return matchedRouteName; } this.getCurrentRoute = function() { // Assuming we're on app_dev var currentPath = window.location.pathname; var pathInformation = currentPath.replace('/symfony/spaProject/web/app_dev.php/', '/'); // Check if current url matches with any static routes var matchedRouteName = this.getStaticPathRoute(pathInformation); if (matchedRouteName != null) { return this.routeConfig[matchedRouteName]; } else { // Check for dynmaic routes console.log('route not found'); } return null; } this.addRoutingConfiguration = function(configuration) { this.routeConfig[configuration.routeName] = { model: (configuration.hasOwnProperty('model') ? configuration.model : {}), view: configuration.view, }; if (configuration.type == 'dynamic') { this.routeListing.dynamic[configuration.routeName] = configuration.pathName; } else { this.routeListing.static[configuration.routeName] = configuration.pathName; } } }; function AppView(appRouter) { this.initialize = function() { this.route = appRouter.getCurrentRoute(); this.view = this.route.view; if (this.view.hasOwnProperty('init')) { // Initialize Model var tempModel = this.route.view.init(); this.view.model = tempModel; } } this.render = function() { var templateString = this.view.loadTemplate(this.view.model); // Inject into view $('.app-container').empty(); $('.app-container').append(templateString); } // Updates Browser URL and History State this.updateRoute = function(pathInformation){ var newPathURL = '/symfony/spaProject/web/app_dev.php' + pathInformation; stateObj = {page: 'home'}; window.history.pushState(stateObj, 'Default', newPathURL); this.initialize(); this.render(); }; // For Local Reference var self = this; $('body').on('click', 'a', function(e) { e.preventDefault(); var targetElement = e.currentTarget; self.updateRoute(targetElement.pathname); }); }; 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 function AppRouter() { this.routeConfig = {}; this.routeListing = { static: {}, // For Static Route Paths }; this.getAllRoutes = function() { return this.routeConfig; }; this.getStaticPathRoute = function(pathInformation) { var matchedRouteName = null; $.each(this.routeListing.static, function(routeName, routePath) { if (routePath == pathInformation) { matchedRouteName = routeName; return false; } }); return matchedRouteName; } this.getCurrentRoute = function() { // Assuming we're on app_dev var currentPath = window.location.pathname; var pathInformation = currentPath.replace('/symfony/spaProject/web/app_dev.php/', '/'); // Check if current url matches with any static routes var matchedRouteName = this.getStaticPathRoute(pathInformation); if (matchedRouteName != null) { return this.routeConfig[matchedRouteName]; } else { // Check for dynmaic routes console.log('route not found'); } return null; } this.addRoutingConfiguration = function(configuration) { this.routeConfig[configuration.routeName] = { model: (configuration.hasOwnProperty('model') ? configuration.model : {}), view: configuration.view, }; if (configuration.type == 'dynamic') { this.routeListing.dynamic[configuration.routeName] = configuration.pathName; } else { this.routeListing.static[configuration.routeName] = configuration.pathName; } }}; function AppView(appRouter) { this.initialize = function() { this.route = appRouter.getCurrentRoute(); this.view = this.route.view; if (this.view.hasOwnProperty('init')) { // Initialize Model var tempModel = this.route.view.init(); this.view.model = tempModel; } } this.render = function() { var templateString = this.view.loadTemplate(this.view.model); // Inject into view $('.app-container').empty(); $('.app-container').append(templateString); } // Updates Browser URL and History State this.updateRoute = function(pathInformation){ var newPathURL = '/symfony/spaProject/web/app_dev.php' + pathInformation; stateObj = {page: 'home'}; window.history.pushState(stateObj, 'Default', newPathURL); this.initialize(); this.render(); }; // For Local Reference var self = this; $('body').on('click', 'a', function(e) { e.preventDefault(); var targetElement = e.currentTarget; self.updateRoute(targetElement.pathname); });}; spa.js consists of two functions AppRouter() & AppView() which will act as an object. AppRouter will be used to initialize our routes (we’ll get to that in a second), and AppView will be used to render our application’s view. Router: Unlike MPA, where our application is divided into multiple pages, we only have a single page in the case of SPA. The entire application is then managed through this single page. So there comes the obvious question of how we manage our routes, and when the load any particular pages. This will be done through the AppRouter. For each individual page for our application, we will create an object of AppRouter which will map to that given page, controlling the logic, loading the data from the server required for rendering the view for the respective page. So let’s take a look over at main.js and see how to go about that: var homeRouter = { pathName: '/', routeName: 'home', routeType: 'static', routeTitle: 'Home', view: { events: {}, loadTemplate: function(templateModel) { var templateBody = ''; templateBody += '<h1>Home Page</h1>'; return templateBody; } } }; var welcomeRouter = { pathName: '/welcome', routeName: 'welcome', routeType: 'static', routeTitle: 'Welcome', view: { events: {}, model: { username: 'Akshay Kumar', }, loadTemplate: function(templateModel) { var templateBody = ''; templateBody += '<h1>Welcome ' + templateModel.username + '</h1>'; return templateBody; } } }; $(document).ready(function() { // Load Routing var appRouter = new AppRouter(); appRouter.addRoutingConfiguration(homeRouter); appRouter.addRoutingConfiguration(welcomeRouter); // Render View var appView = new AppView(appRouter); appView.initialize(); appView.render(); }); 12345678910111213141516171819202122232425262728293031323334353637383940414243444546 var homeRouter = { pathName: '/', routeName: 'home', routeType: 'static', routeTitle: 'Home', view: { events: {}, loadTemplate: function(templateModel) { var templateBody = ''; templateBody += '<h1>Home Page</h1>'; return templateBody; } }}; var welcomeRouter = { pathName: '/welcome', routeName: 'welcome', routeType: 'static', routeTitle: 'Welcome', view: { events: {}, model: { username: 'Akshay Kumar', }, loadTemplate: function(templateModel) { var templateBody = ''; templateBody += '<h1>Welcome ' + templateModel.username + '</h1>'; return templateBody; } }}; $(document).ready(function() { // Load Routing var appRouter = new AppRouter(); appRouter.addRoutingConfiguration(homeRouter); appRouter.addRoutingConfiguration(welcomeRouter); // Render View var appView = new AppView(appRouter); appView.initialize(); appView.render();}); As we can see over here, we have created two JSON Objects home router and welcome route. If we take a look at our $(document).ready() function, once all the necessary resources have loaded, we first create an AppRouter object, then pass on all the available route configs for our application (home router & welcome route represented as JSON objects) to the AppRouter. Once we have included all the available routes, we then go on to create an AppView object, passing our AppRouter object as the constructor argument. Once we do that, all our routes are initialized into our application. App view works by checking the current URL in our web browser and then matching that to one of the many routes we created. Once a route is matched, then the route is loaded and it’s view accordingly. This cycle repeats whenever we make a transition from one view to another the URL in the browser updates. Conclusion: This is a very basic overview of the SPA pattern. It can be extended to create a more promising real-world application. We can extend this to fully implement the MVC pattern as well, specifically loading our model from the server and then injecting that into our view. There are no real benefits to building a Single Page Application other than to improve the user experience (which is very important), but like anything that’s ever invented, everything has its pros & cons. With SPA, you have to take into account many things, like the state of the application, security, and managing browser history, as well as take into consideration maintaining such a system. I personally prefer to follow a more “hybrid” approach where the application is a mix between the MPA & SPA pattern. And as the case is with many JS-based frameworks such as BackboneJS, AngularJS, etc. The support to build a Single Page Application is always there but is not always a necessity to build one. So use whatever works for you, and your end-users. Tag(s) AJAX JavaScript Single Page Application SPA Category(s) Design