1
\$\begingroup\$

I made a basic SPA framework because I tried to make a SPA app before and had some issues. I then modeled this after that experience to account for everything.

Project page

Code

The things I noticed when doing an SPA is:

  1. Need to support browser back and forward buttons, listening for popstate event was added.
  2. Need to change the URL completely, so history.pushState was used.
  3. Need to lazy load instances of pages, so a function that makes an instance of a page is required rather than a direct class. It returns it via callback parameter. This way it is asynchronous to support Ajax calls in between, like getting data or checking if user is still authenticated/logged in.
  4. If I am on page A with some data, then go forward to page B, then go forward to page A with new data, I then want to go back twice, and get back on page A with the old data, so that means the page A with old data would still have to be in the DOM with a different URL. So I put a timestamp on the URL to ensure a unique URL. New URLs generate a new instance of a DOM element in the DOM.
  5. Data parameters should be in the URL, that way if you were to copy the URL and give to someone else, they should load the same page with same data.
  6. Even though it's a SPA, there are elements on the page that are persistent such as headers, footers, navigation drawers, etc... So I put support for page layout and context DOM element pointers, which allow you to keep persistent parts of the page. So you can make a general layout with a header and footer, and set the body part as the context, then the SPA pages are just within the body part. This way only one page within the body shows, but the header and footer always shows.
  7. Events to run when a page is first opened, opened, or closed, to help with lazy loading data.

I believe all these are accounted for in a pretty simplistic and small way.

Does anyone see any issues I may of missed? Also, this is just about SPA, so I don't want to deal with any data binding or templating issues.

Code

var SPA = new function() {

    var opened = {},
        pages = [],
        active = null;

    this.timestamp = () => new Date().valueOf();

    this.addPages = function(user_pages) {
        user_pages.forEach(p => pages.push(p));
    };

    this.start = function(data) {
        loadPageFromHash(data);
    };

    this.openPage = function(stateObj, url) {
        history.pushState(stateObj, "", url);
        loadPageFromHash(stateObj);
    };

    function loadPageFromHash(data) {
        var path = window.location.pathname;
        var page = pages.find(p => p.path.toLowerCase() == path.toLowerCase()) || pages.find(p => p.default);
        if (page) {
            var url = window.location.pathname + window.location.hash + window.location.search;
            var p = opened[url];
            if (p) {
                enablePage(page, p);
            } else {
                openPage(url, page, data);
            }
        } else {
            console.warn("SPA: No route found for", path);
        }
    }

    function enablePage(page, p) {
        pages.forEach(p => {
            p.layout.style.display = p == page ? "block" : "none";
        });

        page.context.childNodes.forEach(c => {
            c.style.display = c == p ? "block" : "none";
        });

        if (active) active.close();
        active = page;

        if (!page.opened) {
            page.opened = true;
            page.first_open();
        }
        page.open();
    }

    function openPage(id, page, data) {
        page.content(p => {
            opened[id] = p;
            page.context.appendChild(p);
            enablePage(page, p);
        }, data);
    }

    window.addEventListener('popstate', event => {
        var data = event.state;
        loadPageFromHash(data);
    }, false);
};

Example

<!DOCTYPE html>
<html>
    <head>
        <title>SPA Test</title>
        <script src="spa.js"></script>
    </head>

    <body>
        <script>

            var layout = document.createElement("div");
            var h = document.createElement("div");
            var b = document.createElement("div");
            var f = document.createElement("div");
            layout.appendChild(h);
            layout.appendChild(b);
            layout.appendChild(f);

            h.innerHTML = "header";
            f.innerHTML = "footer";

            document.body.appendChild(layout);

            var c1 = function(callback, data) {
                var a = document.createElement("a");
                a.innerHTML = "click";
                a.setAttribute("href", "javascript:void(0);");
                a.addEventListener("click", function() {
                    SPA.openPage({a:1}, "bar?t=" + SPA.timestamp());
                });

                var r = document.createElement("div");
                r.appendChild(a);
                callback(r);
            };
            var c2 = function(callback, data) {
                var r = document.createElement("div");
                r.innerHTML = "content2";
                callback(r);
            };

            SPA.addPages([
                {
                    path : "/foo",
                    layout : layout,
                    context : b,
                    content : c1,
                    default : true,
                    opened : false,
                    first_open : function() {
                        console.log("foo first opened");    
                    },  
                    open : function() {
                        console.log("foo opened");
                    },
                    close : function() {
                        console.log("foo closed");
                    }
                },
                {
                    path : "/bar",
                    layout : layout,
                    context : b,
                    content : c2,
                    default : false,
                    opened : false,
                    first_open : function() {
                        console.log("bar first opened");    
                    },
                    open : function() {
                        console.log("bar opened");
                    },
                    close : function() {
                        console.log("bar closed");
                    }
                }
            ]);
            SPA.start({a:1});

        </script>
    </body>

</html>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.