Poll webpart using REST and Angularjs in SharePoint 2013

Poll-SharePoint 2013 - 2 by Deepak Virdi

Poll web part will allow users to select one out of 2 to 5 choices and see the results right away, and cannot vote twice for same Poll question.

There is no OOTB Poll webpart

In SharePoint you can use Survey List which is not an ideal solution where you have to just ask a question and want one answer from multiple choices.

There are few available in SharePoint App store but most are paid and other I don’t like, so decided to create mine own as it should be easy right, well almost.

So here is the output we are trying to achieve:

Poll-SharePoint 2013 - 2 by Deepak Virdi

Requirements:

  • User can vote only once for one poll
  • User should see results right away as soon he/she votes
  • If already voted, show result view
  • The result should show the total votes and % of each vote
  • The Web Part should be responsive
  • The poll can have max 5 and minimum of 2 choices
  • Adding question and answers should be super easy for authors

Solution:

We decided to use REST & Angularjs to accomplish this task and using SP List as database as it was internal weekly Poll for our landing page.

You can add angularjs, jquery in masterpage, page layout or just local for this web part.

So let’s create database first, I mean custom lists 🙂

We need to Create three lists:

  1. Poll Questions
  2. Poll Logs
  3. Poll Symbols (You can omit this list if don’t want to use icons in Title, I love to add little things to play with)

The columns for Poll Questions:

This list will have Question and particular answers. Active (Yes/No) will decide which Poll is currently active and can be automated using workflow or manually. But only top one question will shown in webpart. The columns with count in name will have to count of the answers and total count of votes.

Symbol is a lookup column based on list Poll Symbols title.

Column Name Type Required
Question Single line of text True
Answer1 Single line of text True
Answer2 Single line of text True
Answer3 Single line of text False
Answer4 Single line of text False
Answer5 Single line of text False
Symbol Lookup True
Active Yes/No True
CountAnswer1 Number False
CountAnswer2 Number False
CountAnswer3 Number False
CountAnswer4 Number False
CountAnswer5 Number False
TotalCount Number False

The columns for Poll Logs:

This list will keep record of the users who has voted for particular Poll.

Column Name Type Required
Title Single line of text False
QuestionId Single line of text False
SelectedAnswer Single line of text False
loginid Single line of text False
userid Number False

The columns for Poll Symbols:
This list will be lookup for Poll Question and contains the name and value of Font Awesome icons

Column Name Type Required
Title Single line of text True
Value Single line of text True

The list will look like:

Title Value
Globe fa-globe
Jets fa-fighter-jet

Now our database is ready let’s do some coding.

Here is the process I’m following:

Flowchart

The HTML you need is following. I added it as Script Editor webpart and paste it in it.

This solution is using Bootstrap CSS.


 <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
 <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
 <script src="/sites/Style%20Library/js/main.js"></script>

    <div ng-app="ng-app-polls">
        <div ng-controller="pollSubmitCtrl">
            <div>
                <div class="panel panel-primary" ng-show="poll.hasData">
                    <div class="panel-heading">
                        <h3 class="panel-title"><i class="fa fa-lg {{poll.Question.Symbol}}"></i>{{poll.Question.Text}}</h3>
                    </div>
                    <div ng-show="!poll.result">
                        <div class="panel-body">
                            <ul class="list-group ulpoll">
                                <li class="list-group-item"
                                    ng-repeat="answer in poll.Answers"
                                    ng-style="{
											'background-color' : $index == selectedIndex ? '#f5f5f5' : ''
											}"
                                    ng-click="selectedAnswer($index)">
                                    <div class="radio">
                                        <label>
                                            <input type="radio"
                                                ng-model="poll.selectedAnswer"
                                                ng-value="{{ answer.Id }}"
                                                name="radioPollAnswers">
                                            {{ answer.Text }}
                                        </label>
                                    </div>
                                </li>
                            </ul>
                        </div>
                        <div class="panel-footer text-center">
                            <button type="button"
                                class="btn btn-success btn-md"
                                ng-disabled="!poll.selectedAnswer"
                                ng-click="pollSubmit()">
                                <span class="fa fa-check-square-o fa-lg"></span>Vote
                            </button>
                        </div>
                    </div>
                    <div ng-show="poll.result">
                        <div class="panel-body">
                            <ul class="list-group ulpoll">
                                <li class="list-group-item"
                                    ng-repeat="answer in poll.Answers">
                                    <div class="pollAnswer">
                                        <span>{{ answer.Text }}</span><span class="pollPercent">{{answer.Percent}}</span>
                                    </div>
                                    <div class="progress">
                                        <div class="progress-bar" role="progressbar" aria-valuenow="{{ answer.Count }}" aria-valuemin="0" aria-valuemax="100" ng-style="{'width': answer.Percent}">
                                            <span class="sr-only">{{answer.Percent}}</span>
                                        </div>
                                    </div>
                                </li>
                            </ul>
                        </div>
                        <div class="panel-footer">
                            <div ng-show="poll.result">
                                <p class="bg-primary">Total - {{ poll.Question.Total }}</p>
                            </div>
                        </div>
                    </div>
                </div>
                <div ng-show="!poll.hasData">
                    <div class="alert alert-danger" role="alert">No Active Poll for now.</div>
                </div>
            </div>
        </div>
    </div>

Add the following code in main.js. I’m not going to go in details of angular code or there may be better way of doing it. I’m also learning it day by day, this works for me.


var PollQuestionslistName = 'Poll Questions';
var PollLogslistName = 'Poll Logs';

var appPollResults = angular.module('ng-app-polls', []);

//Controller
appPollResults.controller('pollSubmitCtrl', function ($scope, pollResultsService) {
    $scope.poll = [];

    $scope.getUserDetails = function (pollData) {
        pollResultsService.getUserDetailById(pollData.Question.Id)
        .then(
        function (isSubmitted) {

            pollData.result = isSubmitted;
            $scope.poll = pollData;
        }
        );
    }

    $scope.getPollsData = function () {
        pollResultsService.getPollDetails(PollQuestionslistName)
        .then(
        function (pollData) {
            $scope.getUserDetails(pollData);
        }
        );
    }

    $scope.pollSubmit = function () {

        pollResultsService.getLoginName()
        .then(
        function (userLogin) {
            pollResultsService.setUserDetailById($scope.poll.Question.Id, $scope.poll.selectedAnswer, userLogin)
            .then(
            function (success) {
                if (success) {
                    pollResultsService.getItemById($scope.poll.Question.Id)
                    .then(
                    function (data1, pollData) {
                        var selectedAns = $scope.poll.selectedAnswer;
                        pollResultsService.setPollDetails(selectedAns, data1, pollData)
                        .then(
                        function (success) {
                            if (success) {
                                pollResultsService.getPollDetails(PollQuestionslistName)
                                .then(
                                function (pollData) {
                                    pollData.result = true;
                                    $scope.poll = pollData;
                                });
                            }
                        }
                        );
                    }
                    );
                } else {
                    alert('unknown error occured');
                }
            });
        });
    }

    $scope.getPollsData();

    $scope.selectedIndex = null;
    $scope.selectedAnswer = function (index) {
        $scope.selectedIndex = index;
    }

});

//Services
appPollResults.service(
        'pollResultsService',
        function ($http, $q) {
            return ({
                getPollDetails: getPollDetails,
                setPollDetails: setPollDetails,
                setUserDetailById: setUserDetailById,
                getUserDetailById: getUserDetailById,
                getItemById: getItemById,
                getLoginName: getLoginName
            });

            //Get the Poll Question and Answers
            function getPollDetails() {

                var fullUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + PollQuestionslistName + "')/Items?$Select=";
                fullUrl += 'ID,Title,Answer1,Answer2,Answer3,Answer4,CountAnswer1,CountAnswer2,CountAnswer3,CountAnswer4,TotalCount,Symbol/Value&$expand=Symbol&$orderby=ID desc&$filter=Active eq 1&$Top=1';

                var request = $http({
                    method: 'Get',
                    url: fullUrl,
                    headers: { "accept": "application/json;odata=verbose" }
                });

                return (request.then(successGetPollDetails, handleError));
            }

            //Success Poll Details
            function successGetPollDetails(data) {
                var pollData;
                if (data.data.d.results.length > 0) {
                    var result = data.data.d.results[0];
                    var percent1 = ((result.CountAnswer1 / result.TotalCount) * 100).toFixed(0) + '%';
                    var percent2 = ((result.CountAnswer2 / result.TotalCount) * 100).toFixed(0) + '%';
                    var percent3 = ((result.CountAnswer3 / result.TotalCount) * 100).toFixed(0) + '%';
                    var percent4 = ((result.CountAnswer4 / result.TotalCount) * 100).toFixed(0) + '%';
                    var percent5 = ((result.CountAnswer5 / result.TotalCount) * 100).toFixed(0) + '%';

                    pollData = angular.fromJson({
                        "hasData": true,
                        "result": false,
                        "Question": {
                            "Id": result.ID,
                            "Text": result.Title,
                            "Symbol": result.Symbol.Value,
                            "Total": result.TotalCount
                        },
                        "Answers": [{
                            "Id": 1,
                            "Text": result.Answer1,
                            "Count": result.CountAnswer1,
                            "Percent": percent1
                        }, {
                            "Id": 2,
                            "Text": result.Answer2,
                            "Count": result.CountAnswer2,
                            "Percent": percent2
                        }]
                    });

                    if (result.Answer3) {
                        var obj = {};
                        obj["Id"] = 3;
                        obj["Text"] = result.Answer3;
                        obj["Count"] = result.CountAnswer3;
                        obj["Percent"] = percent3;
                        pollData.Answers.push(obj);
                    }
                    if (result.Answer4) {
                        var obj = {};
                        obj["Id"] = 4;
                        obj["Text"] = result.Answer4;
                        obj["Count"] = result.CountAnswer4;
                        obj["Percent"] = percent4;
                        pollData.Answers.push(obj);
                    }
                    if (result.Answer5) {
                        var obj = {};
                        obj["Id"] = 5;
                        obj["Text"] = result.Answer5;
                        obj["Count"] = result.CountAnswer5;
                        obj["Percent"] = percent5;
                        pollData.Answers.push(obj);

                    }
                } else {
                    pollData = angular.fromJson({
                        "hasData": false,
                        "result": false,
                        "Question": {
                            "Id": 0,
                            "Text": "",
                            "Symbol": "",
                            "Total": 0
                        },
                        "Answers": [{}]
                    });
                }
                return (pollData);
            }

            //Set the Poll Details
            function setPollDetails(selectedAnsId, data1) {

                var fullUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + PollQuestionslistName + "')/items";
                var itemType = "SP.Data." + PollQuestionslistName + "ListItem";

                var selAns = 'CountAnswer' + selectedAnsId;
                var value = '';
                var cat = {
                    '__metadata': { "type": itemType }
                };

                if (selectedAnsId == '1') {
                    value = data1.data.d.CountAnswer1;
                } else if (selectedAnsId == '2') {
                    value = data1.data.d.CountAnswer2;
                } else if (selectedAnsId == '3') {
                    value = data1.data.d.CountAnswer3;
                } else if (selectedAnsId == '4') {
                    value = data1.data.d.CountAnswer4;
                else {
                    value = data1.data.d.CountAnswer5;
                }

                cat[selAns] = value + 1;
                cat['TotalCount'] = data1.data.d.TotalCount + 1;

                var request = $http({
                    url: data1.data.d.__metadata.uri,
                    method: "POST",
                    data: JSON.stringify(cat),
                    headers: {
                        "Accept": "application/json;odata=verbose",
                        "X-RequestDigest": $("#__REQUESTDIGEST").val(),
                        "X-HTTP-Method": "MERGE",
                        "Content-Type": "application/json;odata=verbose",
                        "If-Match": data1.data.d.__metadata.etag
                    }
                });


                return (request.then(successSetPollDetails, handleError));
            }

            function successSetPollDetails(data) {

                return true;
            }

            //Set the Poll Details
            function setUserDetailById(QuestionId, selectedAnswer, userLogin) {

                var fullUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + PollLogslistName + "')/items";
                var itemType = "SP.Data." + PollLogslistName + "ListItem";

                var cat = {
                    "__metadata": { "type": itemType },
                    "Title": userLogin.UserName,
                    "loginid": userLogin.UserId,
                    "userid": _spPageContextInfo.userId,
                    "SelectedAnswer":String(selectedAnswer),
                    "QuestionId": String(QuestionId)
                };

                var request = $http({
                    url: fullUrl,
                    method: "POST",
                    data: JSON.stringify(cat),
                    headers: {
                        "Accept": "application/json;odata=verbose",
                        "X-RequestDigest": $("#__REQUESTDIGEST").val(),
                        "Content-Type": "application/json;odata=verbose"
                    }
                });

                return (request.then(successSetUserDetailById, handleError));
            }

            function successSetUserDetailById(data) {
                return true;
            }

            function getUserDetailById(itemID) {

                var fullUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + PollLogslistName + "')/Items?$Select=";
                fullUrl += "Title,QuestionId&$filter=userid eq '" + _spPageContextInfo.userId + "' and QuestionId eq '" + itemID + "'&$top=1";

                var request = $http({
                    method: 'Get',
                    url: fullUrl,
                    headers: { "accept": "application/json;odata=verbose" }
                });

                return (request.then(successGetUserDetailById, handleError));
            }

            function successGetUserDetailById(data) {
                if (data.data.d.results.length > 0) {
                    return true;
                } else {
                    return false;
                }

            }

            function getItemById(itemID) {

                var fullUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + PollQuestionslistName + "')/Items('" + itemID + "')";

                var request = $http({
                    url: fullUrl,
                    Method: "GET",
                    headers: { "accept": "application/json;odata=verbose" },
                });

                return (request.then(successGetItemById, handleError));
            }

            function successGetItemById(data) {
                return data;
            }

            function getLoginName() {
                var userid = _spPageContextInfo.userId;
                var fullUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/getuserbyid(" + userid + ")";

                var request = $http({
                    url: fullUrl,
                    contentType: "application/json;odata=verbose",
                    Method: "GET",
                    headers: { "accept": "application/json;odata=verbose" },
                });

                return (request.then(successGetLoginName, handleError));
            }

            function successGetLoginName(data) {
                var lname = data.data.d.LoginName;
                lname = lname.split("|")[1];
                var userLogin = {
                    "UserName": data.data.d.Title,
                    "UserId": lname,
                    "Email": data.data.d.Email
                }
                return userLogin;
            }

            //Handle Error
            function handleError(response) {

                if (
                    !angular.isObject(response.data) ||
                    !response.data.message
                    ) {
                    return ($q.reject("An unknown error occurred."));
                }
                // Otherwise, use expected error message.
                return ($q.reject(response.data.message));
            }
        }
);

Well that’s it. If you find any issues or have question please leave a comment.

19 thoughts on “Poll webpart using REST and Angularjs in SharePoint 2013

  1. Hi Deepak,

    Great sharing, just that when I try to vote the poll I have bad request from angular js, what’s going on? The question loaded correctly just that it stuck with this message:
    [angular.min.js:103] [POST] [https://mysite/_api/web/lists/getbytitle(‘Poll%20Logs’)/items] 400 (Bad Request)

        1. For debugging I’ll put breakpoints at var fullUrl and open that url in IE before executing the function. This way we can omit if url is the issue. Bad request is mostly related to invalid url.
          You can use network tab of Chrome developer tool, to see the requests url and which one is failing.

          1. “{“error”:{“code”:”-1, Microsoft.SharePoint.Client.InvalidClientQueryException”,”message”:{“lang”:”en-US”,”value”:”A type named ‘SP.Data.Poll LogsListItem’ could not be resolved by the model. When a model is available, each type name must resolve to a valid type.”}}}”

            Looks like its looking for ListItemEntityTypeFullName ie: (SP.Data.Poll_x0020_LogsListItem)

          2. When you open the url: http://sitename.com/_api/web/lists/getbytitle('Poll Logs’)/Items
            Make sure the ListItemEntityTypeFullName is same as category term in following xml. I don’t have space in list name: SP.Data.PollLogsListItem
            It is based on internal name of list but rest works on display name

            category term=”SP.Data.PollLogsListItem” scheme=”http://schemas.microsoft.com/ado/2007/08/dataservices/scheme”

  2. Hey Deepak,

    Amazing Article, works super..!!
    Just one query- How do u handle the progress Bar, for me I can not see the progress bar although i can see the percentage, any special classes or angular version needs to be used for it?

    1. No errors in F12 Console log – just a couple warnings (http://imgur.com/7fvuPDq). If it simply didn’t work at all, that’s one thing but I don’t understand why it appears correctly in Edit mode.

      One other thing I noticed is that your instructions reference the List names with a space in them (Poll Questions vs PollQuestions) though your JS does not reference the space. In my case, I was following your instructions in order and created the list names with a space in them. I added a space in the main.js in Lines 1 & 2. Am I supposed to add a space anywhere else maybe?

    2. Also I noticed the list instructions reference a 5th Answer though I don’t see references to Answer5 and CountAnswer5 in the JS, not sure if that might be related. No other ng-app on the page (it’s a fresh team site template home page)

      1. If your list is with space you added it to correct place. Answer5 is just a placeholder if we need it.
        If have attached the files along with list templates https://goo.gl/R2fPso

        The error in Display is mainly because I believe something is blocking the Jquery or AngularJs http://imgur.com/9OruyUo. Use Chrome and use it’s developer tool (F12), add debugger to js file and use F10 to go step by step.

        Do you have internet access on your dev machine? if not or due to proxy setting, just download the jquery & angularjs and add it to Style Library and reference it from there.

        1. Tried the following:
          – Downloaded local copies of jquery and angular and uploaded them to site and changed reference
          – Deleted my 3 lists and then uploaded your STPs to List Templates and then created 3 new lists based off templates. Created lists using names without a space to exactly match your code

          After all that, I see the same behavior – one exception is Poll no longer appears correctly when in Edit mode. Looks the same in either mode (not working).

          1. Well that’s a bummer, same code is working in different environment for me. Anyways I’ll strongly suggest you should create first simple angularjs app and see if it works. I think is more related to angular or jquery not loading properly than the code. Try this one http://www.tutorialspoint.com/angularjs/angularjs_first_application.htm
            Also see if Minimum Download feature is activated. Sometimes it also won’t load the files properly.

            Also see using F12 the files are downloading properly http://imgur.com/1BOp8xe

            Add a debugger on angularjs & jquery and see if browser stops on break point (use chrome)

  3. Thanks for posting this – I have implemented this code in my SharePoint 2013 On Premise site (and changed the reference to the main.js to point to mine). This isn’t quite working for me. Here’s what I see on the page:

    When viewing page normally > http://imgur.com/H0heLpH

    But when I go to Edit mode, the Poll appears correctly…? > http://imgur.com/p5NBXVe

    Any idea what’s going on?

Leave a Reply

Your email address will not be published. Required fields are marked *