Skip to content

Instantly share code, notes, and snippets.

@SethVandebrooke
Last active August 17, 2018 17:32
Show Gist options
  • Save SethVandebrooke/a10e354a9602739c2b9d92a461b9a218 to your computer and use it in GitHub Desktop.
Save SethVandebrooke/a10e354a9602739c2b9d92a461b9a218 to your computer and use it in GitHub Desktop.
SSPAF: Simple SPA functionality

SSPAF

Simple Single Page Application Functionality

Navigating between pages is easy:

<app-page class="default-page" id="home">
  <h1>Home page</h1>
  <a data-goto="new">go to new page</a>
</app-page>

<app-page id="new">
  <h1>New page</h1>
  <a data-goto="home">go to home page</a>
</app-page>

Everything is fully configurable.

<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<title>SPA</title>
<style>
app-page {
position: fixed;
top: 0px;
left: 0px;
padding-top: 35px;
padding-bottom: 10px;
width: 100%;
height: 99%;
left: 100%;
transition: .5s;
margin-top: 25px;
}
.container {
height: inherit;
overflow: scroll;
}
</style>
</head>
<body>
<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#" bind="currentPage:innerHTML">App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#" data-goto="home">Home </a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-goto="notes">Notes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-goto="account">My Account</a>
</li>
</ul>
</div>
</nav>
<app-page class="default-page" id="home">
<div class="container">
<h1>Welcome</h1>
<button type="button" class="btn btn-primary" data-goto="notes">Open Notes</button>
</div>
</app-page>
<app-page id="notes">
<button type="button" class="btn btn-primary rounded-circle"
style="position:fixed; bottom:10px; right:10px; height:65px;width:65px; z-index:100; box-shadow: 0px 0px 10px #aaa;"
data-toggle="modal" data-target="#newNote" id="viewNote" onclick="clearNote()">
New
</button>
<div class="container">
<div bind="notes:innerHTML">
<div class="card mb-2 mx-auto" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">${title}</h5>
<p class="card-text text-left">${body}</p>
<a href="#" class="card-link" onclick="openNote(${index})">Edit</a>
<a href="#" class="card-link" onclick="deleteNote(${index})">Delete</a>
</div>
</div>
</div>
</div>
</app-page>
<app-page id="account">
<div class="container">
<img src="https://api.adorable.io/avatars/285/[email protected]" class="rounded mx-auto d-block" alt="Profile Picture">
<p bind="user.Name:innerHTML"></p>
<p bind="user.Username:innerHTML"></p>
<p bind="user.Email:innerHTML"></p>
</div>
</app-page>
<!-- Modal -->
<div class="modal fade" id="newNote" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<input type="text" class="form-control" placeholder="Enter Title"
bind="note.title:value,note.title-on:change">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Password"
bind="note.body:value,note.body-on:change"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal" onclick="saveNote()">Save</button>
</div>
</div>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
<script src="SPA.js"></script>
<script>
var app = new SPA({
user: {
Name: "John Doe",
Username: "J_Dog",
Email: "[email protected]"
},
note: {
id: "null",
title:"",
body: ""
}
});
app.scope.currentPage.observe(function (page){
if (page == "notes") {
document.getElementById("viewNote").style.display = "block";
} else {
document.getElementById("viewNote").style.display = "none";
}
});
app.scope.currentPage.changed();
function openNote(id) {
var note = app.scope.notes()[id];
document.getElementById("viewNote").click();
app.scope.note.id(note.id);
app.scope.note.title(note.title);
app.scope.note.body(note.body);
}
function clearNote() {
app.scope.note.id("null");
app.scope.note.title("");
app.scope.note.body("This is a bunch of useless content. Pay no attention to the text in front of you. This is not the text you are looking for.");
}
function saveNote() {
var NOTE = app.scope.note(); //get the current note
var ID = NOTE.id; // get the id
if (ID != "null") { // If it is an existing note
app.scope.notes.splice(ID, 1, NOTE); // update it
} else { // otherwise
NOTE.id = app.scope.notes.length;
app.scope.notes.push(NOTE); // add it
}
// clear out current note
clearNote();
}
function deleteNote(id) {
app.scope.notes.splice(id,1);
}
</script>
</body>
</html>
var Wieldable = function(originalValue){
var data, callbacks = [];
function broadcast() {
for (var i = 0; i < callbacks.length; i++) {
if ("function" === typeof callbacks[i]) {
callbacks[i](data);
}
}
}
// (any)
// v: any value you want to wield
var wieldable = function (value) {
if (value != undefined) {
data = value;
var methods = ["push","pop","shift","unshift","splice","slice"];
if (value instanceof Array) {
function methodWrapper(func, ...params) {
var result = data[func](...params);
broadcast();
return result;
}
methods.forEach(function(method){
wieldable[method] = function ( ...params ) { return methodWrapper( method, ...params ); };
});
Object.defineProperty(wieldable, 'length', { get: function() { return data.length; } } );
} else if ("object" === typeof value) {
for (let key in value) {
wieldable[key] = new Wieldable(value[key]);
}
} else { // Clear out helper functions if there
methods.forEach(function(method){
if (wieldable[method]) { delete wieldable[method]; }
});
}
broadcast();
}
if (data instanceof Array) {
var tempArray = [];
for (var i = 0, callbacks = data.length; i < callbacks; i++) {
tempArray[i] = data[i];
}
return tempArray;
} else if ("object" === typeof data) {
var obj = {};
for (var key in wieldable) {
if ("function" === typeof wieldable[key] && "wieldable" === wieldable[key].name) {
obj[key] = wieldable[key]();
}
}
return obj;
}
return data;
};
// (function)
// f: function to run when the value of the wieldable changes
wieldable.observe = function (func) {
if ("function" === typeof func) {
callbacks.push(func);
return {
stop: function () {
callbacks.splice(callbacks.indexOf(func),1)
},
start: function () { callbacks.push(func) }
};
}
};
// (DOM Element, String, Boolean)
// element: DOM Element to bind the wieldable to
// param: Name of event (if input is true) to update on, or DOM Element property to set when updated
// input: whether you are setting the wieldable on an event or getting it when changed
wieldable.bind = function (element,param,input) {
if (input) {
element.addEventListener(param, function () {
wieldable(element.value);
});
if (element.value) wieldable(element.value);
return wieldable.observe(function(text) {
element.value = text;
});
} else {
wieldable.template = element[param];
var observer = wieldable.observe(function (text) {
if (data instanceof Array && wieldable.template) {
element[param] = "";
data.forEach((el,index) => {
var str = wieldable.template;
if ("object" === typeof el) {
for (var key in el) {
str = str.split("${"+key+"}").join(el[key]);
}
} else {
str = str.split("${value}").join(el);
}
str = str.split("${index}").join(index);
element[param] += str;
})
} else {
element[param] = text;
}
});
element[param] = data;
return observer;
}
}
wieldable.changed = broadcast;
wieldable(originalValue);
return wieldable;
};
// (DOM Element, Object)
// tar: target element to bind the scope to
// model: an object of wieldable values inside the bound scope
function WieldyScope(tar,model) {
var scope = this;
scope.debug = false;
scope.initTarget = tar;
scope.run = function (target) {
var html = target.innerHTML;
var temp = html.match(/\{\{.*\}\}/);
if (temp !== null) {
temp.forEach(function(item){
var e = item.replace("{{","").replace("}}","");
html = html.replace(item,eval(e));
});
target.innerHTML = html;
}
target.querySelectorAll("[bind]").forEach(e=> {
var bindings = e.getAttribute("bind"), input = false;
if (!!bindings) {
bindings = bindings.split(",");
bindings.forEach(function(params){
if (params.match(":")!==null) {
params = params.split(":");
var binding = params[0];
var param = params[1];
if (!!binding && !!param) {
if (binding.match("-on")!==null) {
binding = binding.replace("-on","");
input = true;
}
if (binding.replace(".","")!=binding) {
binding = eval("scope."+binding);
if (binding === undefined) {
console.error(binding+" does note exist");
}
} else {
binding = scope[binding] = scope[binding] || new Wieldable();
}
if (input) {
binding.bind(e,param,true);
scope.debug?console.log(e,param, true):null;
} else {
if (!binding()) {
binding(!!e[param]?e[param]:"");
}
binding.bind(e,param);
if (e[param].match(/\$\{.*}/g) != null) {
binding([]);
binding.observe(function(data){
scope.run(e);
})
}
scope.debug?console.log(e,param):null;
return scope;
}
} else {
console.error("No bindings were defined");
return false;
}
} else {
console.error("No bindings were defined");
return false;
}
})
} else {
console.error("No bindings were defined");
return false;
}
});
};
if (model) {
for (var k in model) {
scope[k] = new Wieldable(model[k]);
}
}
if (tar) {
scope.run(tar);
}
}
function SPA(wieldables,{
pageSelector = "app-page",
defaultPageSelector = ".default-page",
pageLinkSelector = "[data-goto]",
gotoAttribute = "data-goto",
gotoEventAttribute = "data-goto-on"
} = {}) {
var spa = this;
wieldables = wieldables || {};
wieldables.currentPage = "";
spa.scope = new WieldyScope(document.body, wieldables);
function hideElement(element) {
element.style = "left: 100%;";
}
function showElement(element) {
element.style = "left: 0%;";
}
function gotoPage(id) {
if (document.getElementById(id)) {
document.querySelectorAll(pageSelector).forEach(function(page){
hideElement(page);
});
showElement(document.getElementById(id));
}
}
document.querySelectorAll(pageLinkSelector).forEach(function (pageLink) {
let customEvent = pageLink.getAttribute(gotoEventAttribute);
pageLink.addEventListener(customEvent || "click", function (ev) {
let pageId = pageLink.getAttribute(gotoAttribute);
spa.scope.currentPage(pageId);
gotoPage(pageId);
});
});
var defaultPage = document.querySelector(defaultPageSelector);
if (defaultPage) {
gotoPage(defaultPage.id);
}
spa.goto = gotoPage;
}
function SPA({
pageSelector = "app-page",
defaultPageSelector = ".default-page",
pageLinkSelector = "[data-goto]",
gotoAttribute = "data-goto",
gotoEventAttribute = "data-goto-on"
} = {}) {
var spa = this;
function hideElement(element) {
element.setAttribute("hidden","");
}
function showElement(element) {
element.removeAttribute("hidden");
}
function gotoPage(id) {
if (document.getElementById(id)) {
document.querySelectorAll(pageSelector).forEach(function(page){
hideElement(page);
});
showElement(document.getElementById(id));
}
}
document.querySelectorAll(pageLinkSelector).forEach(function (pageLink) {
let customEvent = pageLink.getAttribute(gotoEventAttribute);
pageLink.addEventListener(customEvent || "click", function (ev) {
let pageId = pageLink.getAttribute(gotoAttribute);
gotoPage(pageId);
});
});
var defaultPage = document.querySelector(defaultPageSelector);
if (defaultPage) {
gotoPage(defaultPage.id);
}
}
new SPA();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment