Hi there👋🏽.
Intro
Translating a data structure(DS) to a visual component is an essential skill for a developer, especially as a JS dev, as you may find yourself dabbling in the frontend from time to time, which is what we going to do in this tutorial.
this is a build from the previous tut JavaScript tic tac toe - beginner level - t24 where we built a working console logged tic tac toe, you can find the code on git.
Intermediate
Translating a DS into a visual component is part of the intermediate, However the true intermediate comes from handling two environments, as now we are wrapping a node/native app for the browser, which is very exciting actually.
Node vs Browser
Node and the browser are completely two separate distinct environments that JS can run in, Node js(the site) words it way better "Unlike the browser where JavaScript is sandboxed for your safety, node. js has full access to the system like any other native application"
let's get crackin
before we do anything we need a way to detect or tell whether our code is running in the browser or node, for example the browser does not have the readline module while node has no document(DOM), so we need a way to run specific code for a specific env;
let inNode = true; // on top
if (typeof process !== 'undefined') {
// any code specific to node should be in this block and will not run in the browser
// as process is not defined in the browser but node only
// since readline is in node we need to move it's require here
}else{
// browser specific code is controlled by this else
// inNode being false
inNode = false;
}
moving node specific code
// move the readline emitter and require here
// first declare readline on top, as it will hold our readline module on load
let readline; // will remain undefined in the browser
let inNode = true;
if (typeof process !== 'undefined') {
readline = require("readline");
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if (key.ctrl && key.name === 'c') {
process.exit();
} else {
if(turn){
if(combo.row){
combo.col = key.name
turn = false
game.move(combo)
}else{
combo.row = key.name
}
}else{
console.log("wait your turn")
}
}
});
}else{
inNode = false;
}
as simple as that now we can run browser and node specific code in one file.
Browser side
here is a simple html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
box-sizing: border-box;
}
body{
padding: 0;
margin: 0;
font-family: sans-serif;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.app{
display: flex;
flex-wrap: wrap;
}
.reset{
width: 0px;
height: 0px;
border: 1px solid black;
}
.img{
/* width: 100px;
height: 100px; */
object-fit: cover;
}
</style>
</head>
<body>
<div class="app">
</div>
<script src="tic.js"></script>
</body>
</html>
I won't be explaining the css, as I assume it's very easy as this also is an intermediate tut
Out goal here is translate our board :
const board = [[" ", " " , " "],
[" ", " " , " "],
[" ", " " , " "],
]
to a visual board in the browser, so I had two options: to use elements or the canvas, I picked elements, as the canvas is kinda complex but way faster, speed won't be a problem for us thou since our board is small. Maybe in the advanced level we will have a side by side, elements and canvas.
first things first we need to update the updateBoard
function to not console.log in the browser
updateBoard: function(){
if(inNode){
console.log(" ")
board.forEach((arr, i)=> {
console.log(arr.toString().replace(/,/g, "|"))
})
}else{
}
}
the following is browser code block, everything we do from about now, will be in this block
if (!inNode) {
// will run in the browser only
}
I suggest using VScode live server plugin to run this, using tools like Parcel will cause problems, as they transpile the code, I assume, you can try thou.
let w = 0; // declare on top
if (!inNode) {
let app = document.querySelector(".app")
w = Math.floor(300/board.length - 2);
// w is the width of each cell
// our app div is 300 x 300 px, we are dividing by
// board len(rows) so each cell will take up equal space
// basically creating a grid with equal spaces
// the minus 2 accounts for border line in each cell
/**
* @type {HTMLDivElement}
*/
const len = board.length
for(let i = 0; i < len; i++){
for(let j = 0; j < len; j++){
// create cell will return a div
app.appendChild(createCell(w, i, j))
// we are passing i and j so each cell is aware of it's position(coordinates)
// which correspond to coordinates on the board
}
}
}
the createcell function
let cells = {} // declare on top,
...
if (!inNode) {
function createCell(w, col, row){
// closure function,
// to make sure we receive the correct state onclick
function keepContext(){
let state = {
col,
row,
w
}
let d = document.createElement("div");
d.style = `background: lightgray;`
d.classList.add("reset")
// stretching the empty div width and height
d.style.paddingBottom = `${w}px`
d.style.paddingRight = `${w}px`
d.onclick = () => processClick(state)
// cache of the created cells(elements), so we
// use the elements in the updateBoard func to
cells[`${row}-${col}`] = d
return d
}
return keepContext()
}
...
}
all we need now is the processClick function
if (!inNode) {
function processClick(state){
const img = document.createElement("img")
img.src = "./img/X.png" // an Image with x, you can use any you like, I drew it with excalidraw, make the background transparent
img.height = state.w
img.width = state.w
// magic happens here:
if(board[state.row][state.col] === " " && turn){
// if the user clicks on empty cell and it's not computer playing
// append an image on the click row and col
cells[`${state.row}-${state.col}`].appendChild(img)
// also update the board(remember the coords correspond)
// so the computer knows we played and can see updated possible moves
board[state.row][state.col] = "X"
// update possible moves
game.update();
setTimeout(() => {
game.computer()
}, 300);
}
}
...
}
all we need to do now, is update the visual board from the state of the array board, so they are in sync,
in the else of updateBoard
else{
// console.log("updating viz board")
board.forEach((arr, i)=> {
arr.forEach((val, j)=> {
if(val === "O"){
// checking if there's no O image in the current cel
if(!cells[`${i}-${j}`].firstChild){
const img = document.createElement("img")
img.src = "./img/O.png"
img.height = w
img.width = w
cells[`${i}-${j}`].appendChild(img)
// adding O's in the visual board
}
}
})
})
}
remember X's are added onclick,
The only thing left now, is to sync the visual board with the states of winning and draw, meaning the player and computer cannot add O or X when the game is over,
it's a bit messy, but it works, in the advanced level, I will consider refactoring
...
update : function(){
this.isgameOver();
if(gameOver){
this.updateBoard()
console.log(`Game over ${winner} won!`)
if(inNode){
process.exit();
}else{
turn = false
}
}
this.updateBoard();
m = this.possibleMoves();
if(m.length === 0){
gameOver = true;
console.log("Game over by draw")
if(inNode){
process.exit();
}else{
turn = false
}
}
}
...
With that we are done with the intermediate level, as the levels increase the explanation does the opposite, if you get stuck use the repo**** as a reference
Conclusion
I will try to make this series as consistent as possible, More interesting projects coming, including a mini compiler👌🧙♂️, yes I am going all out, teaching is the best way to learn too,.
if you would like to support me, you can buy me a coffee below, I will highly appreciate it❤❤, you certainly don't have to if you can't.
with that cheers till next time, Happy Hacking!!