Introduction
What’s the idea? To create SharePoint app that’s going to show Dilbert comics and to allow SharePoint users to rate those comics.
What’s the solution? SharePoint hosted app which uses AngularJS to fetch and display daily Dilbert comics, and AngularJs and JSOM (Javascript object model) for reading and writing to SharePoint app web.
So, here’s the scenario:
- Dilbert comics rss feed is on the web, out of SharePoint
- SharePoint hosted app (JavaScript only) will fetch this feed and display comics
- SharePoint app web will have custom list with ratings
- Ratings will be displayed for each comic
- SharePoint users would have possibility to rate each comic
- AngularJs would be used as framework for developing app (fetching and displaying data)
Background
Last month I wrote an article about using Visual Studio, JqPlot and AngularJS for creating Charting SharePoint Hosted app. It was a simple app that reads data from host web and renders chart based on that data. I was not happy with some of the solutions there (all Javascript was in one file, angular service overlapped with controller, it was not easy to read or maintain). This is a step forward. In this article I’m going to show you how to build SharePoint hosted app with hybrid data source (rss feed and SharePoint) list.
Solution
Sharepoint App
App was created using Visual Studio 2013. Process was completely equal as in my previous article, new SharePoint project (App for SharePoint 2013), type of app is SharePoint hosted app. This app does not need any special permissions in App manifest.
Data
SharePoint list
As I said before comic ratings will be held in the SharePoint custom list named DilbertRating. The idea is to have one list row for each comic (one comic a day). Each vote (rating from one to 10) will be summed up in a column named Rating, and number of voters (their count value) will be held in column named Votes. So average rating for a comic will be result of dividing of two columns: Rating and Votes. For the simplicity of the solution there is no checking if particular user has already voted, so one can vote unlimited number of times.
The list consists of 5 columns:
- Title – default system column of type text
- Rating – custom column, type of number (used for storing summed votes)
- Votes – custom column, type of number (used for storing votes count)
- Strip Id – custom column, type of text (used for storing comic title aka “Dilbert 2014-04-24”)
- Strip Date – existing column, type of date, used for storing comic date
Column creation
Columns Rating, Votes and StripId were made in the app. It’s pretty straightforward process. Right click on project’s name and select add new item. In new item dialog select Site Column (picture below)
For column Rating I changed type to Number, and group to CPU in VS2013 XML editor.
="1.0"="utf-8"
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Field
ID="{e284742d-784e-4e28-b909-3ea081b483f6}"
Name="Rating"
DisplayName="Rating"
Type="Number"
Required="FALSE"
Group="CPU">
</Field>
</Elements>
I did same operations for column Votes.
For column StripId I changed only group to CPU.
List Instance
Right click on project’s name and select add new item. In new item dialog select List. On the next screen (picture below) enter name DilbertRating and leave first option (Create a customizable list template…) selected. Click Finish.
List designer form will open. Title column is already added to our list. What we need to do is to add columns we previously added (Rating, Votes and StripId) to our list. Also we will add column Article date to list, but for the purpose of this project we will rename it to the “Strip Date”. So Display name of this column will be “Strip Date” and internal name will stay ArticleStartDate. At the end our list should be looking like on the picture below:
That’s all our list is ready.
Conclusion: when someone installs our app this list will be created in the app web. That’s why no additional permissions are required.
Note: Double check app feature. Columns, list template and list instance definition should be in items in the feature panel of the feature designer.
Using the code aka Fun Part
We will use angular (angular-sanitize.min.js and angular.js) and bootstrap from the third party tools. I put them into the lib folder.
Also we will add Controllers.js and Service.js files into the Scripts folder of the app.
Jquery and SharePoint stuff are included by default.
Architecture
There will be one controller DilbertAppCtrl, in Controllers.js in Scripts folder. Behavior of our app would be defined here.
In Service.js we will define two services: FeedService and SPService.
The responsibility of FeedService is communication with the Dilbert’s rss feed and fetching data. FeedService is pretty generic. It is not aware of the feed url. Url of the feed will be injected in the moment of calling service method. So this service could be used in other situations too.
Other service is SpService. Its responsibility is communication with SharePoint (reading ratings from the SP list, updating list with new ratings).
App view will be defined in Default.aspx page in Pages folder.
Script file App.js will glue up the whole app.
View
Plan is to display one comic at the time. So our layout will look something like this: comic title, comic image, date, rating info with option to vote and navigation buttons at the bottom.
Our view is simple, inside PlaceHolderMain content placeholder we have:
<div class="container-fluid" ng-app="DilbertApp">
<div data-ng-controller="DilbertAppCtrl">
<div class="row-fluid"><h4>Dilbert demo</h4></div>
<div class="row-fluid">
<h5><a target="_blank" href="{{currItem.link}}">{{currItem.title}}</a></h5> <p class="text-left">{{currItem.contentSnippet}}</p>
<span class="small">{{currItem.publishedDate}}</span>
<div ng-bind-html="currItem.content"></div>
Rating is {{currItemRating}} <br/>
<div fundoo-rating rating-value="currItemRating" max="10"
on-rating-selected="saveRatingToServer(rating)"></div>
<button type="button" ng-click="prev()" ng-show="position > 0">Prev</button>
<button type="button" ng-click="next()" ng-show="position < feeds.length-1">
Next</button>
</div>
</div>
</div>
Because we are creating app for SharePoint we can’t add ng-app directive to the html element so everything is inside div element in the Main placeholder. So first div defines our app – RSSFeedApp.
In the second one we are defining that we’re going to use FeedCtrl controller.
Then we have template. We are fetching the list of comics from the web but we are displaying one at a time. Object currItem will held the current selected item (comic). For this current item we will display:
- It’s title (currItem.title)
- Link to the dilbert web site (currItem.link)
- Comic date (currItem.publishedDate)
- Image (currItem.content)
- Rating currItemRating
Navigation buttons are at the bottom. There are two of them, one previous and one next button. They have click event (move back and forth) and ng-show directive. Prev button is displayed only if the position is bigger than 0 (0 is first position) and Next button is displayed only if the position property is lesser than comics count minus 1.
Few words about rating directive. We are using fundoo rating for that purpose. Here is markup for rating:
<div fundoo-rating rating-value="currItemRating" max="10" on-rating-selected="saveRatingToServer(rating)"></div>
We’re binding curritemRating (Rounded integer of Rates/Number of votes division) property to it. We’re setting max value of 10, and event saveRatingToServer to be called when someone rates the comic.
Controller
DilbertAppCtrl controller has following properties:
- Feeds – collection of dilbert comics
- feedSrc - url to the Dilber rss feed
- position – because we are displaying one comic at the time we need position property, to know what comic we are currently displaying
- currItem – current comic object that is displayed
- currItemRating – rating of the currently selected comic
- ratings – collection of ratings, that was read from the SharePoint list
Besides properties our controller has following methods:
- GetFeeds - method that calls FeedService’s parseFeed method and fetches comics from the rss fees into the feeds collections. It also sets current item (currItem) to the first object in the collection.
- GetRatings – method that calls SpService’s GetData method. It fetches last 14 ratings (because rss feed holds last 14 comics) from SharePoint into ratings collection. Also this method refreshes ratings for the current item when data fetching is done.
- SetRatings – method that is responsible for calculating rating of the currently displayed item.
-
saveRatingToServer – method responsible for saving new ratings. It calls methods (defined in SpService) for saving data in the SharePoint list.
-
$scope.saveRatingToServer = function (rating) {
$window.alert('Rating selected - ' + rating);
var stripId=$scope.feeds[$scope.position].title;
SpService.CheckRating($scope, stripId).then(
function (res) {
$scope.ratings[stripId].id = res.id;
$scope.ratings[stripId].votes = res.votes + 1;
$scope.ratings[stripId].rating = res.rating + rating;
if (res.id == 0)
SpService.AddData(stripId, $scope.ratings[stripId], $scope);
else {
SpService.UpdateData(stripId, $scope.ratings[stripId], $scope);
}
$scope.currItemRating =
Math.round($scope.ratings[stripId].rating / $scope.ratings[stripId].votes);
}
);
- It’s important to note that this method is using chained promises. Firstly, we’re calling CheckRating method of SpService to check if someone else rated our comic in the meantime, and after that, we’re passing result to anonymous function (function after “then(“), where depending on whether there is a rating or not, we call the appropriate method of SPService (AddData for nonexisting ratings and UpdateData for existing ones).
Using chained promised is much more readable and easier than using callback functions for async calls.
- Prev – method. Sets currItem to the previous object in the collection, if position is not at the beginning of the collection.
- Next– method. Sets currItem to the next object in the collection, if position is not at the end of the collection.
Services
As I said before we have two services FeedService (parses RSS feed) and SpService (reading data from SharePoint and writing data back).
FeedService is pretty straightforward. It has only one method, parseFeed, with one parameter url.
dilbertServices.factory('FeedService', ['$http', '$q', function ($http, $q) {
return {
parseFeed: function (url) {
var deferred = $q.defer();
$http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=50&callback=JSON_CALLBACK&q=' +
encodeURIComponent(url)).success(
function (data, status) {
deferred.resolve(data);
}).error(function (data, status) {
deferred.reject(data);
});
return deferred.promise;
}
}
}]);
Note that we’re using promises and $q service for handling asynchronous calls.
SpService is a little more complicated. It has 4 methods:
- GetData – fetches data (ratings) from SharePoint list in App web. It’s much more the same as method for reading data in my previous article, only difference is using of $q service and promises (there is no more changing of $scope data in service).
Note the part. We are getting only last 14 rows from rss feed so we need only last 14 ratings from the Ratings list.
- CheckRating – checks to see if someone has rated this comic in the meantime
- UpdateData – updates ratings of the previously rated comics
- AddData – adds ratings for the previously unrated comics.
All methods use JSOM for communication with SharePoint
UpdateData and AddData are pretty much the same (code sample below).
var context = new SP.ClientContext(appweburl);
var web =context.get_web();
context.load(web);
var oList = web.get_lists().getByTitle('DilbertRating');
var oListItem = oList.getItemById(obj.id);
oListItem.set_item('Title', guid);
oListItem.set_item('StripId', guid);
oListItem.set_item('Rating', obj.rating);
oListItem.set_item('Votes', obj.votes);
oListItem.update();
We're getting app web context first, and referencing the DilbertRating list (which we created earlier in this demo) later. Only difference is that UpdateData method instantiates ListItem object by calling getItemById (rating already exists in list) and AddData method instantiates ListItem object by calling addItem method (code below) of SharePoint list(rating does not exists in the database). The rest is trivial.
var listItemInfo = new SP.ListItemCreationInformation();
var listItem = oList.addItem(listItemInfo);
App.js
File that glues up whole app.
This is the key line (instantiates App as DilbertApp with our controller and services):
var App = angular.module('DilbertApp', ['ngSanitize','dilbertControllers','dilbertServices'])
Besides that, this file defines one more thing, directive that’s going to be used for ratings. I choose fundooRating (link here) for that purpose. So, for the ratings we are using third party directive that is defined in the App.js file.
Final result
Item Display
First comic in the collection with rating:
Second comic (note the Prev button) in the moment of rating.
Second comic after rating.
Ratings list
If you don’t know how to reach it, it’s in your app on Lists/DilbertRating address. On my server it is on:
http://app-c543c3600f43bf.abcapps.com/DilbertApp/Lists/DilbertRating address
Conclusion
That's it. Rating system described here can be easily implemented in some other scenario (any non SharePoint data displayed inside SharePoint).
Also chaining promises are much simpler and cleaner to work with than to create callback pyramids. So using angularjs as framework for SharePoint brings new quality into App development.