Amitav Roy

Blog on web and travel

Ajax for Like and Dislike using voting API | Drupal

Posted on July 2019

Drupal makes me happy

Using ajax can really improve the user experience of your site. In this tutorial, we will see how using the powerful voting API we can create a simple like dislike module and use ajax for that.

/**
 * Implementing hook_init()
 */
function likedislike_init() {
    $base_path = base_path();
    $module_path = drupal_get_path('module', 'likedislike');
    drupal_add_js($module_path . "/likedislike.js");
    drupal_add_js("var base_path = '".$base_path."'; var module_path = '".$module_path."';","inline");
    drupal_add_css($module_path."/templates/likedislike.css");
}

In hook_init() I have added one js file and an inline js variable “basepath” because I found it really difficult to get the base path in javascript. Finally, I came up with the solution of having a variable declared through inline js.

/**
 * Implementing hook_permission()
 */
function likedislike_permission() {
    return array(
        'like node' => array(
            'title' => t('Add like to node'),
            'description' => t('Allow users to add like to the nodes.'),
            'restrict access' => TRUE,
        ),
        'dislike node' => array(
            'title' => t('Add dislike to node'),
            'description' => t('Allow users to add dislike to the nodes.'),
            'restrict access' => TRUE,
        ),
        'like comment' => array(
            'title' => t('Add like to comment'),
            'description' => t('Allow users to add like to the comments.'),
            'restrict access' => TRUE,
        ),
        'dislike comment' => array(
            'title' => t('Add dislike to comment'),
            'description' => t('Allow users to add dislike to the comments.'),
            'restrict access' => TRUE,
        ),
    );
}

Here I have used hook_permission() for checking the access. Although this is not a compulsory step, I feel that having custom permissions will be a good option to easily restrict users based on the role if at any point it is required.

Ok, so after the permissions thing, it is the theme that we will be looking at. This is a very important part.

/**
 * Implementing hook_theme().
 */
function likedislike_theme() {
    return array(
        'like' => array(
            'template' => 'templates/like',
        ),
        'dislike' => array(
            'template' => 'templates/dislike',
        ),
    );
}

I have two theme variables: like and dislike. Each will have their own tpl which you can easily guess. And they will be using the tpl file which is located in the templates folder. [NOTE: It is very important that the tpl name and the variable in the template are having the same name, or it will not work.

/**
 * Implementing hook_menu()
 */
function likedislike_menu() {
    //Node like and dislike menu item.
    $items['likedislike/like/node/add'] = array(
        'title' => 'Add a like to the entity.',
        'description' => t('Add a vote to the node entity using voting api.'),
        'access arguments' => array('like node'),
        'page callback' => '_add_entity_like',
    );
    $items['likedislike/dislike/node/add'] = array(
        'title' => 'Add a dislike to the entity.',
        'description' => t('Add a vote to the node entity using voting api.'),
        'access arguments' => array('dislike node'),
        'page callback' => '_add_entity_dislike',
    );
      
    //Comment like and dislike menu item.
    $items['likedislike/like/comment/add'] = array(
        'title' => 'Add a like to the entity.',
        'description' => t('Add a vote to the node entity using voting api.'),
        'access arguments' => array('like comment'),
        'page callback' => '_add_entity_like',
    );
    $items['likedislike/dislike/comment/add'] = array(
        'title' => 'Add a dislike to the entity.',
        'description' => t('Add a vote to the node entity using voting api.'),
        'access arguments' => array('dislike comment'),
        'page callback' => '_add_entity_dislike',
    );
    return $items;
}

You can see there are four menu items that I have created. Two are for adding like to a node and adding a dislike to the node. And the other two are for adding like and dislike to the comment. Here in the menu items, you can see that I am checking the permissions that I have created in access arguments.

/**
 * Implementing hook_comment_load()
 */
function likedislike_comment_load($comments) {
    global $user;
    foreach ($comments as $comment) {
        $comment->like = theme('like',array(
            'eid' => $comment->cid,
            'likes' => _get_entity_vote_count($comment->cid,'like','comment'),
            'likestatus' => _get_entity_vote_count($comment->cid,'like','comment',$user->uid),
            'entity' => "entity-comment",
        ));
        $comment->dislike = theme('dislike', array(
            'eid' => $comment->cid,
            'dislikes' => _get_entity_vote_count($comment->cid,'dislike','comment'),
            'dislikestatus' => _get_entity_vote_count($comment->cid,'dislike','comment',$user->uid),
            'entity' => "entity-comment",
        ));
    }
}

Now using hook_comment_load() to load the elements to the comment when the comment is getting loaded through Drupal.

In comment->like you can see I have passed likes which is calculating the total likes of the comment entity. And the second is like status which is actually to check if the current user has any like or dislike to the current comment/entity. I have done almost the same thing to the node in hook_node_load() in the code below.

/**
 * Implement hook_node_load()
 */
function likedislike_node_load($nodes, $types) {
    global $user;
    foreach ($nodes as $node) {
        $node->like = theme('like',array(
            'eid' => $node->nid,
            'likes' => _get_entity_vote_count($node->nid,'like','node'),
            'likestatus' => _get_entity_vote_count($node->nid,'like','node',$user->uid),
            'entity' => "entity-node",
        ));
        $node->dislike = theme('dislike', array(
            'eid' => $node->nid,
            'dislikes' => _get_entity_vote_count($node->nid,'dislike','node'),
            'dislikestatus' => _get_entity_vote_count($node->nid,'dislike','node',$user->uid),
            'entity' => "entity-node",
        ));
    }
}

In both codes, you can see a function _get_entity_vote_count that I have used. This is one function to get the vote status where I am using voting API to get the desired results. Here is the function:

/**
 * This function gives back the number of votes for a particular entity with a particular type of voting.
 * For example, it can be used to get the number of likes and also dislikes. Just need to change the type.
 *
 * @param type $nodeId: the node id of the node for which the number of votes is required.
 * @param type $type: the category of vote: like/dislike etc.
 */
function _get_entity_vote_count($nodeId,$type,$entity,$uid=NULL) {
    if ($uid == NULL) {
        $criteria = array(
            'entity_id' => $nodeId,
            'tag' => $type,
            'entity_type' => $entity,
        );
    } else {
        $criteria = array(
            'entity_id' => $nodeId,
            'tag' => $type,
            'uid' => $uid,
            'entity_type' => $entity,
        );
    }
    $count = sizeof(votingapi_select_votes($criteria));
    if (!isset($count)) {
        $count = 0;
    }
    return $count;
}

Here votingapi_selecte_votes is the voting API function which does the magic for us. $criteria are the array of arguments that are required for the function. For more details about Voting API, you need to check the documentation for Voting API.

After this the only thing that remains on the module is the adding and removing the likes based on the entity.

/**
 * This function is getting called to add the vote to the current node.
 * Data type is a post.
 * Using Voting API to add votes and also to select the number of votes.
 * TODO: Need to get the value through a setting page so that admin can set the number of votes that can be added per like
 *
 */
function _add_entity_like() {
    global $user;
    if ($_GET['entityid']) {
        $nodeId = $_GET['entityid'];
        $entity_type = $_GET['entity'];
        //Check if disliked
        $checkCriteria = array(
            'entity_id' =< $nodeId,
            'tag' =< 'dislike',
            'uid' =< $user-<uid,
            'entity_type' =< $entity_type,
        );
        $dislikeResult = votingapi_select_votes($checkCriteria);
        $dislikeCount = sizeof($dislikeResult);
          
        if ($dislikeCount == 1) {
            print $dislikeResult-<vote_id;
            votingapi_delete_votes($dislikeResult);
        }
          
        $vote = array(
            'entity_id' =< $nodeId,
            'value'=< 1,
            'tag' =< 'like',
            'entity_type' =< $entity_type,
        );
        $setVote = votingapi_set_votes($vote);
          
        //Generating the likeCount and dislikeCount again.
        $criteriaLike = array(
            'entity_id' =< $nodeId,
            'tag' =< 'like',
            'entity_type' =< $entity_type,
        );
        $criteriaDislike = array(
            'entity_id' =< $nodeId,
            'tag' =< 'dislike',
            'entity_type' =< $entity_type,
        );
          
        $likeCount = sizeof(votingapi_select_votes($criteriaLike));
        $dislikeCount = sizeof(votingapi_select_votes($criteriaDislike));
        print $likeCount . "/" . $dislikeCount;
    }
}

Here we are using the data which will be coming from ajax post GET method. You can also use POST method which is a bit safer. But as I was developing this module I kept it in GET to test things out. Once I am happy with the testing, I would change it to POST. It really does not make a difference in the way that you code.

When I want to add a like, I need to check if the user has already disliked the same entity. If yes, first we need to remove it. We don’t want a situation where a user can like and even dislike the same entity.

Then we are adding the like to the following entity. The print thing has a “/” to it. Yes, the thing is when using ajax, you cannot return anything. You need to print it. And I failed to find a way to pass both the data expect this way. (If there is a better way, please let me know).

So, now all the code inside my module file is done. Let’s see how we are going to handle the ajax and what code will go inside the js file. As I have told before, the js file code is very much depended on the way the markup is generated. I am using jQuery instead of javascript and so any change in the tpl may result in a change in DOM hierarchy and that might break our jQuery code.

/**
 * This js file handles only the actions which are related to the like/dislike module.
 * Even the styling is handled by the CSS of this module css file so that it becomes ready to cook kind of thing.
 */
if(Drupal.jsEnabled) {
    $(document).ready(handleFlag);
}
  
function handleFlag() {
      
}

This is just the basic check that I have seen in most of the drupal js files, so just followed the convention. I guess checking Drupal.jsEnabled is a good thing to do anyway.

[NOTE: If you don’t know, Drupal core comes with jquery. It might not show any of the js files at the start if you are not using anything related to jquery. But as soon as you start using it, the required js file with the jquery library is enabled and included.]

//Handling the ajax thing for like node.
function likeNode(nodeId) {
    jQuery.ajax({
        type: "GET",
        url: base_path+"likedislike/like/node/add",
        data: 'entityid='+nodeId+"&entity=node",
        success: function(msg) {
            var arrLikeCount = msg.split("/");
            var likeCount = arrLikeCount[0];
            var dislikeCount = arrLikeCount[1];
              
            var msgDivId = "#dislike-container-"+nodeId+" .dislike-count-entity-node";
            jQuery(msgDivId).html(dislikeCount);
              
            var msgDivId = "#like-container-"+nodeId+" .like-count-entity-node";
            jQuery(msgDivId).html(likeCount);
              
            var imageNameLiked = "likeAct.png";
            var imageNameDislike = "dislike.png";
              
            jQuery("#like-container-"+nodeId+' .like a.entity-node').toggleClass('disable-status');
            jQuery("#dislike-container-"+nodeId+' .dislike a.entity-node').toggleClass('disable-status');
            jQuery("#like-container-"+nodeId+' .like img.entity node').attr('src',base_path+module_path+"/images/"+imageNameLiked);
            jQuery("#dislike-container-"+nodeId+' .dislike img.entity-node').attr('src',base_path+module_path+"/images/"+imageNameDislike);
        }
    });
}

This is the javascript which will take care of handling the like to the node. I am just passing the node id as a parameter to this function. The entity type is hardcoded here. (Although that can also be made generic, right now I have found functions which do the job for me.

  • Add like to node.
  • Add dislike to node.
  • Add like to comment.
  • Add dislike to comment.

What is it doing? Well, first is the thing about method: GET. Yes, as mentioned earlier, I am using GET here for the data from the page. As I am still testing, the GET is still there. Once I am comfortable with the code, will change these to POST (anyways, its just a node id).

URL here is the menu item that we have added to the hook_menu which has a page callback to _add_entity_like function. Here I used the basepath variable which I have already defined as an inline variable in hook_init. It is a js file and so the famous base_path() will not work. And hardcoding the path is out of question, so this is what I came up with.

Data is like the variables that I will be passing to the function through the url. Here the entity variable is hardcoded and that is why I have four different functions. Will try and eliminate the two functions and make it generic.

The success part is like what will happen once the vote is added to the entity. Right now, in the success part, I am using arrLikeCount which is nothing but the print at the end of the _add_entity_like function. Because we cannot return anything through ajax, I passed a string with data separated by slash and then used split to get the desired data. Rest: its simple jquery where I am changing the data inside the div containers. Basically, if someone clicks on like, then a green thumbs-up icon will come. If he had already disliked it, the red icon will turn grey and the green icon will then get disable-status.

/**
 * Handling the events on ready function.
 */
jQuery(document).ready(function () {
    //This is handling the click on the like link for node only.
    jQuery('.like-container-entity-node .like a').click(function () {
        var nodeId = jQuery(this).attr('nodeid');
        likeNode(nodeId);
    });
      
    //This is handling the click on the dislike link for node only.
    jQuery('.dislike-container-entity-node .dislike a').click(function () {
        var nodeId = jQuery(this).attr('nodeid');
        dislikeNode(nodeId);
    });
      
    //This is handling the click on the like link for comments only.
    jQuery('.like-container-entity-comment .like a').click(function () {
        var nodeId = jQuery(this).attr('nodeid');
        likeComment(nodeId);
    });
      
    //This is handling the click on the dislike link for node only.
    jQuery('.dislike-container-entity-comment .dislike a').click(function () {
        var nodeId = jQuery(this).attr('nodeid');
        dislikeComment(nodeId);
    });
});

This is the function which handles the click on the like button and triggers the event – the four different functions.

Although the rest four functions are same, still I would add it here just for reference.

//Handling the ajax thing for like node.
function likeNode(nodeId) {
    jQuery.ajax({
        type: "GET",
        url: base_path+"likedislike/like/node/add",
        data: 'entityid='+nodeId+"&entity=node",
        success: function(msg) {
            var arrLikeCount = msg.split("/");
            var likeCount = arrLikeCount[0];
            var dislikeCount = arrLikeCount[1];
              
            var msgDivId = "#dislike-container-"+nodeId+" .dislike-count-entity-node";
            jQuery(msgDivId).html(dislikeCount);
              
            var msgDivId = "#like-container-"+nodeId+" .like-count-entity-node";
            jQuery(msgDivId).html(likeCount);
              
            var imageNameLiked = "likeAct.png";
            var imageNameDislike = "dislike.png";
              
            jQuery("#like-container-"+nodeId+' .like a.entity-node').toggleClass('disable-status');
            jQuery("#dislike-container-"+nodeId+' .dislike a.entity-node').toggleClass('disable-status');
            jQuery("#like-container-"+nodeId+' .like img.entity-node').attr('src',base_path+module_path+"/images/"+imageNameLiked);
            jQuery("#dislike-container-"+nodeId+' .dislike img.entity-node').attr('src',base_path+module_path+"/images/"+imageNameDislike);
        }
    });
}
  
//Handling the ajax thing for dislie node.
function dislikeNode(nodeId) {
    jQuery.ajax({
        type: "GET",
        url: base_path+"likedislike/dislike/node/add",
        data: 'entityid='+nodeId+"&entity=node",
        success: function(msg) {
            var arrLikeCount = msg.split("/");
            var likeCount = arrLikeCount[0];
            var dislikeCount = arrLikeCount[1];
              
            var msgDivId = "#dislike-container-"+nodeId+" .dislike-count-entity-node";
            jQuery(msgDivId).html(dislikeCount);
              
            var msgDivId = "#like-container-"+nodeId+" .like-count-entity-node";
            jQuery(msgDivId).html(likeCount);
              
            var imageNameDisliked = "dislikeAct.png";
            var imageNameLike = "like.png";
              
            jQuery("#dislike-container-"+nodeId+' .dislike a.entity-node').toggleClass('disable-status');
            jQuery("#like-container-"+nodeId+' .like a.entity-node').toggleClass('disable-status');
            jQuery("#dislike-container-"+nodeId+' .dislike img.entity-node').attr('src',base_path+module_path+"/images/"+imageNameDisliked);
            jQuery("#like-container-"+nodeId+' .like img.entity-node').attr('src',base_path+module_path+"/images/"+imageNameLike);
        }
    });
}
  
//Handling the ajax thing for like node.
function likeComment(commentId) {
    jQuery.ajax({
        type: "GET",
        url: base_path+"likedislike/like/comment/add",
        data: 'entityid='+commentId+"&entity=comment",
        success: function(msg) {
            var arrLikeCount = msg.split("/");
            var likeCount = arrLikeCount[0];
            var dislikeCount = arrLikeCount[1];
              
            var msgDivId = "#dislike-container-"+commentId+" .dislike-count-entity-comment";
            jQuery(msgDivId).html(dislikeCount);
              
            var msgDivId = "#like-container-"+commentId+" .like-count-entity-comment";
            jQuery(msgDivId).html(likeCount);
              
            var imageNameLiked = "likeAct.png";
            var imageNameDislike = "dislike.png";
              
            jQuery("#like-container-"+commentId+' .like a.entity-comment').toggleClass('disable-status');
            jQuery("#dislike-container-"+commentId+' .dislike a.entity-comment').toggleClass('disable-status');
            jQuery("#like-container-"+commentId+' .like img.entity-comment').attr('src',base_path+module_path+"/images/"+imageNameLiked);
            jQuery("#dislike-container-"+commentId+' .dislike img.entity-comment').attr('src',base_path+module_path+"/images/"+imageNameDislike);
        }
    });
}
  
//Handling the ajax thing for dislie node.
function dislikeComment(commentId) {
    jQuery.ajax({
        type: "GET",
        url: base_path+"likedislike/dislike/comment/add",
        data: 'entityid='+commentId+"&entity=comment",
        success: function(msg) {
            var arrLikeCount = msg.split("/");
            var likeCount = arrLikeCount[0];
            var dislikeCount = arrLikeCount[1];
              
            var msgDivId = "#dislike-container-"+commentId+" .dislike-count-entity-comment";
            jQuery(msgDivId).html(dislikeCount);
              
            var msgDivId = "#like-container-"+commentId+" .like-count-entity-comment";
            jQuery(msgDivId).html(likeCount);
              
            var imageNameDisliked = "dislikeAct.png";
            var imageNameLike = "like.png";
              
            jQuery("#dislike-container-"+commentId+' .dislike a.entity-comment').toggleClass('disable-status');
            jQuery("#like-container-"+commentId+' .like a.entity-comment').toggleClass('disable-status');
            jQuery("#dislike-container-"+commentId+' .dislike img.entity-comment').attr('src',base_path+module_path+"/images/"+imageNameDisliked);
            jQuery("#like-container-"+commentId+' .like img.entity-comment').attr('src',base_path+module_path+"/images/"+imageNameLike);
        }
    });
}

Ok, now that all the coding part is done, let’s have a look at the like tpl.

<?php /** * This tpl handles the like link and its look and feel. * variables avaiable: * @id: the node id of the node/comment on which the link is getting printed. * @likes: the number is likes that is casted to the node/comment. */ $path = base_path() . drupal_get_path("module","likedislike"); ?>
 
<div class="like-container-<?php print $entity ?>" id="like-container-<?php print $eid; ?>">
 
<div class="like inline float-left">
        <?php if ($likestatus == 0): ?>
            <a href="javascript:;" nodeid="<?php print $eid; ?>" class="<?php print $entity ?>"><img src="<?php print $path ?>/images/like.png" alt="Like" title="Like" class="<?php print $entity ?>"></a>
        <?php endif; ?>
        <?php if ($likestatus == 1): ?>
            <a href="javascript:;" nodeid="<?php print $eid; ?>" class="disable-status <?php print $entity ?>"><img src="<?php print $path ?>/images/likeAct.png" alt="Like" title="Like" class="<?php print $entity ?>"></a>
        <?php endif; ?>
    </div>
 
 
<div class="float-left like-count-<?php print $entity ?>"><?php print $likes; ?></div>
 
</div>

The styling and all is supported by the css file that I have already added in hook_init.

When all these are done, you need to do print the variables in the tpl. Yes, right now they will not be visible automatically. So for the node tpl, you need to print the variables where you want them to be displayed. For example in my bartik theme, I have the following code just after render($content).

if (isset($like))  {
    print $like;
}
if (isset($dislike)) {
    print $dislike;
}

Once done, you need to clear the performance cache and then like and dislike links would be visible.