2

In my Meteor project, I have two buttons. One button is an upvote button, which adds a point to the score of an entry, while the other is a downvote button, which does the opposite. My site doesn't require login.

How can I restrict it so that any given device can only click either the upvote or downvote button initially, and then if that device decides to change it's vote, it should be able to click only the other button, and so on?

Dirk Jan
  • 2,075
  • 3
  • 13
  • 30
jswny
  • 157
  • 12

2 Answers2

2

It sounds like a regular old radio button should do the trick.

I made some fancier stuff as well, see this CodePen.

Update

Added @4castle's rescind vote function. Nice touch.

Update 2

Per OP's request, the radio buttons are now the arrows.

CodePen 2

html,
body {
  box-sizing: border-box;
  background: #111;
  color: #DDD;
  font: 400 16px/1.4'Verdana';
  height: 100vh;
  width: 100vw;
}
*,
*:before,
*:after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
  border: 0 none hlsa(0%, 0, 0, 0);
  outline: 0 none hlsa(0%, 0, 0, 0);
}
fieldset {
  margin: 0 1em 1em 1em;
  padding: 8px;
  border-radius: 9px;
  border: 3px double #FF8;
  width: 100%;
  max-width: 19em;
}
legend {
  font: small-caps 700 1.5rem/2"Palatino Linotype";
  color: #FD1;
}
/* RadZ */

#radz input.chkrad {
  display: none;
}
#radz input.chkrad + label {
  color: #EEE;
  background: transparent;
  font-size: 16px;
}
#radz input.chkrad:checked + label {
  color: #0ff;
  background: transparent;
  font-size: 16px;
}
#radz input.chkrad + label span {
  display: inline-block;
  width: 18px;
  height: 18px;
  margin: -1px 15px 0 0;
  vertical-align: baseline;
  cursor: pointer;
}
#radz input + label span {
  background: transparent;
  line-height: 100%;
}
input.A + label span:before {
  content: '△';
  color: #0ff;
  font-style: normal;
  font-weight: 700;
  font-size: 24px;
}
input.A:checked + label span:before {
  padding: -5px 15px 5px;
  font-size: 16px;
}
input.A:checked + label span:before {
  content: '▲';
  color: #0ff;
  font-style: normal;
  font-weight: 700;
  font-size: 24px;
}
input.B + label span:before {
  content: '▽';
  color: #0ff;
  font-style: normal;
  font-weight: 700;
  font-size: 24px;
}
input.B:checked + label span {
  padding: -5px 15px 5px;
  font-size: 16px;
}
input.B:checked + label span:before {
  content: '▼';
  color: #0ff;
  font-style: normal;
  font-weight: 700;
  font-size: 24px;
}
input.fade + label span,
input.fade:checked + label span {
  -webkit-transition: background 0.7s linear;
  -moz-transition: background 0.7s linear;
  transition: background 0.7s linear;
}
<fieldset id="radz" name="radz">
  <legend>Vote</legend>
  <input type='radio' name='rad' id="rad1" class="chkrad A fade" value='1' />
  <label for="rad1"><span></span>Up</label>
  <br/>
  <input type='radio' name='rad' id="rad2" class="chkrad B fade" value='2' />
  <label for="rad2"><span></span>Down</label>
  <br/>
</fieldset>

// Rescind vote function provided by 4castle
// http://stackoverflow.com/users/5743988/4castle

var selectedRad;
var voteRads = document.querySelectorAll('input[name="vote"]');
for (var i = 0; i < voteRads.length; i++) {
  voteRads[i].onclick = function() {
    if (selectedRad == this) {
      this.checked = false;
      selectedRad = null;
    } else {
      selectedRad = this;
    }
  };
}
.rad1 + label:after {
  content: '△';
}
.rad1:checked + label:after {
  content: '▲';
}
.rad2 + label:after {
  content: '▽';
}
.rad2:checked + label:after {
  content: '▼';
}
<input id="up" type="radio" class="rad1" name="vote">
<label for="up"></label>
<br/>
<label>Vote</label>
<br/>
<input id="down" type="radio" class="rad2" name="vote">
<label for="down"></label>
zer00ne
  • 31,838
  • 5
  • 32
  • 53
  • 1
    Add the JS code in [this fiddle](https://jsfiddle.net/ohmt9b2h/1/) to allow the user to deselect their vote by clicking the selected vote again. – 4castle Mar 07 '16 at 15:52
  • @zer00ne Any way to have the icons act as the radio buttons so you can't see the radio buttons next to the arrows? – jswny Mar 07 '16 at 16:56
  • @jswny Check my [CodePen](http://codepen.io/zer00ne/pen/bpVJBp) The trick is to style the label while the actual input is `display: none` The details are in the CodePen, you'll have to adjust according to what font icon you plan to use. – zer00ne Mar 07 '16 at 17:43
2

You can use HTML 5 LocalStorage. But it will only work on latest browsers. If you want suppoert for old browsers as well then you might be interested in this question as well. If your user base doesn't use very old browsers then you can do it with LocalStorage like this,

In template's created callback,

Template.yourTemplate.created = function () {
    var template = this;
    var userVote = null;
    if(typeof(Storage) !== "undefined") {
        userVote = localStorage.getItem("userVote");
    }
    template.userVote = new ReactiveVar(userVote); //or you can use Session.setDefault("userVote", userVote)
}

When user clicks on the up or down button

Template.yourTemplate.events({
    'click #upButton': function (ev, template) {
         localStorage.setItem("userVote", "up");
         template.userVote.set("up"); // or Session.set("userVote", "up");
    },
    'click #downButton': function (ev, template) {
         localStorage.setItem("userVote", "down");
         template.userVote.set("down"); // or Session.set("userVote", "down");
    }
});

Then to disable buttons, you can do something like this in your helpers,

Template.yourTemplate.helpers({
    'isUpButtonDisabled': function () {
         var template = Template.instance();
         var userVote = template.userVote.get(); // or Session.get("userVote");
         return userVote === "up";
    },
    'isDownButtonDisabled': function (ev, template) {
         var template = Template.instance();
         var userVote = template.userVote.get(); // or Session.get("userVote");
         return userVote === "down";
    }
});

Update: This answer uses localStorage so that the application can keep track of the user's vote even when user visits the same site at a later date, which was what OP was trying to do, since user can vote without a login.

EDIT: Based on your comment to have different votes for different templates/topics. Assuming you have current topic's id in template's current data. You can do something like this,

In template's created callback,

Template.yourTemplate.created = function () {
    var template = this;
    template.userVote = new ReactiveVar(null); //or you can use Session.setDefault("userVote", null)
    template.autorun(function () {
        var data = Template.currentData();
        var topicId = data.topicId;
        var userVote = null;
        if(typeof(Storage) !== "undefined") {
            userVote = localStorage.getItem("userVote" + topicId);
        }
        template.userVote.set(userVote); //or you can use Session.set("userVote", userVote);
    });
}

When user clicks on the up or down button

Template.yourTemplate.events({
    'click #upButton': function (ev, template) {
         var topicId = this.topicId;
         localStorage.setItem("userVote" + topicId, "up");
         template.userVote.set("up"); // or Session.set("userVote", "up");
    },
    'click #downButton': function (ev, template) {
         var topicId = this.topicId;
         localStorage.setItem("userVote" + topicId, "down");
         template.userVote.set("down"); // or Session.set("userVote", "down");
    }
});

Then to disable buttons, you can do something like this in your helpers,

Template.yourTemplate.helpers({
    'isUpButtonDisabled': function () {
         var template = Template.instance();
         var userVote = template.userVote.get(); // or Session.get("userVote");
         return userVote === "up";
    },
    'isDownButtonDisabled': function (ev, template) {
         var template = Template.instance();
         var userVote = template.userVote.get(); // or Session.get("userVote");
         return userVote === "down";
    }
});
Community
  • 1
  • 1
Kishor
  • 2,589
  • 4
  • 14
  • 33
  • 1
    I disagree. The radio button solution is not persistent. I appreciate this answer as well! – jswny Mar 07 '16 at 16:14
  • 1
    Ah, my apologies. If you were to update your answer, perhaps mentioning that the goal is to give persistence, then SO will let me change my downvote to an upvote. Sorry for the misunderstanding – 4castle Mar 08 '16 at 00:55
  • @Kishor will this work if I my app uses many different templates which each need to individually check whether the user had hit upvote/downvote on itself? – jswny Mar 09 '16 at 06:32
  • @jswny No this will override previous one when you do the same thing on another template. If you want this to work for individual topics, then you might need to do something like this `localStorage.getItem("userVote" + topicId);`, where topicId comes from the topic currently being voted. – Kishor Mar 09 '16 at 06:36
  • @Kishor after the new edit, it seems to be working great except for the fact that when I vote on one topic, then refresh the page, that vote is applied to all other topics for which I've not voted yet. – jswny Mar 09 '16 at 07:51
  • @jswny Strange! That will only happen if `topicId` or whatever `id` that you are using in `localStorage.getItem("userVote" + topicId);` is null or has same value on all templates. Could you console or alert and see if topicId is not null. May be you can update your question with some sample code so that I will be able to help. – Kishor Mar 09 '16 at 07:57
  • 1
    @Kishor It seems I had typed something wrong. All fixed now, thank you very much! – jswny Mar 09 '16 at 08:07