Hi all,
A while ago, I wanted to create a blog to practice English, HTML and write about coding. Something simple was good enough, so I used a Jekyll theme.
I made some changes to the theme and wrote some articles, it was good, but not what I wanted (I didn’t practice HTML at all).
So, I decided to write a very simple blog. I don’t like those “flashes” when I click an article and then other page is opened, something smoother is better and that was the first “feature” that I wanted in my blog.
At first, I thought about using JavaScript, the createElement method:
function addElement () {
// create a new div element
var newDiv = document.createElement("div");
// and give it some content
var newContent = document.createTextNode("Hi there and greetings!");
// add the text node to the newly created div
newDiv.appendChild(newContent);
// add the newly created element and its content into the DOM
var currentDiv = document.getElementById("div1");
document.body.insertBefore(newDiv, currentDiv);
}
But that didn’t look right to me, that seems a good amount of JavaScript per article (to write and maintain). Maybe(?) I can try to automate the process and create a JSON[0] file, and process it with some kind of parser[1], but the initial idea was practice HTML. Also, is not like the articles are some kind of arbitrary data that I will receive, the articles are part of the blog (not something different).
I kept thinking and thought that a good way to avoid the “flashes” is:
write the entire blog including the articles in the same file, and only show the selected article (and hide the other ones). I have a first draft of this idea and I want to show it .
I know the code is ugly, basic and simple, but I also believe that is OK to write something like this because is better than nothing (you need to start somewhere).
I used a statechart[2] because is a personal project and I find the native control flow/structure[3] of JavaScript more difficult to use than a statechart . For the animations I used GreenSock .
About the ugly code and bad names
I will rewrite all the code (at the moment the project is only a draft) and rethink the statechart (states, inputs, etc.).
The customary recommendation when working with a statechart is to design and
then write the code … I wanted to know how difficult can be to explore the problem
and the solution (design) while coding the implementation … and it was harder than
I thought … I ended hacking all my way to the end of the draft. Pen and paper is
faster than a keyboard.
The Diagram
The HTML part
- content
<div class="content" id="content">
<div class="content__header">
<h1 id="recentPostsHeader" data-state="home page">Recent Posts</h1>
</div>
<div class="page__container" data-state="home page">
<<page1>>
<<page2>>
</div>
<<about>>
<<aboutrecursion>>
<<aboutCommentsAndFunctionsignatures>>
<<howToTakeScreenshots>>
<<ELN>>
<<AsciiUnicode>>
</div> <!-- ./content -->
- main
<!doctype html>
<html lang="en">
<<head>>
<body>
<div class="container" id="container">
<<content>>
<<aside>>
<<pagination>>
<<backbtn>>
<footer id="footer" class="footer"></footer>
<<scripts>>
</div>
</body>
</html>
The code above is from the literate document[4] that I use, is easy see the structure of the final HTML.
The interesting part is this:
<h1 id="recentPostsHeader" data-state="home page">Recent Posts</h1>
The data-state= part:
-
Specific HTML elements have a “data-state” attribute
-
The data attribute[5] value of those HTML elements will be one or more ‘states’
-
An HTML element is only displayed (visible) if one of his states matches the current state
Another example:
<div id="about" data-state="about">
<h2>About</h2>
...
</div>
The “more declarative” part, JavaScript code
const aboutAnimation = ctx => {
/* This is setup */
let state = blogService.state.value;
let currentState =
typeof state === "string" ? state : Object.values(state)[0];
let duration = 0.6;
let delay = duration;
/* This is setup */
// this is the interesting part
/* ANIMATIONS */
let hideElements = new Promise((resolve, reject) =>
TweenMax.fromTo(
`[data-state]:not([data-state~=${currentState}])`,
duration,
{ opacity: 1 },
{ opacity: 0, display: "none", onComplete: () => resolve() }
)
);
let showElements = new Promise((resolve, reject) =>
TweenMax.fromTo(
`[data-state~=${currentState}]`,
duration,
{ opacity: 0 },
{
opacity: 1,
display: "inline-block",
delay: delay,
onComplete: () => resolve()
}
)
);
// xstate expects a promise
return Promise.all([hideElements, showElements]);
};
The statechart controls the flow, so, I don’t need to write all the if…else code necessary without it. Basically it says:
-
if not current state => hide
-
if it is current state => display
To open an article I use a list of buttons, like this one:
<ul id="postsList" class="page__list" data-state="home page">
<li class="page__item"><button type="button" class="articleBtn" id="aboutRecursionBtn">About Recursion</button>
</li>
<li class="page_list__item">
<button type="button" class="articleBtn" id="aboutCommentsAndFunctionsignaturesBtn">About comments and function signatures</button>
</li>
<li class="page_list__item">
<button type="button" class="articleBtn" id="howToTakeScreenshotsBtn">How to take screenshots</button>
</li>
<li class="page_list__item">
<button type="button" class="articleBtn" id="ELNBtn">ELN</button>
</li>
<li class="page_list__item">
<button type="button" class="articleBtn" id="AsciiUnicodeBtn">ASCII UNICODE</button>
</li>
</ul>
The list is time consuming to write and because I used really long names in camelCase, rewriting it in the JavaScript side of the project was something I didn’t wanted, so I used querySelectorAll to add the event listener that will send the input to the statechart:
/* ARTICLES BUTTONS and LISTENERS*/
document.querySelectorAll(".articleBtn").forEach(elem => {
document.getElementById(elem.id).addEventListener("click", () => {
let articleId = elem.id.replace(/Btn/gi, "");
blogService.send({ type: "READARTICLE", currentArticle: articleId });
});
});
logs
I can get some info about how the blog is running:
const blogService = interpret(blog).onTransition(state => {
console.log("current state: ", state.value);
console.log("page Number: ", state.context.pageNumber);
console.log("current Article: ", state.context.currentArticle);
console.log("--------------------------------------------");
});
Manual testing
I recorded some videos to show how it works:
- Loading page first time:
- Click “Previous” button:
- Click “Next” button:
- Click “1” button:
- Click “2” button:
- Click “1” button, Click “2” button:
- Click “About Recursion”:
- Click “ELN”:
- Click “About”:
- Demo:
Automated testing
After a while I realize that the manual testing was time consuming, so I wrote a
function to do the work for me:
/*
AUTOMATIC TESTING
*/
// I need to test this
var testingCaseIndex = -1;
var automaticTestingID;
var automaticTesting = () =>(automaticTestingID = window.setInterval(callback, 2800));
function callback() {
let testEvent = testingCase[(testingCaseIndex += 1)];
console.log("PERFORMING AUTOMATIC TESTING");
console.log("sending test event ", testEvent);
blogService.send(testEvent);
}
const testingCase = [
{ type: "PREVIOUS" },
{ type: "NEXT" },
{ type: "NUMBERED", pageNumber: 1 },
{ type: "NUMBERED", pageNumber: 2 },
{ type: "NUMBERED", pageNumber: 1 },
{ type: "NUMBERED", pageNumber: 2 },
{ type: "NUMBERED", pageNumber: 1 },
{ type: "READARTICLE", currentArticle: "ELN" },
{ type: "BACK" },
{ type: "ABOUT" },
{ type: "BACK" },
{ type: "STOPTESTING" }
];
function stopTesting() {
console.log("stopTesting fn called");
clearInterval(automaticTestingID);
testingCaseIndex = -1;
console.log("ENDING AUTOMATIC TESTING");
console.log("--------------------------------------------");
}
I can start the testing calling the function automaticTesting, to stop the testing I need to add to the testingCase array an the object “STOPTESTING” (maybe I can do it another way, but was easy to write and good enough for now)
That is all for now.
Cheers and happy coding
Footnotes:
[0] JSON - Wikipedia
[3] https://developer.mozilla.org/en-/docs/Web/JavaScript/Guide/Control_flow_and_error_handling