creating a Javascript voting system
I'm using the Prototype Pattern to organize the code.
This will work for n
.vote
classes in the HTML
. For example, given the following HTML
, two vote objects will be created and associated with their respective UI.
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">0</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">0</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
You might have noticed that there are no id
s in the above HTML
. The id
s are created dynamically in a forEach
loop and assigned on init
of each object. I'm using myVotePrototype
as a template, copying its prototype
into each new object created in myVote
. myVote
takes an id
to initialize, which is how each vote knows where to find its associated UI piece.
What about colors?
The JavaScript
sets the voting direction in the .vote
parent container. So, after an up-vote, the HTML
for that vote object would look like this:
<div class="vote vote-up">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">0</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
I added a little CSS
so that each button knows when to assume the active color. I found this less messy than writing the hex code directly to a style
attribute.
.vote.vote-up .up-vote,
.vote.vote-down .down-vote {
color: #3CBC8D;
}
Demo
const myVotePrototype = {
init: function(id) {
this.voteId = id;
// Prepare for voting clicks
this.bindEvents();
},
votes: 0,
upVote: function() {
this.votes++;
this.setVoteDirection('up');
},
downVote: function() {
this.votes--;
this.setVoteDirection('down');
},
setVoteDirection: function(direction) {
let voteObj = document.getElementById(this.voteId);
if (direction === 'up') {
voteObj.classList.add('vote-up');
if (voteObj.classList.contains('vote-down')) {
voteObj.classList.remove('vote-down');
}
} else if (direction === 'down') {
voteObj.classList.add('vote-down');
if (voteObj.classList.contains('vote-up')) {
voteObj.classList.remove('vote-up');
}
}
},
updateUI: function() {
document.querySelector(`#${this.voteId} .number`).innerHTML = Number(this.votes);
},
bindEvents: function() {
document
.querySelector(`#${this.voteId} .up-vote`)
.addEventListener('click', () => {
this.upVote();
this.updateUI();
});
document
.querySelector(`#${this.voteId} .down-vote`)
.addEventListener('click', () => {
this.downVote();
this.updateUI();
})
}
};
function myVote(id) {
function V() {};
V.prototype = myVotePrototype;
let v = new V();
v.init(id);
return v;
}
// Loop through all votes in the UI
const votes = document.querySelectorAll('.vote');
votes.forEach((vote, index) => {
// Create an id
let voteId = `vote_${index}`;
// Set the id in the UI so we can find it later for updating
vote.setAttribute('id', voteId);
// Create a new vote object, passing in the vote id
myVote(voteId);
});
.number {
display: inline-block;
text-align: center;
}
.vote {
display: flex;
flex-direction: column;
text-align: center;
}
.up-vote,
.down-vote {
color: dimgray;
cursor: pointer;
}
.vote.vote-up .up-vote,
.vote.vote-down .down-vote {
color: #3CBC8D;
}
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">0</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">0</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">0</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
Ideas for improvement
- Semantic, accessible
HTML
(<button>
instead of<span>
) - Use of ARIA to associate
button
s withnumber
output localStorage
to remember vote states between page loads (or use a realDB
:))
I think you will find this easier to control and think about if you go with the array method you mentioned as option #1 and then make a single function for upvoting and a single function for downvoting instead of creating separate functions for every single up and down arrow like you are doing now.
You have four groups right now, so inside your for
loop we can initialize an array like this:
const votes = [
0: { up: false, down: false },
1: { up: false, down: false },
2: { up: false, down: false },
3: { up: false, down: false },
];
Then you can just call your up- and down-voting functions and check the value of the object in that array that corresponds to the group number of the arrow you clicked.
The other thing that I think helps readability is separating out changing the vote total from the color changes.
Your vote logic includes three distinct possibilities, 1.) arrow was already checked, 2.) opposite arrow was checked, and 3.) neither arrow was checked. But your color-changing logic and the logic for changing the checked value of each arrow really only has two possibilities: either the arrow was checked before or it wasn't.
So I went ahead and made that change in the below snippet as well.
Hope this helps.
const up_vote_spans = document.getElementsByClassName('up-vote');
const down_vote_spans = document.getElementsByClassName('down-vote');
const count = document.getElementsByClassName('number');
let votes = [];
for (let i = 0; i < count.length; i += 1) {
const thisUpVoteSpan = up_vote_spans[i];
const thisDownVoteSpan = down_vote_spans[i];
votes[i] = { up: false, down: false };
thisUpVoteSpan.addEventListener('click', handleUpvote.bind(null, i), false);
thisDownVoteSpan.addEventListener('click', handleDownvote.bind(null, i), false);
}
function handleUpvote(i) {
const currentVote = votes[i];
const matchingUpSpan = up_vote_spans[i];
const matchingDownSpan = down_vote_spans[i];
const matchingCount = count[i];
const currentCount = parseInt(matchingCount.innerHTML);
if (currentVote.down) {
matchingCount.innerHTML = currentCount + 2;
} else if (currentVote.up === false) {
matchingCount.innerHTML = currentCount + 1;
} else {
matchingCount.innerHTML = currentCount - 1;
}
if (!currentVote.up) {
matchingUpSpan.style.color = "#3CBC8D";
matchingDownSpan.style.color = 'dimgray';
currentVote.up = true;
currentVote.down = false;
} else {
matchingUpSpan.style.color = 'dimgray';
currentVote.up = false;
}
}
function handleDownvote(i) {
const currentVote = votes[i];
const matchingUpSpan = up_vote_spans[i];
const matchingDownSpan = down_vote_spans[i];
const matchingCount = count[i];
const currentCount = parseInt(matchingCount.innerHTML);
if (currentVote.up) {
matchingCount.innerHTML = currentCount - 2;
} else if (currentVote.down === false) {
matchingCount.innerHTML = currentCount - 1;
} else {
matchingCount.innerHTML = currentCount + 1;
}
if (!currentVote.down) {
matchingDownSpan.style.color = "#3CBC8D";
matchingUpSpan.style.color = 'dimgray';
currentVote.down = true;
currentVote.up = false;
} else {
matchingDownSpan.style.color = 'dimgray';
currentVote.down = false;
}
}
.number {
display: inline-block;
text-align: center;
}
.vote {
display: flex;
flex-direction: column;
text-align: center;
}
.up-vote {
color: dimgray;
cursor: pointer;
}
.down-vote {
cursor: pointer;
color: dimgray;
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
</head>
<body>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">990</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">990</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">990</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
<div class="vote">
<span class="up-vote"><i class="fas fa-angle-up"></i></span>
<span class="number">990</span>
<span class="down-vote"><i class="fas fa-angle-down"></i></span>
</div>
</body>
</html>