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 ids in the above HTML. The ids 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 buttons with number output
  • localStorage to remember vote states between page loads (or use a real DB :))

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>