I'm trying to create a website builder (drag and drop page builder) and was wondering where to store the styles when someone changes the styles of an element. For example, in WordPress you can type in your own custom CSS in Customizer (image example: https://i.imgur.com/qaUiVl6.png)
In other page builders like Wix or Google Chrome Inspect Element, you can click button to enable or disable styles.
While making current/live CSS edits to the page, where and how are these styles saved? (I'm not talking about a database as the code has not been saved yet. I'm talking about while making changes onsite changes, where do these "temporary/live" CSS styles get saved?)
You can use the CSSStyleSheet APIs to generate a stylesheet in memory then use insert and delete methods to add or remove rules from the stylesheet at will. When the user is done modifying you could then pass the generated stylesheet back server side to save perm.
Ref docs can be found here:
https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#Methods
Compatability is IE9+ and all other modern browsers so it has good coverage.
Quick and dirty example below.
var style = (function() {
// Create the <style> tag
var style = document.createElement("style");
// Add the <style> element to the page
document.head.appendChild(style);
return style;
})();
function AddRule(){
//append rule from textbox to ss here
style.sheet.insertRule(document.getElementById("cssIn").value, 0);
document.getElementById("appliedRules").innerHTML = '';
var rules = style.sheet.cssRules;
for (var r in rules) {
if(rules[r].cssText){
document.getElementById("appliedRules").innerHTML += '<br>' + rules[r].cssText;
}
}
}
//enable this to see your special prize in the console
//console.log(style.sheet);
<div class="test"> here we go</div>
Add Rule: <input type="text" id="cssIn" value=".test {color:blue}">
<button type="button" onClick="AddRule();">Add</button>
<div id="appliedRules"></div>
Here is a simple proof-of-concept that demonstrates how this can be done using pure javascript. Just click the save button to see the CSS in the textarea get applied to the page. The CSS is just stored as the input value of the textarea element. You can also make it more complex by using localStorage and an iframe or shadow dom so you only affect a "preview" pane. But this is just a demonstration.
function saveStyles() {
document.querySelector('#style-container').innerHTML = document.querySelector('#style-input').value;
}
#style-input {
width: 100%;
box-sizing: border-box;
display: block;
margin-bottom: 8px;
}
<style id="style-container"></style>
<textarea id="style-input" rows="5">body{background:red;}</textarea>
<button onclick="saveStyles()">Save</button>
Here's an alternative that puts the stylesheet into memory and loads it via a blob URL.
This behaves a bit more like a real stylesheet than inline styles do in some edge cases, which may be desirable in some cases. It can also work on a webpage that blocks inline styles via a Content Security Policy (provided blob URL's are allowed).
(function() {
var styles = document.getElementById('styles');
var save = document.getElementById('save');
var link = null;
function getLink() {
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
document.head.appendChild(link);
}
return link;
}
save.addEventListener('click', function() {
var link = getLink();
if (link.href) {
URL.revokeObjectURL(link.href);
}
link.href = URL.createObjectURL(new Blob([styles.value], {type: 'text/css'}));
});
})();
#styles {
display: block;
width: 95%;
}
<textarea id="styles" rows="5">body {
background: green;
}
</textarea>
<button id="save">Save</button>
The answers here focus on the methods for building a stylesheet and adding css rules using common methods browsers provide as part of the DOM api. These are the underlying function calls that any UI framework on the web will use.
But when you ask, "Where is this stored?". In a sense, you are asking how is the "state" managed. If you look at original post jQuery/web app building frameworks, like Backbone.js -- its motto was, "Get your model out of the DOM". So generally the "elements" of the ui-building tools will themselves be represented as component/models.
If you look at view-centric frameworks, like React or Vue, more complex web apps will use a framework like Redux to handle "state", which is stored in single object store. This represents the current options of all the components on the page.
So ultimately, a non-toy WYSIWYG web editor will likely treat each "element" as a component that renders its styles based on an inputted state.
This coupled with a controlled and predictable way to change state, allows for the managing of complex UI. For instance, the click method would trigger an action that acts like the name of an event handler, triggering the functions (in redux world, reducers), which ultimately changes the state, in our example, color of the element.
The low-level calls to the DOM that facilitate this in a complex web-app/web-editor would be abstracted away.
Based on the discussion, I can suggest to use separate styles for each element id. Here is a sketch.
<script>
function setStyle(id, style_text)
{
var style_id = "style_" + id;
var style_forId = "#" + id + " " + style_text;
var styleDom = document.getElementById(style_id);
if(!styleDom)
{
styleDom = document.createElement('style');
styleDom.type = 'text/css';
styleDom.id = style_id;
styleDom.innerHTML = style_forId;
document.getElementsByTagName("head")[0].appendChild(styleDom);
}
else
{
styleDom.innerHTML = style_forId;
}
}
</script>
<button id="myButton1" type="button" >My Button 1</button>
<button id="myButton2" type="button" >My Button 2</button>
<br>
<button onclick="setStyle('myButton1', '{color:red}'); "> Set Red color for myButton1 </button>
<br>
<button onclick="setStyle('myButton2', '{color:red}'); "> Set Red color for myButton2 </button>
<br>
<button onclick="setStyle('myButton1', '{color:green}'); "> Set Green color for myButton1 </button>
<br>
<button onclick="setStyle('myButton2', '{color:green}'); "> Set Green color for myButton2 </button>
<br>
Great Answers already just putting a different viewpoint out there.
Simple Version
Using CSSStyleSheet
const style = document.createElement("style");
document.head.appendChild(style);
style.sheet.insertRule(`
header {
background: 'black'
}
`, 0);
Real World
I would take this simple idea and control it through a data structure like this.
// A JS Object to control and modify CSS styles on.
const css = {
header: {
background: 'black',
border: '2px green solid',
'font-size': '12px'
}
}
// Converts JS Object to CSS Text (This is not battle tested)
function objToCss(style) {
return Object.entries(style).reduce((styleString, [propName, propValue]) => {
propName = propName.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
if (typeof propValue === 'object') {
return `${styleString}${propName}{${objToCss(propValue)}}`;
} else {
return `${styleString}${propName}:${propValue};`;
}
}, '')
}
// Setup
const style = document.createElement("style");
document.head.appendChild(style);
style.sheet.insertRule(objToCss(css), 0);
// Updates
css.header.border = '1px blue solid';
style.sheet.replace(objToCss(css), 0);
Related
To start, I am working with react-swipe-to-delete-component, but I feel that should make little difference in the question.
I am trying to change the background-color property of a "built out" element. I have no control over the class name as the plugin uses it, and I cannot assign it (I think) a separate class name as it stands. Is there a way to change the style of a element solely based on selector, like in vanilla JavaScript?
I have this element <SwipeToDelete>. It builds out a div that I need to access by the selector:
<div class="swipe-to-delete">
<div class="js-delete">
And thus I need to be able to access
.swipe-to-delete .js-delete
as a selector.
Is there a way like in JS IE document.getElementsByClassName('.swipe-to-delete .js-delete')[0].style.background-color = "#FFF")
I need to change the background color based on which function is called:
const [isRight, setRight] = useState(false);
const [isLeft, setLeft] = useState(false);
const onLeft = (...args) => {
// Make Mask Red with Icon
console.log('Set left to true');
setLeft(true);
}
const onRight = (...args) => {
// Make Mask Green with icon
console.log('Set right to true');
setRight(true);
}
<SwipeToDelete
onLeft={onLeft}
onRight={onRight}
>
This works.. I just don't know how to incorporate this pseudo statement
isRight ? '.swipe-to-delete .js-delete'.backgroundColor="green" : ''
Since I can't access <SwipeToDelete>'s inner workings to set a class name somewhere, without modifying the node-module code. I really am stuck here.
I just want to simply change the background color of .swipe-to-delete .js-delete depending on right or left function. Am I looking at this the wrong way?
There is a classNameTag prop you can use to define a className on the wrapper element for <SwipeToDelete> (the element with className="swipe-to-delete").
Based on the swipe direction you set the className accordingly:
<SwipeToDelete
classNameTag={isRight ? "is-right" : isLeft ? "is-left" : ""}
onLeft={onLeft}
onRight={onRight}
>
And styling with CSS:
.swipe-to-delete.is-right .js-delete {
background-color: green;
}
.swipe-to-delete.is-left .js-delete {
background-color: red;
}
I have dynamic list of 'posts' where I wanted to truncate the text (if it goes beyound a certain # of lines) and show a Read More button that users can click to show the entire text.
In VueJS, I decided to attach a ref to the div I want to append the button to (if the text is truncated).
The component is just a button really but it has some stylings and behaviors I want to copy over. The reason why this got more complicated then it needs to (bad thing?) is because I'm doing the truncating with CSS. I understand that using Javascript might have been easier.
So anyways, how can I dynamically add a component to this div (or its parent) using Javascript only? My own reference to the location would be the ref item.
// code after the promise of getting the posts has resolved in the created() hook
.then(() => {
const posts = this.$refs.posts
posts.forEach(p => {
if (this.Overflown(f)) {
// I want to attach a component (AwesomeButtonComponent) to this p div.
}
}
})
And for clarity:
HTML:
<div v-for="post in posts">
<div class="postBody ref="posts">{{ post.body }}</div>
</div>
isOverflown(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
The CSS that is truncating the text
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
ALTERNATIVE POSSIBILITIES:
This button will only have ONE functionality, so it being a component is not important and adding styling isn't so difficult.
.then(() => {
const announcementBodies = this.$refs.announcementBody;
announcementBodies.forEach(a => {
if (this.isOverflown(a)) {
const button = document.createElement('button');
button.innerText = 'Click Me';
button.onClick = 'doThis';
a.parentElement.appendChild(button);
}
});
In which case the difficult part would be to add a v-on:click directive to that button and then target that specific tag to remove the clamp css attribute.
Following our discussion into comments, I'll show you 2 way you can do this and try to explain the difference between them and let you decide how you will achieve this.
EXAMPLE ONE
The first example is the shortest I could do. This will need every post to have an isOverflow attribute. There is many way to do it client or server side. The other example will not need it.
<div v-for="post in posts">
{{(post.isOverflow == true) ? post.body.substring(0,3)+'...' : post.body}} <button v-on:click="post.isOverflow = !post.isOverflow">{{(post.isOverflow == true) ? 'SHOW MORE' : 'SHOW LESS'}}</button>
</div>
This is not beautiful, but it work and it let you understand that you can manipulate the post inside the v-for. Each button will be automatically associate with the right post, so when you will click it, only the post associated will be affected.
EXAMPLE TWO
The other example i'll give you is by creating a new component for each post. Let's start with the v-for:
<post-component v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></post-component>
And the new component:
<template>
<div v-bind:class="{'postsBody': isOverflow}">
{{post.body}}
<button v-on:click="changeState()">{{(post.isOverflow) ? 'SHOW LESS' : 'SHOW MORE'}}</button>
</div>
</template>
<script>
export default {
props: {
post:{}
},
data() {
return {
isOverflow: true
}
},
methods: {
changeState: function() {
this.isOverflow = !this.isOverflow;
}
},
}
</script>
<style> //Please, put this in a CSS file, it's only for the example purpose.
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
CONCLUSION
In the end, both of them will have the same result. The difference is what you prefer. I tried to show you two different way to let you understand how things work with Vue. Let me know if you need more explanations.
I create directed graphs like the following from wikidata with the help of networkx and nxv. The result is an svg file which might be embedded in some html page.
Now I want that every node and every edge is "clickable", such that a user can add their comments to specific elements of the graph. I think this could be done with a modal dialog popping up. This dialog should know from which element it was triggered and it should send the content of the textarea to some url via a post request.
What would be the best way to achieve this?
Wrapped in a W3C standard Web Component (supported in all Modern Browsers) you can make it generic for any src="filename.svg"
Simple example: How to get SVG document data to be inserted into the DOM?
More complex example:
<graphviz-svg-annotator src="https://graphviz.org/Gallery/directed/fsm.svg">
</graphviz-svg-annotator>
The SVG is loaded with an async fetch
Nodes and Edges are clickable in this SO Snippet
add your own, better modal, window and saving to database
Try the SVGs from: https://graphviz.org/Gallery/directed/Genetic_Programming.html
<graphviz-svg-annotator src="fsm.svg"></graphviz-svg-annotator>
<graphviz-svg-annotator src="Linux_kernel_diagram.svg"></graphviz-svg-annotator>
<style>
svg .annotate { cursor:pointer }
</style>
<script>
customElements.define('graphviz-svg-annotator', class extends HTMLElement {
constructor() {
let loadSVG = async ( src , container = this.shadowRoot ) => {
container.innerHTML = `<style>:host { display:inline-block }
::slotted(svg) { width:100%;height:200px }
</style>
<slot name="svgonly">Loading ${src}</slot>`;
this.innerHTML = await(await fetch(src)).text(); // load full XML in lightDOM
let svg = this.querySelector("svg");
svg.slot = "svgonly"; // show only SVG part in shadowDOM slot
svg.querySelectorAll('g[id*="node"],g[id*="edge"]').forEach(g => {
let label = g.querySelector("text")?.innerHTML || "No label";
let shapes = g.querySelectorAll("*:not(title):not(text)");
let fill = (color = "none") => shapes.forEach(x => x.style.fill = color);
let prompt = "Please annotate: ID: " + g.id + " label: " + label;
g.classList.add("annotate");
g.onmouseenter = evt => fill("lightgreen");
g.onmouseleave = evt => fill();
g.onclick = evt => g.setAttribute("annotation", window.prompt(prompt));
})
}
super().attachShadow({ mode: 'open' });
loadSVG("//graphviz.org/Gallery/directed/"+this.getAttribute("src"));
}});
</script>
Detailed:
this.innerHTML = ... injects the full XML in the component ligthDOM
(because the element has shadowDOM, the lightDOM is not visible in the Browser)
But you only want the SVG part (graphviz XML has too much data)... and you don't want a screen flash; that is why I put the XML .. invisible.. in lightDOM
A shadowDOM <slot> is used to only reflect the <svg>
with this method the <svg> can still be styled from global CSS (see cursor:pointer)
With multiple SVGs on screen <g> ID values could conflict.
The complete SVG can be moved to shadowDOM with:
let svg = container.appendChild( this.querySelector("svg") );
But then you can't style the SVG with global CSS any more, because global CSS can't style shadowDOM
As far as I know, nxv generates a g element with class "node" for each node, all nested inside a graph g. So basically you could loop over all gs elements inside the main group and attach a click event listener on each one. (actually, depending of the desired behavior, you might want to attach the event listener to the shape inside the g, as done below. For the inside of the shape to be clickable, it has to be filled)
On click, it would update a form, to do several things: update its style to show it as a modal (when submitted, the form should go back to hiding), and update an hidden input with the text content of the clicked g.
Basically it would be something like that:
<svg>Your nxv output goes here</svg>
<form style="display: none;">
<input type="hidden" id="node_title">
<textarea></textarea>
<input type="submit" value="Send!">
</form>
<script>
const graph = document.querySelector("svg g");
const form = document.querySelector("form");
[...graph.querySelectorAll("g")].map(g => { //loop over each g element inside graph
if (g.getAttribute("class") == "node") { //filter for nodes
let target = "polygon";
if (g.querySelector("polygon") === null) {
target = "ellipse";
}
g.querySelector(target).addEventListener("click",() => {
const node_title = g.querySelector("text").innerHTML;
form.querySelector("#node_title").setAttribute("value", node_title);
form.setAttribute("style","display: block;");
});
}
});
const submitForm = async (e) => { //function for handling form submission
const endpoint = "path to your POST endpoint";
const body = {
source_node: form.querySelector("#node_title").value,
textarea: form.querySelector("textarea").value
}
e.preventDefault(); //prevent the default form submission behavior
let response = await fetch(endpoint, { method: "POST", body: JSON.stringify(body) });
// you might wanna do something with the server response
// if everything went ok, let's hide this form again & reset it
form.querySelector("#node_title").value = "";
form.querySelector("textarea").value = "";
form.setAttribute("style","display: none;");
}
form.addEventListener("submit",submitForm);
</script>
I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>
I've been trying to implement History.js. I've got some understanding of how getting and pushing states work, however I'm having particular trouble with the data storing component of the history along with using global variables.
As a simple example, I decided to try and set up a script which would change the colour of a html box upon being clicked. This would also trigger the history - essentially creating a history for clicking the box (and its colour being changed on each state of the history).
Is there any way to update a global variable based on the data (in this case, updating i per click) supplied in History State's data?
HTML:
<div id="box">CLICK ME</div>
<button id="back">Back</button>
<button id="forward">Forward</button>
CSS:
#box {
width: 300px;
height: 300px;
background-color: black;
color: white;
vertical-align: middle;
text-align: center;
display: table-cell;
}
JavaScript:
var History = window.History;
var i = 0;
if (History.enabled) {
var State = History.getState();
History.pushState({count:i}, $("title").text(), State.urlPath);
} else {
return false;
}
// Bind to StateChange Event
History.Adapter.bind(window,'statechange', function(){
State = History.getState();
console.log(State.data, State.title, State.url);
$(this).css('background-color', getColour());
});
// Trigger the change
$("#div").on("click", function() {
i++;
History.pushState({count:i},"State " + i,"?state=" + i);
});
function getColour() {
var colours = ["red", "orange", "yellow", "green", "aqua","blue", "purple", "magenta","black"];
if (i > colours.length - 1) {
i = 0;
}
if (i < 0) {
i = colours.length - 1;
}
return colours[i];
}
$("#back").on("click", function() {
History.back();
});
$("#forward").on("click", function() {
History.forward();
});
I'm also using JQuery, ajaxify-html5.js and scrollto.js as per recommendation by other threads.
Editable JSFiddle | Viewable JSFiddle
After playing around with this a ton (and reading more questions), I've figured it out. I'll detail what the solution means to any others who come across this.
JSFIDDLE VIEW SOLUTION | JSFIDDLE VIEW SOLUTION
First here's the final code. Note that the JavaScript has document.ready extras to get it working outside of JSFiddle.
It's also worth noting I took out ajaxify-html5.js and scrollto.js out, as they weren't needed (and were breaking the code somewhere).
HTML:
<div id="box">
<div id="count"></div>
<div id="colour"></div>
</div>
<button id="back">Back</button>
<button id="forward">Forward</button>
CSS:
#box {
width: 300px;
height: 300px;
background-color: white;
color: white;
vertical-align: middle;
text-align: center;
display: table-cell;
}
button {
width: 148px;
height: 40px;
}
#count, #colour {
background-color: black;
font-family: "Consolas";
}
JavaScript:
var History = window.History;
var i = 0;
var colour = getColour();
var colourName = getColourName();
$(document).ready(function() {
if (History.enabled) {
changeHistory();
}
else {
return false;
}
// Bind to StateChange Event
History.Adapter.bind(window,'statechange', function(){
State = History.getState();
i = State.data.count;
colour = State.data.colour;
colourName = State.data.colourName;
changeHistory();
});
// Trigger the change
$(document).on("click", "#box", function() {
i = i + 1;
colour = getColour();
colourName = getColourName();
changeHistory();
});
$(document).on("click", "#back", function() {
History.back();
});
$(document).on("click", "#forward", function() {
History.forward();
});
});
function getColour() {
var colours = ["rgb(220,45,45)", "orange", "rgb(230,230,50)", "rgb(15,210,80)", "rgb(100,220,220)","rgb(50,80,210)", "rgb(140,20,180)", "rgb(230,70,110)","grey"];
if (i > colours.length - 1) {
i = 0;
}
if (i < 0) {
i = colours.length - 1;
}
return colours[i];
}
function getColourName() {
var colourNames = ["Red","Orange","Yellow","Green","Light Blue","Blue","Purle","Pink","Grey"];
return colourNames[i];
}
// Make the changes
function changeHistory () {
$("#colour").html(colourName);
$("#count").html(i);
$("#box").css('background-color', colour);
History.pushState({count:i, colour: colour, colourName: colourName},"A Shade of " + colourName,"?colour=" + colourName);
}
So going back to what I wanted to achieve with the question:
Clicking the box would add history
Each history would hold variables required to affect global variables
Its worth noting the solution specifically uses variables from each iteration of the history to power the global variables, whereas the program itself uses the global variables. The variables used to power the interface never access the ones stored in history.
Let's break up the program into separate and simpler processes and functions. Much like other history.js solutions, there's things you require to get it working:
History.getState(): Gets the latest history item "from the stack"
History.Adapter.bind(window,'statechange', function() {}: An event listener which will trigger a function when the window has a statechange (history change in this case)
History.pushState({data}, title, url): Pushes a state into the "history stack". Holds an object (data), title (of the tab/window) and url to display.
Setting the history:
When a user clicks on the box, the program should:
increment the counter (i)
change the colour and colourName
add the new history stack object in
I decided to separate the first two features from the last one. The function changeHistory() is responsible for updating the contents of the box (from global variables) and pushing a new history object in (using global variables).
changeHistory() gets called whenever I want to add a new item of history in and update the contents in the box to reflect the new history - so at launch at when the box is clicked.
When the box is clicked, the first two criteria get met. Using the existing global variables and functions, the new colour and name are retrieved and set as the global variables.
This is how it should behave:
Box Click -> Increment i, Change variables -> Push History
Listen for the history change:
Once a history change has been made (either by clicking the box, pressing back/forward buttons or browser buttons), a change needs to occur.
By creating the variable State = History.getState(), we have an easy way of accessing the data from the latest history stack object.
We'll use this data from the history object.data to assign to the global variables. After updating the variables, we'll update the view using changeHistory().
This is how the model should work:
History changed -> Update globals from history -> Update View
History change will occur whenever someone presses back, forwards or the box, accounting for all possible changes.