node.jsmongodbangularexpressvote

Why are vote options not correctly pre-selected?


You need to be authenticated to vote on a poll. When you vote on a poll, there are 2 issues:

  1. You can vote an infinite number of times until you leave or reload the page

  2. When you reload the page, you are finally prevented from voting but instead of having the option you voted on pre-selected, it's always the second option that is preselected.


WHAT SHOULD HAPPEN:

Sign up/ Login, then vote on a poll. The moment you click on an option, the poll voting choice you made is locked and you can't vote more on that poll.


HOW THE FLOW OF MY CURRENT CODE WORKS:

When a user clicks on an option, the counter is increased and the vote is then saved in an array that is pushed to the User object in the database.

When the component loads, the database data for votes for the currently logged in user is saved through the ngOninit() method inside the local votes variable which is then used to check on which poll the user already voted and what vote he made. The issue is that the choice is made is always choice2 when that is not actually the case.

I understand why you can vote many times until the page reloads, but I just don't know how to immediately lock the poll after the user voted, on the client and on the backend (prevent more votes from being registered if user already voted on poll).

As for why it is already the second choice that is pre-selected, I have no idea.


CODE:

HTML

<div class="formWidth">
    <form (ngSubmit)="onSubmit(f)" #f="ngForm">
        <div class="form-group">
            <label class="inputTitle" for="title">Poll Title</label>
            <input
                    type="text"
                    id="title"
                    class="form-control"
                    [ngModel]="poll?.title"
                    name="title"
                    required maxlenth="30">
            <label class="inputTitle" for="choice1">Choice1</label>
            <input
                    type="text"
                    id="choice1"
                    class="form-control"
                    [ngModel]="poll?.choice1"
                    name="choice1"
                    required maxlenth="20">
            <label class="inputTitle" for="choice2">Choice2</label>
            <input
                    type="text"
                    id="choice2"
                    class="form-control"
                    [ngModel]="poll?.choice2"
                    name="choice2"
                    required maxlenth="20">
        </div>
        <button type="button" class="btn btn-danger" (click)="onClear(f)">Clear</button>
        <button class="btn btn-primary" type="submit">Save</button>
    </form>
</div>

COMPONENT

export class PollComponent {
    @Input() poll: Poll;

    constructor(private pollService: PollService) {}

    votes: any;

    // Pie
    public pieChartLabels:string[] = [];
    public pieChartData:number[] = [];
    public pieChartType:string = 'pie';
    public pieChartOptions:any = {};

    ngOnInit() {
        var result1 = parseFloat(((this.poll.counter1/(this.poll.counter2+this.poll.counter1))*100).toFixed(2));
        var result2 = parseFloat(((this.poll.counter2/(this.poll.counter2+this.poll.counter1))*100).toFixed(2));
        this.pieChartData = [result1, result2];
        this.pieChartLabels = [this.poll.choice1, this.poll.choice2];
        this.pieChartType = 'pie';
        this.pieChartOptions  = {
            tooltips: {
            enabled: true,
            mode: 'single',
                callbacks: {
                    label: function(tooltipItem, data) {
                        var allData = data.datasets[tooltipItem.datasetIndex].data;
                        var tooltipLabel = data.labels[tooltipItem.index];
                        var tooltipData = allData[tooltipItem.index];
                        return tooltipLabel + ": " + tooltipData + "%";
                    }
                }
            }
        }

        this.pollService.voted(localStorage.getItem('userId')).subscribe(
            data => {
                var result = JSON.parse(data);
                this.votes = result.votes;
            },
            err => { console.log("NGONINIT ERROR: "+ err) },
            () => { }
        );
    }

    onEdit() {
        this.pollService.editPoll(this.poll);
    }

    onDelete() {
        this.pollService.deletePoll(this.poll)
            .subscribe(
                result => console.log(result)
            );
    }

    onChoice1() {
      this.pollService.increaseCounter1(this.poll);
      this.onVote1();
      var result1 = parseFloat(((this.poll.counter1/(this.poll.counter2+this.poll.counter1))*100).toFixed(2));
      var result2 = parseFloat(((this.poll.counter2/(this.poll.counter2+this.poll.counter1))*100).toFixed(2));
      this.pieChartData = [result1, result2];
    }

    onChoice2() {
      this.pollService.increaseCounter2(this.poll);
      this.onVote2();
      var result1 = parseFloat(((this.poll.counter1/(this.poll.counter2+this.poll.counter1))*100).toFixed(2));
      var result2 = parseFloat(((this.poll.counter2/(this.poll.counter2+this.poll.counter1))*100).toFixed(2));
      this.pieChartData = [result1, result2];
    }

    onVote1() {
      this.pollService.voteOn(this.poll.pollID,  localStorage.getItem('userId'), 1);
    }

    onVote2() {
      this.pollService.voteOn(this.poll.pollID,  localStorage.getItem('userId'), 2);
    }

    belongsToUser() {
        return localStorage.getItem('userId') == this.poll.userId;
    }

    alreadyVotedFor(choice: number) {
      var result = "";
      if (this.votes) {
          for (var i = 0; i < this.votes.length; i ++) {
              if (this.votes[i].pollID == this.poll.pollID) {
                  result = "disabled";
                  if (this.votes[i].choice == choice) {
                      result =  "selected";
                  }
              }
          }
      }
      return result;
    }

        // events
    public chartClicked(e:any):void {

    }

    public chartHovered(e:any):void {

    }

}

SERVICE

updatePoll(poll: Poll) {
        const body = JSON.stringify(poll);
        const token = localStorage.getItem('token')
            ? localStorage.getItem('token')
            : '';
        const headers = new Headers({
          'Content-Type': 'application/json',
          'Authorization': 'Bearer '+token
        });
        return this.http.patch('https://voting-app-10.herokuapp.com/poll/' + poll.pollID, body, {headers: headers})
            .map((response: Response) => response.json())
            .catch((error: Response) => {
                this.errorService.handleError(error.json());
                return Observable.throw(error);
            });
    }

    increaseCounter1(poll: Poll) {
        poll.counter1++;
        const body = JSON.stringify(poll);
        const token = localStorage.getItem('token')
            ? localStorage.getItem('token')
            : '';
        const headers = new Headers({
          'Content-Type': 'application/json',
          'Authorization': 'Bearer '+token
        });
        this.http.patch('https://voting-app-10.herokuapp.com/poll/vote/' + poll.pollID, body, {headers: headers})
            .map((response: Response) => response.json())
            .catch((error: Response) => {
                this.errorService.handleError(error.json());
                return Observable.throw(error);
            })
            .subscribe();
    }

    increaseCounter2(poll: Poll) {
        poll.counter2++;
        const body = JSON.stringify(poll);
        const token = localStorage.getItem('token')
            ? localStorage.getItem('token')
            : '';
        const headers = new Headers({
          'Content-Type': 'application/json',
          'Authorization': 'Bearer '+token
        });
        return this.http.patch('https://voting-app-10.herokuapp.com/poll/vote/' + poll.pollID, body, {headers: headers})
            .map((response: Response) => response.json())
            .catch((error: Response) => {
                this.errorService.handleError(error.json());
                return Observable.throw(error);
            })
            .subscribe();
    }

    voteOn(pollID: string, userID: string, choice: number) {
      var user;
      this.http.get('https://voting-app-10.herokuapp.com/user/'+userID)
      .map(response => response.json())
      .subscribe(
            json => {
              user = JSON.parse(json);
              if (user.votes == undefined) {
                user.votes = [{pollID, choice}];
              } else {
                user.votes.push({pollID, choice});
              }
              const body = user;
              const token = localStorage.getItem('token')
                  ? localStorage.getItem('token')
                  : '';
              const headers = new Headers({
                'Content-Type': 'application/json',
                'Authorization': 'Bearer '+token
              });
              return this.http.patch('https://voting-app-10.herokuapp.com/user/', body, {headers: headers})
                  .map((response: Response) => response.json())
                  .catch((error: Response) => {
                      this.errorService.handleError(error.json());
                      return Observable.throw(error);
                  })
                  .subscribe();
            }
       )
    }

    voted(userID: string) {
        const headers = new Headers({'Content-Type': 'application/json'});
        return this.http.get('https://voting-app-10.herokuapp.com/user/'+userID,{headers: headers})
                        .map(response => response.json())
                        .catch((error: Response) => {
                            this.errorService.handleError(error.json());
                            return Observable.throw(error);
                        });
    }

ROUTE (BACKEND)

router.patch('/vote/:id', function (req, res, next) {
    var decoded = jwt.decode(getToken(req));
    Poll.findById(req.params.id, function (err, poll) {
        if (err) {
            return res.status(500).json({
                title: 'An error occurred',
                error: err
            });
        }
        if (!poll) {
            return res.status(500).json({
                title: 'No Poll Found!',
                error: {poll: 'Poll not found'}
            });
        }
        poll.title = req.body.title;
        poll.choice1 = req.body.choice1;
        poll.choice2 = req.body.choice2;
        poll.counter1 = req.body.counter1;
        poll.counter2 = req.body.counter2;

        poll.save(function (err, result) {
            if (err) {
                return res.status(500).json({
                    title: 'An error occurred',
                    error: err
                });
            }
            res.status(200).json({
                poll: 'Updated poll',
                obj: result
            });
        });
    });
});

router.patch('/:id', function (req, res, next) {
    var decoded = jwt.decode(getToken(req));
    Poll.findById(req.params.id, function (err, poll) {
        if (err) {
            return res.status(500).json({
                title: 'An error occurred',
                error: err
            });
        }
        if (!poll) {
            return res.status(500).json({
                title: 'No Poll Found!',
                error: {poll: 'Poll not found'}
            });
        }
        if (poll.user != decoded.user._id) {
            return res.status(401).json({
                title: 'Not Authenticated',
                error: {poll: 'Users do not match'}
            });
        }
        poll.title = req.body.title;
        poll.choice1 = req.body.choice1;
        poll.choice2 = req.body.choice2;
        poll.counter1 = req.body.counter1;
        poll.counter2 = req.body.counter2;

        poll.save(function (err, result) {
            if (err) {
                return res.status(500).json({
                    title: 'An error occurred',
                    error: err
                });
            }
            res.status(200).json({
                poll: 'Updated poll',
                obj: result
            });
        });
    });
});

Solution

  • Okay, at first your radio buttons don't get disabled, because you don't update the votes-array in your poll.component.ts after saving a vote.

    I'm not sure if it's a good solution or not:

    In your poll.service.ts:

    voteOn(pollID: string, userID: string, choice: number) {
        var user;
        return new Promise((resolve) => { //Create a new promise to wrap the Subscriptions
        this.http.get('http://localhost:3000/user/' + userID)
            .map(response => response.json())
            .subscribe(
            json => {
                user = JSON.parse(json);
                if (user.votes == undefined) {
    
                ...
    
                    .catch((error: Response) => {
                        this.errorService.handleError(error.json());
                        return Observable.throw(error);
                    }).subscribe(() => {
                        resolve(user.votes); // <- Resolve your promise
                    })
            }
            )
            });
    }
    

    And in your poll.component.ts:

    voteOn(...).then((votes) => {
        this.votes = votes; // To update your votes array
        this.updateVote();
    })
    

    And I don't recommend to call functions in bindings, because it happens that the functions are called very often "to detect changes", like in your components. So I would change the code in following way:

    In your poll.component.ts:

    vote:any //Added to your poll component
    
    
    updateVote() {
        this.vote = this.votes.find((vote) => {
            return vote.pollID === this.poll.pollID;
        });
    }
    

    You need to call this method in your ngOnInit method:

        this.pollService.voted(localStorage.getItem('userId')).subscribe(
            data => {
                var result = JSON.parse(data);
                this.votes = result.votes;
                this.updateVote(); // <- To select the vote of this poll
            },
            err => { console.log("NGONINIT ERROR: " + err) },
            () => { }
        );
    

    And in your poll.component.html:

        <fieldset [disabled]="vote">
          {{ poll.counter1 }} votes <input type="radio" id="{{ poll.choice1 }}" name="my_radio" value="{{ poll.choice1 }}" (click)="onChoice1(form)" [checked]="vote?.choice == 1">  {{ poll.choice1 }}
          <br>
          {{ poll.counter2 }} votes <input type="radio" id="{{ poll.choice2  }}" name="my_radio" value="{{ poll.choice2 }}" (click)="onChoice2(form)" [checked]="vote?.choice == 2">  {{ poll.choice2 }}
        </fieldset>
    

    But if you don't want to change your code in such a way, please tell me, so i can provide another solution.