NovelEssay.com Programming Blog

Exploration of Big Data, Machine Learning, Natural Language Processing, and other fun problems.

Social Like and Share Buttons with AngularJS: 'The Angular Way'

You're using AngularJS for your web site, and you want to add some social media share and like buttons that look roughly like this:



Here's what you should do - Setup your App to inject the angulike like this:

(Full Angulike code is near the bottom of this article.)


var myApp = angular.module('myApp', ['angulike'])
.run([
      '$rootScope', function ($rootScope) {
          $rootScope.facebookAppId = 'Your FB App Id Here'; // set your facebook app id here
      }
])

Be sure to buzz over to Facebook.com and obtain an App ID, and set it in the code above.


Then, in your controller code, create a scope variable that has your Url, Name, and an Imgae to be shared:

    $scope.myModel = {
        Url: 'http://blog.novelessay.com',
        Name: "blog.novelessay.com makes you smarter!",
        ImageUrl: 'http://blog.novelessay.com/Images/awesome.jpg'
    };


In your HTML, simply add a few divs like this:

<div fb-like="myModel.Url"></div>
<div tweet="myModel.Name" tweet-url="myModel.Url"></div>
<div google-plus="myModel.Url"></div>
<div pin-it="myModel.Name" data-pin-it-image="myModel.ImageUrl" pin-it-url="myModel.Url"></div>

They will each become the various like and share buttons. Feel free to add css class styling to the div to fit your needs.

Of course, we need to add the AngularJs and Angulike.js script tags to our HTML too:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.min.js"></script>
    <script type='text/javascript' src='/scripts/angulike.js'></script>

And, finally, Angulike.js is the following:

/**
 * AngularJS directives for social sharing buttons - Facebook Like, Google+, Twitter and Pinterest 
 * @author Jason Watmore <jason@pointblankdevelopment.com.au> (http://jasonwatmore.com)
 * @version 1.2.0
 */
(function () {
    angular.module('angulike', [])

      .directive('fbLike', [
          '$window', '$rootScope', function ($window, $rootScope) {
              return {
                  restrict: 'A',
                  scope: {
                      fbLike: '=?'
                  },
                  link: function (scope, element, attrs) {
                      if (!$window.FB) {
                          // Load Facebook SDK if not already loaded
                          $.getScript('//connect.facebook.net/en_US/sdk.js', function () {
                              $window.FB.init({
                                  appId: $rootScope.facebookAppId,
                                  xfbml: true,
                                  version: 'v2.0'
                              });
                              renderLikeButton();
                          });
                      } else {
                          renderLikeButton();
                      }

                      var watchAdded = false;
                      function renderLikeButton() {
                          if (!!attrs.fbLike && !scope.fbLike && !watchAdded) {
                              // wait for data if it hasn't loaded yet
                              watchAdded = true;
                              var unbindWatch = scope.$watch('fbLike', function (newValue, oldValue) {
                                  if (newValue) {
                                      renderLikeButton();

                                      // only need to run once
                                      unbindWatch();
                                  }

                              });
                              return;
                          } else {
                              element.html('<div class="fb-like"' + (!!scope.fbLike ? ' data-href="' + scope.fbLike + '"' : '') + ' data-layout="button_count" data-action="like" data-show-faces="true" data-share="true"></div>');
                              $window.FB.XFBML.parse(element.parent()[0]);
                          }
                      }
                  }
              };
          }
      ])

      .directive('googlePlus', [
          '$window', function ($window) {
              return {
                  restrict: 'A',
                  scope: {
                      googlePlus: '=?'
                  },
                  link: function (scope, element, attrs) {
                      if (!$window.gapi) {
                          // Load Google SDK if not already loaded
                          $.getScript('//apis.google.com/js/platform.js', function () {
                              renderPlusButton();
                          });
                      } else {
                          renderPlusButton();
                      }

                      var watchAdded = false;
                      function renderPlusButton() {
                          if (!!attrs.googlePlus && !scope.googlePlus && !watchAdded) {
                              // wait for data if it hasn't loaded yet
                              watchAdded = true;
                              var unbindWatch = scope.$watch('googlePlus', function (newValue, oldValue) {
                                  if (newValue) {
                                      renderPlusButton();

                                      // only need to run once
                                      unbindWatch();
                                  }

                              });
                              return;
                          } else {
                              element.html('<div class="g-plusone"' + (!!scope.googlePlus ? ' data-href="' + scope.googlePlus + '"' : '') + ' data-size="medium"></div>');
                              $window.gapi.plusone.go(element.parent()[0]);
                          }
                      }
                  }
              };
          }
      ])

      .directive('tweet', [
          '$window', '$location',
          function ($window, $location) {
              return {
                  restrict: 'A',
                  scope: {
                      tweet: '=',
                      tweetUrl: '='
                  },
                  link: function (scope, element, attrs) {
                      if (!$window.twttr) {
                          // Load Twitter SDK if not already loaded
                          $.getScript('//platform.twitter.com/widgets.js', function () {
                              renderTweetButton();
                          });
                      } else {
                          renderTweetButton();
                      }

                      var watchAdded = false;
                      function renderTweetButton() {
                          if (!scope.tweet && !watchAdded) {
                              // wait for data if it hasn't loaded yet
                              watchAdded = true;
                              var unbindWatch = scope.$watch('tweet', function (newValue, oldValue) {
                                  if (newValue) {
                                      renderTweetButton();

                                      // only need to run once
                                      unbindWatch();
                                  }
                              });
                              return;
                          } else {
                              element.html('<a href="https://twitter.com/share" class="twitter-share-button" data-text="' + scope.tweet + '" data-url="' + (scope.tweetUrl || $location.absUrl()) + '">Tweet</a>');
                              $window.twttr.widgets.load(element.parent()[0]);
                          }
                      }
                  }
              };
          }
      ])

      .directive('pinIt', [
          '$window', '$location',
          function ($window, $location) {
              return {
                  restrict: 'A',
                  scope: {
                      pinIt: '=',
                      pinItImage: '=',
                      pinItUrl: '='
                  },
                  link: function (scope, element, attrs) {
                      if (!$window.parsePins) {
                          // Load Pinterest SDK if not already loaded
                          (function (d) {
                              var f = d.getElementsByTagName('SCRIPT')[0], p = d.createElement('SCRIPT');
                              p.type = 'text/javascript';
                              p.async = true;
                              p.src = '//assets.pinterest.com/js/pinit.js';
                              p['data-pin-build'] = 'parsePins';
                              p.onload = function () {
                                  if (!!$window.parsePins) {
                                      renderPinItButton();
                                  } else {
                                      setTimeout(p.onload, 100);
                                  }
                              };
                              f.parentNode.insertBefore(p, f);
                          }($window.document));
                      } else {
                          renderPinItButton();
                      }

                      var watchAdded = false;
                      function renderPinItButton() {
                          if (!scope.pinIt && !watchAdded) {
                              // wait for data if it hasn't loaded yet
                              watchAdded = true;
                              var unbindWatch = scope.$watch('pinIt', function (newValue, oldValue) {
                                  if (newValue) {
                                      renderPinItButton();

                                      // only need to run once
                                      unbindWatch();
                                  }
                              });
                              return;
                          } else {
                              element.html('<a href="//www.pinterest.com/pin/create/button/?url=' + (scope.pinItUrl || $location.absUrl()) + '&media=' + scope.pinItImage + '&description=' + scope.pinIt + '" data-pin-do="buttonPin" data-pin-config="beside"></a>');
                              $window.parsePins(element.parent()[0]);
                          }
                      }
                  }
              };
          }
      ]);

})();

A big thanks to Jason Watmore the author of Angulike for making the base set of Angular friendly code for this scenario.


After following this process, you should get some social buttons to like and share your content that look like this:



AngularJs Pagination with custom Total Count reuse.

I have an AngularJS application that paginates through a table. You can filter the table by column. I'm using angular-paginate-anything as my AngularJS plugin for my paginated table. My table has over a million rows, and the filtered queries are not fast. Selecting "top 10" records is much faster than "get total count", so I'd like to reuse the total count value if the table filters don't change between pages. Unfortunately, angular-paginate-anything doesn't do that out of the box. Fortunately, this article shows how to do that.


Background, I have another article that shows how to use angular-paginate-anything for an Mvc.Net project with pagination. Consider that the starting point. Go ahead and read it quick. We'll wait right here...

http://blog.novelessay.com/post/mvc-net-with-angularjs-and-pagination


Next, we'll open the paginate-anything-tpls.js file and work with that (since it's not minimized). Again, our goal is to only request a new total count when the parameters change.


When the pagination app initially loads, we need to get a total count so we'll initialize that state to true:

              controller: ['$scope', '$http', function ($scope, $http) {
                  $scope.urlParamsChanged = true;

After we get the paginated results from the server, we want to set the urlParamsChange state to false.

function requestRange(request) {
$scope.$emit('pagination:loadStart', request);
$http({
...
}).success(function (data, status, headers, config) {
$scope.urlParamsChanged = false;

Then, we need to set urlParamsChanged to true again when the filter parameters do change:

                  $scope.$watch('urlParams', function (newParams, oldParams) {
                      if ($scope.passive === 'true') { return; }
                      if (!angular.equals(newParams, oldParams)) {
                          $scope.urlParamsChanged = true;

When we do make a request to the server we need to check the urlParamsChanged and indicate if we need total count or not:

                      $http({
                          method: 'GET',
                          url: $scope.url,
                          params: $scope.urlParams,
                          headers: angular.extend(
                            {}, $scope.headers,
                            { 'Range-Unit': 'items', Range: $scope.urlParamsChanged == true ? [request.from, request.to].join('-') : [request.from, request.to, $scope.numItems].join('-') }
                          ),
That's all the java script changes we need. Next, we need to modify our server to check if the total count is passed back to the server. Originally, total count on the input paging model was always 0, so we'll check if it's non-zero in the new code.

[Route("GetSearchResults"), HttpGet()]
[WithPaging]
public IHttpActionResult Search([FromUri] PagingModel pagingModel
, [FromUri] string Name = null
, [FromUri] string Region = null
)
{
IQueryable <company> query = ...
PagingModeSearch result = new PagingModeSearch();
result.From = pagingModel.From;
result.To = pagingModel.To;
if (pagingModel.TotalRecordCount > 0)
{
result.TotalRecordCount = pagingModel.TotalRecordCount;
}

else
{
result.TotalRecordCount = query.Count();
}


Finally, we need to update our WithPagingAttribute class in our NgPaginateModel.cs file to parse the range header for the total count value like this:

    public class WithPagingAttribute : ActionFilterAttribute
    {
        private readonly string _paramName;
        private static readonly Regex RangeHeaderRegex = new Regex(@"(?<from>\d+)-(?<to>\d*)-(?<total>\d*)", RegexOptions.Compiled);
And, very lastly this:
        private static PagingModel IncomingPagingModel(HttpRequestHeaders headers, PagingModel pagingModel)
        {
            if (pagingModel == null)
                pagingModel = new PagingModel();
            IEnumerable<string> rangeUnitValues;
            IEnumerable<string> rangeValues;
            if (headers.TryGetValues("Range-Unit", out rangeUnitValues)
                && rangeUnitValues.First().Equals("items", StringComparison.OrdinalIgnoreCase)
                && headers.TryGetValues("Range", out rangeValues))
            {
                var rangeHeaderMatch = RangeHeaderRegex.Match(rangeValues.First());
                if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["from"].Value))
                    pagingModel.From = int.Parse(rangeHeaderMatch.Groups["from"].Value);
                if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["to"].Value))
                    pagingModel.To = int.Parse(rangeHeaderMatch.Groups["to"].Value);
                if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["total"].Value))
                    pagingModel.TotalRecordCount = int.Parse(rangeHeaderMatch.Groups["total"].Value);

            }
            return pagingModel;
        }


This makes navigating through each page of a search result much faster when the total count can be reused.















Mvc.Net with AngularJS and Pagination

My personal current "goto" for web based pagination uses angular-paginate-anything plugin with a Mvc.Net backend. As with most web development tools, there's many ways to use something like this. This blog will show one full example of how to do Mvc.Net with AngularJS and Pagination using angular-paginate-anything.


I'm going to try and keep this very simple, so excuse some of the nuance being skipped.

0) Create a MVC.Net project and a controller/view. I'll leave that outside the scope of this article.


1) Create an AngularJS App & Controller. Nothing fancy. Just simple stuff:

Link the AngularJS:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script>
Create a JS file with the AngularJS code:
var app = angular.module('MyPersonalApp', ['bgf.paginateAnything']);
app.controller('MyPersonalCtrl', ['$scope', '$http', '$filter', '$interval', '$location', function ($scope, $http, $filter, $interval, $location) {
...
}]);
In your cshtml view, link your new javascript file, and make some divs that hold your app:
<script src="~/Scripts/Search/MyPersonalApp.js"></script>
<div id="MyPersonalAppId" ng-app="MyPersonalApp">
    <div ng-controller="MyPersonalCtrl" ng-cloak>
...


2) Download and link the javascript in your view (cshtml).

        <script src="~/Scripts/paginate-anything-tpls.js"></script>


3) In side your app divs, add a table to hold the paginated results and bgf-pagination markup like this:

                            <table>
                                <tr>
                                    <th>Name</th>
                                </tr>
                                <tr ng-repeat="result in searchResults">
                                    <td>{{result.Name}}</td>
                                </tr>
                            </table>
                            <bgf-pagination collection="searchResults"
                                            url="'/api/GetMySearchResults'"
                                            url-params="searchParams"
                                            num-items="searchResultsTotalItems">
                            </bgf-pagination>


4) You'll notice that the bgf-pagination references several things. First, lets look at the searchParams. Ideally, you'll want to hook that up to a searchbox of some sort. For now, we'll hard code a query string to the searchParams in our angular controller like this:

app.controller('MyPersonalCtrl', ['$scope', '$http', '$filter', '$interval', '$location', function ($scope, $http, $filter, $interval, $location) {
    $scope.searchParams = {};
    $scope.searchParams.queryName= "Jo";


5) Next, notice that /api/GetFreeSearchResults URL points at a web API endpoint. We'll set that up by installing the WebAPI Nuget project in our MVC.Net project.

    


6) Create new WebAPI Controller in your project, and make it look roughly like this:

    [RoutePrefix("api")]
    public class SearchApiController : ApiController
    {
        MyDbEntities _db;

        public SearchApiController()
        {
            _db = new MyDbEntities();
_db.Configuration.ProxyCreationEnabled = false; } [Route("GetMySearchResults"), HttpGet()]
[WithPaging] public IHttpActionResult Search([FromUri] PagingModeFreeSearch pagingModel , [FromUri] string queryName = null
) { string localQueryName = string.Empty; if(!string.IsNullOrEmpty(queryName ))
{ localQueryName = queryName;
} var peopleResultsTotal= _db.People.Where(t => t.Name.ToLower().Contains(localQueryName));
var peopleResultsSubset = peopleResultsTotal.OrderBy(o => o.Id).Skip(pagingModel.From).Take(pagingModel.Size);
PagingModeMySearch result = new PagingModeMySearch();
result.From = pagingModel.From; result.To = pagingModel.To; result.TotalRecordCount = peopleResultsTotal.Count();
result.People = peopleResultsSubset.ToArray();
return this.PagedPartialContent(result.People , result.From, result.To, (int)result.TotalRecordCount);
} }

Notice the routing annotations are setup. This also assumes you have a DB with a table named People as your MyDbEntities data source.


7) Where does the [WithPaging] annotation come from? Make a file named NgPaginateModels.cs, and use this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.Results;
namespace MyProject.MVC.Models
{
    // Found this awesome sauce here:
    // https://github.com/begriffs/angular-paginate-anything/wiki/How-To-Configure-ASP.NET-Web-API
    public class WithPagingAttribute : ActionFilterAttribute
    {
        private readonly string _paramName;
        private static readonly Regex RangeHeaderRegex = new Regex(@"(?<from>\d+)-(?<to>\d*)", RegexOptions.Compiled);
        public WithPagingAttribute(string paramName = "pagingModel")
        {
            _paramName = paramName;
        }
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            object data;
            if (actionContext.ActionArguments.TryGetValue(_paramName, out data))
            {
                //manipulate/inject paging info from headers
                var pagingModel = data as PagingModel;
                pagingModel = IncomingPagingModel(actionContext.Request.Headers, pagingModel);
                actionContext.ActionArguments[_paramName] = pagingModel;
            }
        }
        private static PagingModel IncomingPagingModel(HttpRequestHeaders headers, PagingModel pagingModel)
        {
            if (pagingModel == null)
                pagingModel = new PagingModel();
            IEnumerable<string> rangeUnitValues;
            IEnumerable<string> rangeValues;
            if (headers.TryGetValues("Range-Unit", out rangeUnitValues)
                && rangeUnitValues.First().Equals("items", StringComparison.OrdinalIgnoreCase)
                && headers.TryGetValues("Range", out rangeValues))
            {
                var rangeHeaderMatch = RangeHeaderRegex.Match(rangeValues.First());
                if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["from"].Value))
                    pagingModel.From = int.Parse(rangeHeaderMatch.Groups["from"].Value);
                if (!string.IsNullOrWhiteSpace(rangeHeaderMatch.Groups["to"].Value))
                    pagingModel.To = int.Parse(rangeHeaderMatch.Groups["to"].Value);
            }
            return pagingModel;
        }
    }
    public class PartialNegotiatedContentResult<T> : OkNegotiatedContentResult<T>
    {
        readonly int _requestedFrom;
        readonly int _requestedTo;
        readonly int _totalCount;
        public PartialNegotiatedContentResult(T content, ApiController controller, int requestedFrom, int requestedTo, int totalCount)
            : base(content, controller)
        {
            _requestedFrom = requestedFrom;
            _requestedTo = requestedTo;
            _totalCount = totalCount;
            if (_requestedTo > _totalCount && _totalCount > 0)
                _requestedTo = _totalCount;
        }
        public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            var response = await base.ExecuteAsync(cancellationToken);
            const string unit = "items";
            response.Headers.Add("Accept-Ranges", unit);
            response.Headers.Add("Range-Unit", unit);
            if (_totalCount > 0)
            {
                response.StatusCode = HttpStatusCode.PartialContent;
                response.Content.Headers.ContentRange = new ContentRangeHeaderValue(_requestedFrom, _requestedTo, _totalCount)
                {
                    Unit = unit
                };
            }
            else
            {
                response.StatusCode = HttpStatusCode.NoContent;
                response.Content.Headers.ContentRange = new ContentRangeHeaderValue(0);
            }
            return response;
        }
    }
    public static class ApiControllerExtensions
    {
        public static IHttpActionResult PagedPartialContent<T>(
            this ApiController controller,
            IEnumerable<T> content, int from, int to, int count) where T : class
        {
            return new PartialNegotiatedContentResult<IEnumerable<T>>(content, controller, from, to, count);
        }
    }
}


8) What about the PagingModel class? Create a PagingModel base class, and then inherit from it like this:

    public class PagingModeMySearch: PagingModel
    {
        public PagingModeMySearch()
        {
        }
        public Person[] People { get; set; }
    }
    public class PagingModel
    {
        public PagingModel()
        {
            From = 0;
            To = 4;
        }
        public int From { get; set; }
        public int To { get; set; }
        public long TotalRecordCount { get; set; }
        public int Page { get { return (To / Size) + 1; } }
        public int Size { get { return To - From + 1; } }
    }


If you spin this up, your web page should load and the table should show Names like "Joe", "Joseph", "John", etc... with the hard coded "Jo" search string we put in our AngularJS controller.


That's really about all there is to making this work. The rest is search controls and styling up the results.


You can get angular-paginate-anything on Github here:

https://github.com/begriffs/angular-paginate-anything