├── .gitignore ├── LICENSE ├── README.md ├── _layouts └── default.html ├── about.md ├── contact.md ├── css └── style.scss ├── index.md └── js └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | _site -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Matias Meno m@tias.me 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stateless-html-example 2 | ====================== 3 | 4 | This is an example repository for a [colorglare.com article](http://www.colorglare.com/2014/11/24/stateless-html.html). 5 | 6 | You can see it in action on [www.colorglare.com/stateless-html-example](http://www.colorglare.com/stateless-html-example). 7 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stateless HTML example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 20 | 21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 | {{ content }} 29 | 30 |
31 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | site: about 4 | --- 5 | 6 | # About 7 | 8 | This page serves as an example, that even links that are in the page downloaded 9 | with AJAX are properly handled by JS, without special implementation. 10 | 11 | [Go back to home](index.html) 12 | (Clicking this link does load the home page with AJAX) -------------------------------------------------------------------------------- /contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | site: contact 4 | --- 5 | 6 | # Contact 7 | 8 | Just send me an email if you want to contact me. 9 | -------------------------------------------------------------------------------- /css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | body { 5 | font-family: "Open Sans", arial, sans-serif; 6 | font-size: 16px; 7 | line-height: 1.5em; 8 | color: #555; 9 | } 10 | 11 | 12 | a { 13 | color: black; 14 | } 15 | 16 | 17 | body > * { 18 | max-width: 600px; 19 | margin: 4rem auto; 20 | } 21 | 22 | h1 { 23 | font-size: 2rem; 24 | font-weight: 200; 25 | } 26 | 27 | footer { 28 | border-top: 1px solid #eee; 29 | color: #ddd; 30 | a { 31 | color: #ddd; 32 | } 33 | margin-top: 6rem; 34 | padding-top: 1rem; 35 | text-align: center; 36 | font-size: 0.875em; 37 | } 38 | 39 | header { 40 | nav { 41 | a { 42 | color: #999; 43 | display: inline-block; 44 | border-radius: 2px; 45 | height: 2em; 46 | line-height: 2em; 47 | padding: 0 1em; 48 | text-decoration: none; 49 | background: #fafafa; 50 | &:hover { 51 | background: #f0f0f0; 52 | } 53 | 54 | &.active { 55 | color: black; 56 | background: #eee; 57 | } 58 | } 59 | } 60 | label { 61 | display: block; 62 | margin-top: 0.6em; 63 | font-size: 0.8em; 64 | } 65 | } 66 | 67 | 68 | hr { 69 | display: block; 70 | border: none; 71 | height: 1px; 72 | background: #eee; 73 | } 74 | 75 | main[aria-busy="true"] { 76 | opacity: 0.4; 77 | } 78 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | site: home 4 | --- 5 | 6 | # Home 7 | 8 | This is the implementation for 9 | [the post on stateless HTML](http://www.colorglare.com/2014/11/24/stateless-html.html). 10 | 11 | 12 | This site is hosted by GitHub, so there is no server logic, only static HTML 13 | pages. When using the main navigation, it loads the page with AJAX, and replaces 14 | the content dynamically. 15 | 16 | * * * 17 | 18 | This page is a proof of concept and only works on modern browsers (IE9+, and 19 | standard Firefox, Chrome and Opera). It would be easy to adapt the code to 20 | work in older browser versions, but I wanted to keep the source code as simple 21 | as possible. -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file gets loaded when the site finished loaded, because it has been 3 | * included with `defer`. 4 | * 5 | * 6 | * * * * * * * 7 | * DISCLAIMER 8 | * * * * * * * 9 | * 10 | * This page is a proof of concept and only works on modern browsers (IE9+, 11 | * and standard Firefox, Chrome and Opera). It would be easy to adapt the 12 | * code to work in older browser versions, but I wanted to keep the source 13 | * code as simple as possible. 14 | */ 15 | 16 | 17 | 18 | /** 19 | * The init() function gets invoked immediately to replace all links to be 20 | * handled by AJAX requests 21 | */ 22 | !function init() { 23 | 24 | var mainElement = document.querySelector("main"); 25 | var fakeSlowConnectionCheckbox = document.getElementById("fake-slow-connection"); 26 | 27 | 28 | convertLinks(); 29 | 30 | 31 | /** 32 | * Converts all links to be handled by JS 33 | */ 34 | function convertLinks(documentRoot) { 35 | if (!documentRoot) documentRoot = document; 36 | 37 | // Get all links 38 | var links = documentRoot.querySelectorAll("a"); 39 | 40 | // Hack to convert the NodeList to an Array... JavaScript :( 41 | Array.prototype.slice.call(links) 42 | .filter(function(link) { 43 | // Remove all links that aren't local. We are only interested in 44 | // relative links. 45 | return link.getAttribute("href").indexOf("http") !== 0; 46 | }) 47 | .forEach(function(link) { 48 | var title = link.innerHTML; 49 | 50 | link.addEventListener("click", function(e) { 51 | 52 | // Make sure that Cmd- or Ctrl-Click still opens the link in a new tab 53 | if (e.metaKey || e.ctrlKey) return; 54 | 55 | // Make sure that the browser doesn't actually follow the link 56 | e.preventDefault(); 57 | 58 | goToPage(link.getAttribute("href"), title); 59 | }); 60 | }); 61 | 62 | } 63 | 64 | 65 | 66 | 67 | 68 | 69 | // Setup the history change event listener to handle when the user clicks the 70 | // back button. 71 | window.onpopstate = function(event) { 72 | var href = event.state.href; 73 | loadPage(href); 74 | }; 75 | 76 | 77 | 78 | 79 | 80 | /** 81 | * Sets the proper history state, and calls loadPage() with the `href` 82 | */ 83 | function goToPage(href, title) { 84 | history.pushState({ href: href }, title, href); 85 | loadPage(href); 86 | } 87 | 88 | 89 | 90 | 91 | var menuLinks = document.querySelectorAll("header nav a"); 92 | 93 | /** 94 | * Actually loads the page from `href` and replaces the #main content with it. 95 | */ 96 | function loadPage(href) { 97 | 98 | console.log("Loading page " + href); 99 | 100 | mainElement.setAttribute("aria-busy", "true"); 101 | 102 | for (var i = 0; i < menuLinks.length; i++) { 103 | var link = menuLinks[i]; 104 | 105 | if (link.getAttribute("href") == href) { 106 | link.classList.add("active"); 107 | } 108 | else { 109 | link.classList.remove("active"); 110 | } 111 | } 112 | 113 | 114 | // Firing off a standard AJAX request 115 | var xmlhttp = new XMLHttpRequest(); 116 | 117 | xmlhttp.onreadystatechange = function() { 118 | if (xmlhttp.readyState == 4) { 119 | if(xmlhttp.status == 200) { 120 | 121 | var fakeDelay = fakeSlowConnectionCheckbox.checked ? 400 : 0; 122 | setTimeout(function() { finishedLoading(xmlhttp.response); }, fakeDelay); 123 | 124 | 125 | } 126 | else { alert("something else other than 200 was returned"); } 127 | } 128 | } 129 | 130 | xmlhttp.open("GET", href, true); 131 | 132 | // Tells the browser to retrieve the response as a HTML document 133 | xmlhttp.responseType = "document"; 134 | 135 | xmlhttp.send(); 136 | 137 | } 138 | 139 | function finishedLoading(responseHtml) { 140 | 141 | mainElement.setAttribute("aria-busy", "false"); 142 | 143 | // Extract the #main div from the response, and update our 144 | // current div. 145 | // This is where the magic happens 146 | mainElement.innerHTML = responseHtml.querySelector("main").innerHTML; 147 | 148 | // Make sure that all links in the newly loaded main div are converted. 149 | convertLinks(mainElement); 150 | 151 | } 152 | 153 | 154 | }(); 155 | 156 | --------------------------------------------------------------------------------