Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ufodisko's avatar

Building a voting system with ajax

I have an upvote/downvote system in Laravel 5 and using jquery ajax to send request to the controller. I am also using jquery.upvote.js to display the voting buttons.

So far I managed to allow get the ajax requests sent to the database just fine. However, I have 3 issues that I cannot complete.

  1. Persisting User Voted on View: I cannot persist the user's votes on the posts when the page reloads, say for example I upvoted and downvoted a few posts, then I reloaded the page, my upvotes on the view will disappear as if I haven't voted yet.

  2. Removing Vote if already exists: Say the user already upvoted a post, now he decides to remove his vote by clicking on the same vote button again, this should remove the post, but instead my code is adding a new row in the database with a new upvote.

  3. Remove Vote & Adding New Value: Say the user already upvoted a post, but now he decided that he wants to downvoted. In this case, I should delete the existing vote and add a new one with a new value, correct? or how is this achieved?

I am using resource for my routes

    Route::resource('subreddit', 'SubredditController');
    Route::resource('posts', 'PostsController');
    Route::resource('votes', 'VotesController');

I've been at this for a week, no solution in sight. Please help by providing code example for both php and jquery. And thank you.

Javascript

    <script type="text/javascript">
            $(document).ready(function() {
                $('.topic').upvote();
    
                $('.vote').on('click', function (e) {
                    e.preventDefault();
                    var data = {value: $(this).data('value'), post_id: $(this).parent().data('post')};
    
                    // var clicked_button = $(this).children();
    
                    $.ajaxSetup({
                        headers: {
                            'X-CSRF-TOKEN': $('[name="_token"]').val()
                        }
                    });
                    $.ajax({
                        type: "POST",
                        url: 'http://localhost/laravel-5/public/votes',
                        dataType: 'JSON',
                        data: data
                    });
                });
            });
        </script>

The relevant part of the View

    <div class="col-md-1">
        @if ($voted = in_array($post->id, $votes))
          {!! Form::open(['method' => 'DELETE', 'url' => ['votes', $post->id]]) !!}
        @else
          {!! Form::open(['url' => 'votes', 'class' => 'votes']) !!}
        @endif
           <div class="upvote topic" data-post="{{ $post->id }}">
           <a class="upvote vote {{ $voted ? 'upvote-on' : '' }}" data-value="1"></a>
           <span class="count">0</span>
           <a class="downvote vote {{ $voted ? 'downvote-on' : '' }}" data-value="-1"></a>
           </div>
           {!! Form::close() !!}
         </div>

SubredditController.php where I display a list of posts belonging to a category/subreddit

    public function show(Subreddit $subreddit)
        {
    
            $posts = Subreddit::findOrFail($subreddit->id)->posts()->get();
    
            $votes = Auth::user()->votes()->get()->toArray();
    
            //dd($votes);
    
            return view('subreddit/show')
                    ->with('subreddit', $subreddit)
                    ->with('posts', $posts)
                    ->with('votes', $votes);
        }

VotesController.php store and destroy methods

    public function store(Requests\VoteRequest $request)
        {
            // AJAX JSON RESPONSE
            $response = array(
                'status' => 'success',
                'msg' => 'Vote has been added.',
            );
            if(Auth::check()){
                \Log::info(Auth::user());
                Auth::user()->votes()->create($request->all());
            } else {
                    return \Response::json('Nope');
            }
    
            return \Response::json($response);
        }
    
    public function destroy(Requests\VoteRequest $request)
        {
            // AJAX JSON RESPONSE
            $response = array(
                'status' => 'success',
                'msg' => 'Vote has been removed.',
            );
    
            if(Auth::check()){
                \Log::info(Auth::user());
                Auth::user()->votes()->delete($request->all());
            } else {
                return \Response::json('Nope');
            }
    
            return \Response::json($response);
        }
0 likes
44 replies
ufodisko's avatar

@jlrdw Not sure if you looked at my code, that's exactly what I'm doing. As I've said, the voting works with few glitches that I'm trying to iron out.

jlrdw's avatar

Sorry deleted misunderstood something.

jlrdw's avatar

Without reading all that code all I know is to update something on a form you have to use the JavaScript to actually update a field or division or whatever without a page refresh what you just said you were doing a page refresh or send.

window.opener.$('#petowner').val(mystr[1]);

Would update the pet owner text field without a page refresh. It would put something in that field not update in the database are you sure you are saving the data to the database prior to a page reload?

ufodisko's avatar

@jlrdw I really don't know what you're talking about as the voting behavior I built is without page refresh.

jlrdw's avatar

In your number one above you said a page reload that's your exact words

ufodisko's avatar

@jlrdw I don't mean to be rude or anything but have you read my original post? Or seen my code?

When I talked about 'page reload' I meant to ask how to persist the votes the user has already made? and if you look closely at my view, I have that mechanism in place, but it's not working.

jlrdw's avatar

Sounds like the fields are getting updated properly but the data is not being saved to the database try routine without Ajax just to test to make sure the method is saving the data to the database.

ufodisko's avatar

@jlrdw the method is saving to the database, I have check with and without AJAX.

However, if I had already upvoted a post and I re-click on the upvote button (to cancel my original vote) it will submit it as a new upvote to that post instead of removing it.

jlrdw's avatar

Post yes, code no. If a record is saved to the database then you load a page that is supposed to have that data if it is not showing the record was never saved to the database. Get this completely working without Ajax then implement your Ajax. That is the best way to always do it first without Ajax get any bugs worked out then whip up the Ajax. Also better to just use one field only in the testing stage so you don't waste your time on several fields if there is a problem.

opheliadesign's avatar

@ufodisko A few things are sticking out to me right away..

First, what's up with the form tags around the buttons? You're using Ajax and I do not see any real correlation between the Ajax call and those form tags:

@if ($voted = in_array($post->id, $votes))
          {!! Form::open(['method' => 'DELETE', 'url' => ['votes', $post->id]]) !!}
        @else
          {!! Form::open(['url' => 'votes', 'class' => 'votes']) !!}
        @endif 

and then, in your Javascript:

 $.ajax({
         type: "POST", // you're sending a POST request no matter what - never a DELETE
         url: 'http://localhost/laravel-5/public/votes',
        dataType: 'JSON',
        data: data
});

And I'm not sure this little bit will actually work, but I'm preparing to test -

Auth::user()->votes()->delete($request->all());
ufodisko's avatar

@opheliadesign in the view, I am getting the logged-in user's votes and if a vote exists on a certain post, I will send the form with the delete method oherwise show the normal submit form.

How do I check if a vote exists in javascript? I know I need to pass `type: "DELETE" but how can I tell it to differentiate between the post and delete and when to use which?

I was never able to test delete($request->all()) I hope it works with you.

opheliadesign's avatar

@ufodisko Okay.. I installed your repo and I have absolutely no idea what I'm looking at lol. How do I get to the point where I can try voting on something? I created a user account, it logged me in, now what? Kinda unclear what this is all about..

Anyway, my initial suggestion would be to ditch the whole Route::resource() approach and stick to just doing a regular POST to a route. Then, in your controller (or repository, whatever you want to use), handle the logic to see if a user has already voted. I believe this is kind of how Stack Overflow works, if you try voting on your own post it seems to work for a split second until the JS receives an Ajax response saying that you can't vote on your own post.

ufodisko's avatar

@opheliadesign sorry about that.

After you've logged in. Create a new subreddit, then create a new post, in the "Subreddit" field on the post/create page enter the id of the subreddit you wish to associate the post with and then visit the subreddit, that's it.

ufodisko's avatar

@opheliadesign I do not mind using regular post routes for the voting. As long as it works.

I am already getting the logged-in user's votes on all posts. But how do I check for them in the controller and send them to my view and use them with AJAX?

I believe I need to check for 3 things here, which makes it a bit complicated for me as this is the first time I attempt to do something like this.

opheliadesign's avatar

@ufodisko Okay, hang tight a few so we don't flood the forum.. playing around with it now. Another thing that jumped out at me is that you're using a fully formed URL for your Ajax call, switch that to relative - /votes. This prevented the Ajax from working at all for me until I changed it.

Also, if Post model has a foreign key constraint for the Subredit model, you really need a select or something instead of just a text field. Nobody is going to know the ID of something like that and it isn't clear that's what is required there. I'm guessing a lot of this is just unfinished for testing purposes??

1 like
ufodisko's avatar

@opheliadesign using a relative path for ajax requests give me the error POST http://localhost/votes 404 (Not Found)

I don't understand your second point, I am using Route-Model-Binding here. Yes, this is still unfinished. I am attempting to create a reddit-like community so I can learn Laravel better and improve my coding skills. I will share the app on github when it's done. I intend to add a lot of features. But step by step for me.

opheliadesign's avatar

@ufodisko Okay well you really should not have a full URL like that. Working fine for me.

My friend, I really do not want to hurt your feelings, but it's clear that you really need to brush up on some basics. I'd try to work on learning more about Eloquent (especially relationships), Ajax (as suspected, your form tags were not doing anything and everything is just a POST to votes, therefor a store() call), etc. Also, protected $primaryKey = 'subreddit_id'; on your Post model seemed to be breaking your relations.

The reason your vote was not displayed on page reload is because you just set it to 0 in your HTML, there is no mechanism to change that anywhere. So, since it's just <span class="count">0</span>, it will always be the same.

Take a look at this revised show method for your SubredditController. I'm using eager loading for the posts and votes:

public function show(Subreddit $subreddit)
    {
    // note the with('posts.votes') - this will pull in all of the posts and their votes for each subreddit
        $subreddit = Subreddit::with('posts.votes')->findOrFail($subreddit->id);

        return view('subreddit/show')->with('subreddit', $subreddit);
    }

And then, in your view (I trimmed out all of the form stuff for now because it isn't doing anything):

@foreach($subreddit->posts as $post)
        <div class="row">
            <div class="span8">
                <div class="row">
                    <div class="col-md-12">
                        <h4><strong><a href="#"></a></strong></h4>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-1">
                        <div class="upvote topic" data-post="{{ $post->id }}">
                            <a class="upvote vote upvote-on" data-value="1"></a>
                            <!-- Notice how we set the sum of the votes for this post here -->
                            <span class="count">{{ $post->votes->sum('value') }}</span>
                            <a class="downvote vote" data-value="-1"></a>
                        </div>
                    </div>
                    {{-- The rest of the code was omitted..--}}
                </div>
            </div>
        </div>
    @endforeach 

Also, I strongly suggest simply having an index of Posts for each Subreddit and then having the voting logic on a separate Post show.blade.php. That will improve performance quite a bit and make a lot of these issues much easier to solve. For example, you won't need some sort of $votes array to see if the user voted on one of the Posts, you can simply query if the authenticated user's ID is in the returned votes collection.

I may try to tinker around with this more later but yeah, brush up on the basics before trying to dive into a project like this. Just bring it all back to the basics, play with Eloquent relations by just using dd() or returning output from queries to see how it all fits together.

ufodisko's avatar

@opheliadesign That's not what I meant by persisting user votes. I know I am not pulling the votes' count and always displaying 0. What I meant was if I had already upvoted/downvoted a post, I want to see the arrow in orange when I reload the page and that's what I have attempted to do in the view. To add the class upvoted-on if it's already that.

Again, I do not want to display the count of the votes for each post, I just want to show the user that he already upvoted this post by turning the arrows orange.

And I am not sure how would the voting work now that you have removed the forms. Don't I need any forms with AJAX requests?

opheliadesign's avatar

@ufodisko No, you do not need a form with Ajax. However, you will need to set the csrf_token for your Ajax calls - this is how I normally handle it:

In the <head> section, place:

<meta name="csrf-token" id="csrf-token" content="{{ csrf_token() }}">

Then, in jQuery,

var csrf_token = $('meta[name="csrf-token"]').attr('content');
$.ajaxPrefilter(function(options, originalOptions, jqXHR){
    switch (options['type'].toLowerCase()) {
        case "post":
        case "delete":
        case "put":
            // add leading ampersand if `data` is non-empty
            if (options.data != '') {
                options.data += '&';
            }
            // add _token entry
            options.data += "_token=" + csrf_token;
            break;
    }
});

Then you can do things like:

$.post('/votes', {id: postId, vote: 1});
$.ajax({
    url: '/votes',
    type: 'DELETE',
    data: {id: postId},
    dataType: 'JSON',
    success: function(result) {
        // Do something with the result
    }
});

To display if someone already voted up or down and adjust that class, again I would suggest displaying the voting only on the main page for a Post. That's how Stack Overflow and Laracasts does it, right? It will make all of this much simpler.

You can accomplish this either in your Blade view by figuring out some sort of way to check if a vote with the logged in user's id exists in the votes collection and then checking its value (negative or positive) or perhaps by making a separate query using Ajax once the page has loaded - something like:

public function checkVote(Request $request)
{
   $vote = \App\Vote::wherePostId($request->input('id'))->whereUserId(\Auth::id())->first();
    $status = $vote ? ($vote->value > 0 ? 'upVote' : 'downVote') : 'noVote';

    return response()->json($status);
}

and then handle the logic of setting the classes of the buttons in your Javascript.

opheliadesign's avatar

@ufodisko Using my prior Blade code and eager loading in the Controller, this will work for changing your class. It's a bit long and messy, perhaps someone can suggest an improvement but it works.

<div class="upvote topic" data-post="{{ $post->id }}">
                            <a class="upvote vote {{ $post->votes && $post->votes->contains('user_id', Auth::id()) ? ($post->votes->where('user_id', Auth::id())->first()->value > 0 ? 'upvote-on' : null) : null}}" data-value="1"></a>
                            <!-- Notice how we set the sum of the votes for this post here -->
                            <span class="count">{{ $post->votes->sum('value') }}</span>
                            <a class="downvote vote {{ $post->votes && $post->votes->contains('user_id', Auth::id()) ? ($post->votes->where('user_id', Auth::id())->first()->value < 0 ? 'downvote-on' : null) : null}}" data-value="-1"></a>
                        </div>
ufodisko's avatar

@opheliadesign I am using the Form::open() facade which automatically creates a hidden field with the name _token and generates it with a random string.

Then in my ajax request, I am already including the token with this

$.ajaxSetup({
      headers: {
              'X-CSRF-TOKEN': $('[name="_token"]').val()
      }

And I'm already getting the user's votes on post with this

$votes = Auth::user()->votes()->get()->toArray();

doing a dd($votes) will return an array of all the votes by the logged in user, like this

array:2 [▼
  0 => array:6 [▼
    "id" => 110
    "value" => 1
    "user_id" => 2
    "post_id" => 10
    "created_at" => "2015-09-20 20:02:07"
    "updated_at" => "2015-09-20 20:02:07"
  ]
  1 => array:6 [▼
    "id" => 111
    "value" => 1
    "user_id" => 2
    "post_id" => 10
    "created_at" => "2015-09-20 20:02:19"
    "updated_at" => "2015-09-20 20:02:19"
  ]
]
opheliadesign's avatar

@ufodisko I get what you're trying to accomplish, I'm just trying to tell you that there are better ways to go about it. :) It's just my advice for whatever it's worth from my experience.

Creating a form just for the CSRF token and then using Ajax is not the standard way of going about it. I generally have the jQuery Ajax setup code I posted previously added to every page in a site before any other scripts that use Ajax.

Imagine that you have a user that has thousands of Posts that they voted on and you're just trying to see if they voted on the five or so you're displaying for a Subreddit. That's a huge hit to performance when you can do something like what I suggest above, using eager loading and Laravel's Collection functions - http://laravel.com/docs/5.1/collections. Does that make sense?

ufodisko's avatar

@opheliadesign yes, that makes perfect sense. Thank you.

However, I still need to figure out how to complete the voting system even in the wrong way before I start optimizing my code.

Btw I am following 'Laravel 5 Fundamentals` series in how to do things.

ufodisko's avatar

@opheliadesign I just tried your snippets, it did not work.

I only tried getting the sum of the votes and persisting user's vote by changing the css class. Neither worked.

opheliadesign's avatar

@ufodisko everything working for me.. I'm working on trying to fork your project (Github isn't my strong suit).

I have the voting working without multiple entries per user.

Next

Please or to participate in this conversation.