Refactoring a video player using reveal module and command pattern in JS
• • 11 min readToday, I'm going to show a basic code in which a video player is done using a few good practices to develop code in the present days in the JavaScript language. My intention is show step to step the process from the original code to the finish code using several classic patterns such as Reveal Module and Command.
So, the initial code is the following:
player.html
<!DOCTYPE html>
<html lang="es">
<head>
<title>Reproductor de Video</title>
<link rel="stylesheet" href="player.css">
<script src=" player.js"></script>
</head>
<body>
<section id="reproductor">
<video id="medio" width="720" height="400">
<source src="http://minkbooks.com/content/trailer.mp4">
<source src="http://minkbooks.com/content/trailer.ogg">
<video>
<nav>
<div id="botones">
<button type="button" id="reproducir">Reproducir</button>
</div>
<div id="barra">
<div id="progreso"></div>
</div>
<div style="clear: both"></div>
</nav>
</section>
</body>
</html>
player.js
function iniciar() {
maximo=600;
medio=document.getElementById('medio');
reproducir=document.getElementById('reproducir');
barra=document.getElementById('barra');
progreso=document.getElementById('progreso');
reproducir.addEventListener('click', presionar, false);
barra.addEventListener('click', mover, false);
}
function presionar(){
if(!medio.paused && !medio.ended) {
medio.pause();
reproducir.innerHTML='Reproducir';
window.clearInterval(bucle);
}else{
medio.play();
reproducir.innerHTML='Pausa';
bucle=setInterval(estado, 1000);
}
}
function estado(){
if(!medio.ended){
var total=parseInt(medio.currentTime*maximo/medio.duration);
progreso.style.width=total+'px';
}else{
progreso.style.width='0px';
reproducir.innerHTML='Reproducir';
window.clearInterval(bucle);
}
}
function mover(e){
if(!medio.paused && !medio.ended){
var ratonX=e.pageX-barra.offsetLeft;
var nuevoTiempo=ratonX*medio.duration/maximo;
medio.currentTime=nuevoTiempo;
progreso.style.width=ratonX+'px';
}
}
window.addEventListener('load', iniciar, false);
player.css
body{
text-align: center;
}
header, section, footer, aside, nav, article, figure, figcaption,
hgroup{
display : block;
}
#reproductor{
width: 720px;
margin: 20px auto;
padding: 5px;background: #999999;
border: 1px solid #666666;
border-radius: 5px;
}
nav{
margin: 5px 0px;
}
#botones{
float: left;
width: 100px;
height: 20px;
}
#barra{
position: relative;
float: left;
width: 600px;
height: 16px;
padding: 2px;
border: 1px solid #CCCCCC;
background: #EEEEEE;
}
#progreso{
position: absolute;
width: 0px;
height: 16px;
background: rgba(0,0,150,.2);
}
The result of our code is the shown in the following image.
USE INTERNATIONAL LANGUAGE TO CODE: ENGLISH + PRETTIER
The first step in any code of this world is use the international language of coding. Today the mainstream language is English, I know that the other languages such as Chinese or Spanish are top trending language but really is more easy understand a code which is written in English. I'm Spanish-native and for me is more difficult write and speak in English but I know that if I write my code in English I can share my code with the world.
So, the first step is translate my code to English, It is not required a perfect English because the majority of the audience is not English-native.
Another important and basic tip to code is use a tool which normalize your code because there is not relevant the spaces or tabs in your code, the number of characters which use in each line or the way in that your create a new object but there is very important that you always do this task the same way. I.e, you need normalize your code with you and your partners. The best tool in the Web develop ecosystem today es Prettier. So, I recommend you that you install this tool in your favorite IDE (in my case I'm using the extension in VSCode) .
Now, you can read the code using an international language and using a tool which formatted my code:
<!DOCTYPE html>
<html lang="eng">
<head>
<title>Video Player</title>
<link rel="stylesheet" href="player.css" />
<script src="player.js"></script>
</head>
<body>
<section id="player">
<video id="media" width="720" height="400">
<source
src="https://www.w3schools.com/html/mov_bbb.mp4"
type="video/mp4"
/>
<source
src="https://www.w3schools.com/html/mov_bbb.ogg"
type="video/ogg"
/>
</video>
<nav>
<div id="controls"><button type="button" id="play">Play</button></div>
<div id="bar"><div id="progress"></div></div>
<div style="clear: both"></div>
</nav>
</section>
</body>
</html>
player.css
body {
text-align: center;
}
header,
section,
footer,
aside,
nav,
article,
figure,
figcaption,
hgroup {
display: block;
}
#player {
width: 720px;
margin: 20px auto;
padding: 5px;
background: #999999;
border: 1px solid #666666;
border-radius: 5px;
}
nav {
margin: 5px 0px;
}
#controls {
float: left;
width: 100px;
height: 20px;
}
#bar {
position: relative;
float: left;
width: 600px;
height: 16px;
padding: 2px;
border: 1px solid #cccccc;
background: #eeeeee;
}
#progress {
position: absolute;
width: 0px;
height: 16px;
background: rgba(0, 0, 150, 0.2);
}
player.js
unction init() {
max = 600;
media = document.getElementById('media');
play = document.getElementById('play');
bar = document.getElementById('bar');
progress = document.getElementById('progress');
play.addEventListener('click', click, false);
bar.addEventListener('click', move, false);
}
function click() {
if (!media.paused && !media.ended) {
media.pause();
play.innerHTML = 'Play';
window.clearInterval(loop);
} else {
media.play();
play.innerHTML = 'Pause';
loop = setInterval(state, 1000);
}
}
function state() {
if (!media.ended) {
var total = parseInt((media.currentTime * max) / media.duration);
progress.style.width = total + 'px';
} else {
progress.style.width = '0px';
play.innerHTML = 'Play';
window.clearInterval(loop);
}
}
function move(e) {
if (!media.paused && !media.ended) {
var mouseX = e.pageX - bar.offsetLeft;
var newTime = (mouseX * media.duration) / max;
media.currentTime = newTime;
progress.style.width = mouseX + 'px';
}
}
window.addEventListener('load', init, false);
The GitHub project in this branch is https://github.com/Caballerog/refactoring-js-patterns-tips/tree/01-international-code.
VARIABLE SCOPE - REVEAL MODULE PATTERN
There is very important that you know how variable scope works in your language because you can avoid effects side in the future and to do your code more cleaner and readable.
In JavaScript there are 4 way to define a variable and the main difference between those way is the scope of the variable. In the following links you can read about this Vojtechruzicka Blog, Stackoverflow QA and Andy Carter Blog but a brief summary could be the following:
- Only write your variable in any place of your code. This is the worst way to declare a variable. In this case, the variable has a scope GLOBAL in your app (OMG!! since a private function are you defining a GLOBAL variable) which can provoke side-effects and several conflicts in your code. So, never declare a variable without a keyword.
- Use the keyword var. Using the keyword var you have hoisting and functional scope. Your variable can provoke side-effects inside a function because the variable is define in the top of your code (in your function) and there is in any place inside your function and the value is assigned in the line of code in which you are initializing your variable.
- Use the keyword let/const. This is the way in which ES6 and the community prefer used to define the variables because the scope of this variables is block (as you expect it to behave because most languages using a block scope) but the main different between let and const is that the pointer of memory in which is store the reference of the value of the variable can change or not (OMG!). In other words, let is used to variables in which the value can change in the code and const is used when the value of the variable can not change if the type is primitive (number or string) and can the pointer of the value can not change if the type is not primitive, for example objects and array.
Ok, in this moment I know that I should change the way in which I'm defining my variables in JavaScript. So, the code would be the following:
const max = 600;
const media = document.getElementById('media');
const play = document.getElementById('play');
const bar = document.getElementById('bar');
const progress = document.getElementById('progress');
let loop;
function init() {
play.addEventListener('click', click, false);
bar.addEventListener('click', move, false);
}
The global variables that was define in the init function are moved to the top of the script and use the keyword const
because of there are references to the DOM or a primitive value such as max
. Also, there is a variable called loop
which is used to store the reference ID using returned in the setTimeInterval
function.
At the moment, both code are the same behavior because the variables were global and in this moment are global too. For this reason, we could use the pattern Reveal Module which allow create private scope to the functions and variables. This pattern can be read in this blogpost.
The pattern module reveal allow us convert in public our interface of communication with the other pieces of the code. In this case the methods init
, click
, and move
. The method state
now has the scope of our module which is called player
. The rest of variables are private too, so there, we just to avoid side-effects provoke for the global scope of our variables or methods.
The code with applying this pattern is the following:
const player = (function() {
const max = 600;
const media = document.getElementById('media');
const play = document.getElementById('play');
const bar = document.getElementById('bar');
const progress = document.getElementById('progress');
let loop;
function init() {
play.addEventListener('click', click, false);
bar.addEventListener('click', move, false);
}
function click() {
if (!media.paused && !media.ended) {
media.pause();
play.innerHTML = 'Play';
window.clearInterval(loop);
} else {
media.play();
play.innerHTML = 'Pause';
loop = setInterval(state, 1000);
}
}
function move(e) {
if (!media.paused && !media.ended) {
const mouseX = e.pageX - bar.offsetLeft;
const newTime = (mouseX * media.duration) / max;
media.currentTime = newTime;
progress.style.width = mouseX + 'px';
}
}
function state() {
if (!media.ended) {
const total = parseInt((media.currentTime * max) / media.duration);
progress.style.width = total + 'px';
} else {
progress.style.width = '0px';
play.innerHTML = 'Play';
window.clearInterval(loop);
}
}
return {
init,
click,
move
};
})();
window.addEventListener('load', player.init, false);
But the code below there is not works due to, the pattern module reveal is using a IIFE which run before that the DOM is loaded, so the variables media
, play
, etc. are null
when the script run for these lines. The solution is move the definition of this variables inside the method init, but in this refactoring we going to create a object called GUI
which store the information about the DOM (in large apps this object would be translate to other files using the famous pattern MVC.
So, the final code in this step is the following:
const player = (function(view) {
const max = 600;
let GUI = {};
let loop;
function init() {
GUI = {
media: view.getElementById('media'),
play: view.getElementById('play'),
bar: view.getElementById('bar'),
progress: view.getElementById('progress')
};
GUI.play.addEventListener('click', click, false);
GUI.bar.addEventListener('click', move, false);
}
function click() {
if (!GUI.media.paused && !GUI.media.ended) {
GUI.media.pause();
GUI.play.innerHTML = 'Play';
clearInterval(loop);
} else {
GUI.media.play();
GUI.play.innerHTML = 'Pause';
loop = setInterval(state, 1000);
}
}
function move(e) {
if (!GUI.media.paused && !GUI.media.ended) {
const mouseX = e.pageX - bar.offsetLeft;
const newTime = (mouseX * media.duration) / max;
GUI.media.currentTime = newTime;
GUI.progress.style.width = mouseX + 'px';
}
}
function state() {
if (!GUI.media.ended) {
const total = parseInt(
(GUI.media.currentTime * max) / GUI.media.duration
);
GUI.progress.style.width = total + 'px';
} else {
GUI.progress.style.width = '0px';
GUI.play.innerHTML = 'Play';
clearInterval(loop);
}
}
return {
init,
click,
move
};
})(window.document);
window.addEventListener('load', player.init, false);
The GitHub project in this branch is hhttps://github.com/Caballerog/refactoring-js-patterns-tips/tree/02-scope-module.
CREATE CONDITIONAL METHODS
The following step to avoid complex in our code consist in create functions which abstract your complex logic in simple and readable functions to take decision about of different states.
For example, the logic from the conditional sentence can be extract in short functions using the prefix "is" to express that the function return a boolean value.
function click() {
if (!GUI.media.paused && !GUI.media.ended) {
GUI.media.pause();
GUI.play.innerHTML = 'Play';
clearInterval(loop);
} else {
GUI.media.play();
GUI.play.innerHTML = 'Pause';
loop = setInterval(state, 1000);
}
}
function move(e) {
if (!GUI.media.paused && !GUI.media.ended) {
const mouseX = e.pageX - bar.offsetLeft;
const newTime = (mouseX * media.duration) / max;
GUI.media.currentTime = newTime;
GUI.progress.style.width = mouseX + 'px';
}
}
function state() {
if (!GUI.media.ended) {
const total = parseInt(
(GUI.media.currentTime * max) / GUI.media.duration
);
GUI.progress.style.width = total + 'px';
} else {
GUI.progress.style.width = '0px';
GUI.play.innerHTML = 'Play';
clearInterval(loop);
}
}
The code could be the following:
function click() {
if (!isVideoPausedOrEnded()) {
GUI.media.pause();
GUI.play.innerHTML = 'Play';
clearInterval(loop);
} else {
GUI.media.play();
GUI.play.innerHTML = 'Pause';
loop = setInterval(state, 1000);
}
}
function move(e) {
if (!isVideoPausedOrEnded()) {
const mouseX = e.pageX - bar.offsetLeft;
const newTime = (mouseX * media.duration) / max;
GUI.media.currentTime = newTime;
GUI.progress.style.width = mouseX + 'px';
}
}
function state() {
if (!isVideoEnded()) {
const total = parseInt(
(GUI.media.currentTime * max) / GUI.media.duration
);
GUI.progress.style.width = total + 'px';
} else {
GUI.progress.style.width = '0px';
GUI.play.innerHTML = 'Play';
clearInterval(loop);
}
}
function isVideoPausedOrEnded() {
return GUI.media.paused || GUI.media.ended;
}
function isVideoEnded() {
return GUI.media.ended;
}
The GitHub project in this branch is https://github.com/Caballerog/refactoring-js-patterns-tips/tree/04-create-conditional-methods.
EXTRACT CODE IN READABLE FUNCTIONS
The next step can be extract code in readable pieces of code (functions) which add semantic value in our code. Thanks to this decision we can think about the goal of our application. So, the method click and state can be refactor in the following methods:
Refactor code
function click() {
if (!isVideoPausedOrEnded()) {
pauseVideo();
} else {
playVideo();
}
}
function pauseVideo() {
GUI.media.pause();
GUI.play.innerHTML = 'Play';
clearInterval(loop);
}
function playVideo() {
GUI.media.play();
GUI.play.innerHTML = 'Pause';
loop = setInterval(state, 1000);
}
function stateEndVideo() {
GUI.progress.style.width = '0px';
GUI.play.innerHTML = 'Play';
clearInterval(loop);
}
function statePlayingVideo() {
const total = parseInt((GUI.media.currentTime * max) / GUI.media.duration);
GUI.progress.style.width = total + 'px';
}
function state() {
if (!isVideoEnded()) {
statePlayingVideo();
} else {
stateEndVideo();
}
}
The GitHub project in this branch is https://github.com/Caballerog/refactoring-js-patterns-tips/tree/05-short-methods.
COMMAND PATTERN
In this moment, the code is a bit confuse because we are using if-else to use a function which to do a action or command. There is a classical pattern called Command
which allows us abstract our actions/commands in an object. This pattern is more complex in its traditional version but in JavaScript we can implement this pattern using an object which receive a key to indicate the action to do.
So, the first step is define the commands/actions, to simply the code, this object is define in the same player.js.
const ACTIONS = {
PLAY_VIDEO: 'PLAY',
PAUSE_VIDEO: 'PAUSE',
STATE_END: 'STATE_END',
STATE_PLAYING: 'STATE_PLAYING'
};
The actions are Play, Pause, State end and State Playing. The command pattern consist in execute a method or action using this key (in our example there is not required parameters), therefor the command object is the following:
const command = {
[ACTIONS.PLAY_VIDEO]: playVideo,
[ACTIONS.PAUSE_VIDEO]: pauseVideo,
[ACTIONS.STATE_END]: stateEndVideo,
[ACTIONS.STATE_PLAYING]: statePlayingVideo,
execute: function(action) {
this[action]();
}
};
Now, the moment in which the if-else dissapers using the pattern command:
function click() {
const action = isVideoPausedOrEnded()
? ACTIONS.PLAY_VIDEO
: ACTIONS.PAUSE_VIDEO;
command.execute(action);
}
function state() {
const action = isVideoEnded() ? ACTIONS.STATE_END : ACTIONS.STATE_PLAYING;
command.execute(action);
}
The GitHub project in this branch is https://github.com/Caballerog/refactoring-js-patterns-tips/tree/06-command-pattern.
RESUME
In this example I've done several changes to improve the quality of my code. The quality of my code is not relational with performance but also about the readable and extensible. The following list is a resume about the different techniques which I've been applied in my code.
- International Code. Never programming using your local language, unless your local language is English. Nowadays, the international language is the english. So, you must develop your code using english. Imagine a code in German or French or Polish and you don't know that language!
- Prettier + CommitLint
- Variables Scope. Study in your language how the scope works in the declaration of variables. In JavaScript must understand that there are 4 ways to define a variable and its scope is totally different.
- Create conditional methods. Refactor your condition in functions which abstract your complex logic in a simple and readable function.
- Create short, simple and readable functions. Extract code in reusable functions or, at least, add semantic value in your code.
The GitHub project is https://github.com/Caballerog/refactoring-js-patterns-tips.