Single Page Application

An SPA (Single Page Application) which includes several pages and page transitions.



Launch App


Description


SPA or Single Page Applications are becoming quite fashionable because they are responsive and create wonderful user experiences. In an SPA there is no need for full page refreshes anymore; only partial pages are populated through behind-the-scenes Ajax calls. A disadvantage of SPAs is that they make it more difficult for Search Engines to properly index your web site.

In the sample SPA app, each page features a nice sliding effect that is implemented with jQuery's animation functionality. When the page is first loaded it displays a timestamp (in blue). You'll see it never changes, even when viewing different pages by clicking on the menus, meaning the page itself is never refreshed.

The template pages are brought in via <script> tags. This was done because the file:// protocol has limited privileges to load files (for those viewing this framework as a set of HTML files). However, if you are running it from a webserver (using http://) you can swap out the loadTemplates method to the one that is commented out. This one dynamically loads the template pages from the /templates directory. The advantage of using separate template files is that each developer can work on their own pages without interfering with other developers.

Here is the code for the SPA App:

// ** AMD pattern
require(['patterns'], function (Patterns) {
   
    // ** namespace pattern 
    // ** revealing module pattern
    // ** singleton pattern
    Patterns.namespace("InAction").App = (function () {

        // ** namespace pattern (Models, Collections, Views, Routers)
        var Models = {};
        var Views = {};
        var Routers = {};

        var start = function (content) {
            var router = new Routers.Router({ el: content });
            var templates = ["AboutUs", "OurStory", "Management", "Investors", 
                             "Careers", "Newsroom", "ContactUs"];
            loadTemplates(templates, function () {
                Backbone.history.start();
            });
        };

        // run this locally (with file:// protocol) 
        var loadTemplates = function (views, callback) {

            // ** iterator pattern
            $.each(views, function (index, view) {
                if (Views[view]) {
                    Views[view].prototype.template = _.template($("#" + view).html());
                } else {
                    alert(view + " not found.");
                }
            });

            callback();
        };

        // run this on a web server (with http:// protocol)
        //var loadTemplates = function (views, callback) {
            //var deferreds = [];

            // ** iterator pattern
            //$.each(views, function (index, view) {
            //    if (Views[view]) {
            //        deferreds.push($.get('../templates/' + 
            //                       view.toLowerCase() + '.htm', function (data) {
            //            Views[view].prototype.template = _.template(data);
            //        }));
            //    } else {
            //        alert(view + " not found.");
            //    }
            //});

            //// ** apply invocation Pattern
            //$.when.apply(null, deferreds).done(callback);
        //}

        var selectMenu = function (item) {
            $('.nav li').removeClass('active');

            // ** truthy/falsy idiom
            if (item) {
                $('.' + item).addClass('active');
            }
        };

        // ** extend pattern
        Routers.Router = Backbone.Router.extend({
            // ** init pattern
            initialize: function (options) {
                this.el = options.el;
            },
            routes: {
                "": "first",
                "aboutus": "display",
                "ourstory": "display",
                "management": "display",
                "investors": "display",
                "careers": "display",
                "newsroom": "display",
                "contactus": "display"
            },

            // ** Factory method
            factory: function(type) {
                switch(type) {
                    case "aboutus": return new Views.AboutUs();
                    case "ourstory": return new Views.OurStory();
                    case "management": return new Views.Management();
                    case "investors": return new Views.Investors();
                    case "careers": return new Views.Careers();
                    case "newsroom": return new Views.Newsroom();
                    case "contactus": return new Views.ContactUs();
                    default: return null;
                }
            },

            first: function () {
                var key = "aboutus";
                var view = this.factory(key);

                selectMenu(key);
                this.el.html(view.render().el);
            },

            display: function () {
                var key = Backbone.history.fragment;
                var view = this.factory(key);

                selectMenu(key);
                this.transition(view);
            },

            transition: function (view) {
                    this.el.animate({ width: '-=668' }, 300, function () {
                    this.el.html(view.render().el);
                    this.el.animate({ width: '+=668' }, 300);
                }.bind(this));
            },
        });

        // ** extend pattern
        Views.AboutUs = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());
                return this;
            }
        });

        // ** extend pattern
        Views.OurStory = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());
                return this;
            }
        });

        // ** extend pattern
        Views.Management = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());
                return this;
            }
        });

        // ** extend pattern
        Views.Investors = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());
                return this;
            }
        });

        // ** extend pattern
        Views.Careers = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());
                return this;
            }
        });

        // ** extend pattern
        Views.Newsroom = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());
                return this;
            }
        });

        // ** extend pattern
        Views.ContactUs = Backbone.View.extend({
            render: function () {
                this.$el.html(this.template());

                // ** zero timout pattern
                setTimeout(function () {
                    $("#name").focus();

                    // ** chaining pattern
                    $("#submit").off().on('click', function (e) {
                        alert("Great! Thank you for your message.");
                        return reset(e);
                    });

                    // ** chaining pattern
                    $("#reset").off().on('click', function (e) {
                        return reset(e);
                    });
                    var reset = function (e) {
                        e.preventDefault();
                        $("#name").val("").focus();
                        $("#email").val("");
                        $("#message").val("");
                        return false;
                    }
                },0);

                return this;
            }
        });

        return {
            start: start
        };

    })();

    $(function () {

        // Display timestap at top of page 
        $("#time").html(new Date().toLocaleTimeString());

        // ** facade pattern
        Patterns.InAction.App.start($("#content-slide"));
    });

});

Patterns and idioms in this sample include:

  • AMD Pattern
  • Truthy/falsy idiom
  • || and && idiom
  • Option Hash idiom
  • Namespace pattern
  • Single var pattern
  • Factory Method pattern
  • Apply Invocation pattern
  • Zero Timeout pattern
  • Module pattern
  • Extend pattern
  • Init pattern
  • Chaining pattern
  • Iterator pattern
  • Singleton pattern
  • Observer pattern
  • Façade pattern

This is the only Patterns-in-Action example in which we use Require.js showing the AMD pattern. We kept it simple by only loading the patterns.js file. It shows the basic principle of script loading and module dependencies.

This sample has a high level of code reuse, courtesy of the Factory Method pattern. The Factory Method allows us to map all urls to the same display method on the Router. The parameter named key in factory determines the view (page) that gets created and ultimately rendered.

The Zero Timeout pattern is used in activating a script for the Contact Us page. This gives JavaScript the time to render the page, so that the DOM is in place and the script can attach event handlers to the controls.

If you recall, the Apply Invocation pattern is the most flexible of the four invocation patterns. It is used with jQuery's when method which is applied to the deferred template loaders. When the loading of all templates is done a callback is invoked (which will start Backbone). The first argument in apply, which is the object context, is set to null because it is not used.


Launch App




  Mapping