Skip to content

Instantly share code, notes, and snippets.

@dgs700
Created February 25, 2013 02:18
Show Gist options
  • Save dgs700/5026933 to your computer and use it in GitHub Desktop.
Save dgs700/5026933 to your computer and use it in GitHub Desktop.
First Angular.js implementation example. Tibbr plugin components "Like" and "Follow" that were previously handled with untestable jQuery spaghetti refactored to Angular conventions. Besides its main role as a tight data binding (MVVM) framework, Angular is showing promise for creating reusable and exportable UI components without too much overhead.
//Tibbr plugins html directives and associated controllers
(function () {
'use strict';
//declare the plugins module with dependancies
var tibbrPlugins = angular.module('TibbrPlugins', ['TibbrFilters', 'Tibbr', 'EventsBus', 'PageBus', 'TibbrAPI']);
//code to run when a like tag is encountered
tibbrPlugins.directive('tibrLike', function factory($window) {
var directiveDefinitionObject = {
//template: '<div>hi</div>',
templateUrl: 'partials/like_button.html',
replace: true,
scope: {},
controller: 'LikeController',
//transclude: true,
link: function postLink(scope, elm, attrs) {
//pull the resource info form the tag attributes
scope.resource = {
id: attrs.ogId || null,
key: attrs.ogKey || null,
url: attrs.ogUrl || $window.location.href,
title: attrs.ogTitle || '',
image: attrs.ogImage || null,
description: attrs.ogDescription || null,
type: "og:link"
}
}
};
return directiveDefinitionObject;
});
//app logic and template vars for the like template
tibbrPlugins.controller('LikeController', function factory($scope, $_tibbr, $_events, $_pagebus, $_tibbrAPI, $_constants, $location, $window) {
//set some default values for the template vars
//this could easily be the toJSON() output from Backbone models
var likeModel = {
currentUser: $_tibbr.currentUser,
host: $_tibbr._host,
likeStatus: '',
likeCount: null,
you: '',
and: '',
others: '',
firstOne: 'first_one',
likeThis: 'like_this',
hideBubble: false,
hideProfile: false,
likers: [],
resourceType: "og:link"
};
angular.extend($scope, likeModel);
//callback function passed as a param to the like api methods
$scope.onLikeResponse = function (data, rc) {
//be sure to reset any necessary vars
var user = {};
if (rc !== "error") {
$scope.you = $scope.and = $scope.others = '';
$scope.likeStatus = (data.ilike) ? 'unlike' : 'like';
$scope.likeCount = data.count;
$scope.hideBubble = (data.count == 0);
$scope.likers = [];
//if the current user likes whatever
if (data.ilike) {
var cu = $scope.currentUser = $_tibbr.currentUser;
$scope.you = 'you';
$scope.profileUrl = user.profileUrl = '//' + $_tibbr._host + '/#!/users/' + cu.id + '/profile';
user.displayName = cu.display_name;
$scope.likers.push(user);
}
if (data.others.length) {
$scope.and = ($scope.you) ? 'and' : '';
$scope.others = data.others.length + ' others';
//if there are other likers add up to five including the current user
}
$scope.$digest(); //refresh the view
} else {
$window.console.warn("Error posting like to tibbr");
}
};
$scope.getLikeStatus = function () {
$_tibbrAPI({
//debug: true,
url: $_constants.URLS.get_like,
method: "GET",
params: {
client_id: $_tibbr.client_id,
resource: $scope.resource
},
onResponse: $scope.onLikeResponse
});
};
$scope.postLikeStatus = function () {
var p = {
//factor up to general resource maker
resource: $scope.resource,
like: $scope.likeStatus,
client_id: $_tibbr.client_id
};
$_tibbrAPI({
url: $_constants.URLS.post_like,
method: "POST",
params: p,
onResponse: $scope.onLikeResponse
});
}
//scope methods- some of these might need to be factored out into services
//event handler for button click
$scope.checkSession = function (type) {
$_pagebus.publish("tibbr:session:test:request", type);
};
//I think what this does is test for an active (logged in) session before the actual proxied ajax call
$_pagebus.subscribe("tibbr:session:test:response", function (data) {
if (data._tc == "on") {
if (data._callback == 'likeMe') {
$scope.postLikeStatus();
}
} else {
$_tibbr.login(); // should be our own service
}
});
// legacy code depends on an outside login event for proper functioning
$_events.on('login', function (data) {
$scope.getLikeStatus();
});
//$scope.getLikeStatus(); // this call fails - why?
});
////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
//code to run when a follow tag is encountered
tibbrPlugins.directive('tibrFollow', function factory($window) {
var directiveDefinitionObject = {
//template: '<div>hi</div>',
templateUrl: 'partials/follow_button.html',
replace: true,
scope: {},
controller: 'FollowController',
//transclude: true,
link: function postLink(scope, elm, attrs) {
//pull the EOG resource info form the tag attributes
scope.resource = {
id: attrs.ogId || null,
key: attrs.ogKey || null,
url: attrs.ogUrl || $window.location.href,
title: attrs.ogTitle || '',
image: attrs.ogImage || null,
description: attrs.ogDescription || null,
type: "og:link"
}
}
};
return directiveDefinitionObject;
});
//app logic and template vars for the like template
tibbrPlugins.controller('FollowController', function factory($scope, $_tibbr, $_pagebus, $_tibbrAPI, $_constants, $window) {
//set some default values for the template vars
var followModel = {
currentUser: $_tibbr.currentUser,
host: $_tibbr._host,
followStatus: 'follow',
isFollowing: false,
resourceType: "og:link"
};
angular.extend($scope, followModel);
//it should toggle the values of followStatus and isFollowing switch to the opposite of the current value
$scope.onFollowResponse = function (data, rc) {
//be sure to reset any necessary vars
if (rc == "200") {
//toggle the 'follow' status
$scope.followStatus = ($scope.isFollowing) ? 'follow' : 'unfollow';
$scope.isFollowing = ($scope.isFollowing) ? false : true;
$scope.$digest(); //refresh the view
} else {
$window.console.warn("Error posting follow to tibbr");
}
};
//it should post a follow/unfollow api call based on the current isFollowing switch
$scope.postFollowStatus = function () {
var action, method;
if($scope.isFollowing){
action = $_constants.URLS.unfollowResource;
method = 'DELETE';
}else{
action = $_constants.URLS.followResource;
method = 'POST';
}
var params = {
//todo: factor up to general resource maker
resource: $scope.resource,
follow: $scope.followStatus,
client_id: $_tibbr.client_id
};
var config = {
url: action,
method: method,
params: params,
onResponse: $scope.onFollowResponse
};
$_tibbrAPI(config);
return config; //for testing
}
//scope methods- some of these might need to be factored out into services
//event handler for button click
$scope.followThis = function (type) {
$_pagebus.publish("tibbr:session:test:request", type);
};
//this one makes no sense
$_pagebus.subscribe("tibbr:session:test:response", function (data) {
if (data._tc == "on") {
if (data._callback == 'followMe') {
$scope.postFollowStatus();
}
} else {
$_tibbr.login(); // should be our own service
}
});
// todo: determine if server api for initial 'follow' status exists yet
//$_events.on('login', function (data) {
//token, //loginstuff
//$scope.getLikeStatus();
//});
//$scope.getLikeStatus(); // this call fails - why?
});
})();
'use strict';
/* jasmine specs for controllers go here */
describe('Tibbr Plugin controllers', function () {
beforeEach(module('TibbrPlugins'));
describe('FollowController', function () {
var followController, config;
var scope = {
followStatus: 'follow',
isFollowing: false,
};
beforeEach(inject(function ($controller) {
followController = $controller(FollowController, {$scope: scope});;
}));
it('should configure a REST POST api call when follow is clicked', function () {
config = $scope.postFollowStatus();
expect(config.url).toEqual('/resources/follow');
expect(config.method).toEqual('POST');
});
it('should configure a REST DELETE api call when unfollow is clicked', function () {
$scope.isFollowing = true;
config = $scope.postFollowStatus();
expect(config.url).toEqual('/resources/unfollow');
expect(config.method).toEqual('DELETE');
});
it('should correctly toggle the isFollowing switch', function () {
$scope.isFollowing = true;
$scope.onFollowResponse({}, '200');
expect($scope.isFollowing).toEqual(false);
$scope.onFollowResponse({}, '200');
expect($scope.isFollowing).toEqual(true);
});
});
describe('LikeController', function () {
var likeController;
var scope = {
likeStatus: '',
likeCount: null
};
var data = {
ilike: true,
others: []
};
beforeEach(inject(function ($controller) {
likeController = $controller(FollowController, {$scope: scope});;
}));
it('should toggle the like status based on data returned with the onLikeResponse handler', function () {
$scope.onLikeResponse(data, '200');
expect($scope.likeStatus).toEqual('unlike');
data.ilike = false;
expect($scope.likeStatus).toEqual('like');
});
});
});
<span class="tibbr-connect">
<span class="tibbr-share-links" ng-click="followThis('followMe')">
<span class="tibbr-share-btn">
<a href="javascript:void(0);"><span></span>{{followStatus | translate}}</a>
</span>
</span>
</span>
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8">
<title>Tibbr Template Application</title>
<link rel="stylesheet" href="http://dshapiro.tibbr.com:3001/connect/stylesheets/tibbr_share-min.css"/>
</head>
<body data-spy="scroll" data-offset="50">
<h2>Tibbr Template Application</h2>
<div class="container">
<span tibr-like
og-id=""
og-key=""
og-url=""
og-image=""
og-title="My app title"
og-description="This is the description of the thing I am liking">
</span>
<span tibr-follow
og-id=""
og-key=""
og-url=""
og-image=""
og-title="My app title"
og-description="This is the description of the thing I am following">
</span>
</div> <!-- /container -->
<script type="text/javascript">
var tib_init = {
prefix: "",
client_id: 1,
host: "dshapiro.tibbr.com:3001", // host, domain (and port if non-standard) to your Tibbr instance
tunnelUrl: "http://dshapiro.tibbr.com/tunnel.html" // full url to tunnel.html on your application server
};
//for now loading the TIB stuff and initializing synchronously until its loading scheme can be corrected
</script>
<script src="http://dshapiro.tibbr.com:3001/connect/js/TIB3.js"></script>
<script src="http://dshapiro.tibbr.com:3001/connect/js/pagebus-ie8plus-min.js"></script>
<script src="http://dshapiro.tibbr.com:3001/connect/js/tibbr.pagebus.js"></script>
<script src="http://dshapiro.tibbr.com:3001/connect/js/tib-min.js"></script>
<!--<script src="http://dshapiro.tibbr.com:3001/connect/js/plugins.js"></script>-->
<script>
TIB._setupCORS();
var TIBR = TIB;
</script>
<script src="http://dshapiro.tibbr.com/angular-seed/app/lib/angular/angular.js"></script>
<script src="http://dshapiro.tibbr.com/angular-seed/app/js/app.js"></script>
<script src="http://dshapiro.tibbr.com/angular-seed/app/js/services.js"></script>
<script>
//bootstrap angular w/o using markup outside of our widgets
angular.element(document).ready(function () {
angular.bootstrap(document, ["TibbrPlugins"]);
});
</script>
</body>
</html>
<div id="like_button">
<span class="tibbr-connect">
<span class="tibbr-share-links" ng-click="checkSession('likeMe')">
<span class="tibbr-share-btn">
<a href="javascript:void(0);"><span></span>{{likeStatus | translate}}</a>
</span>
</span>
<span class="bubble" ng-hide="hideBubble">
<span class="bubblePointer"></span>
<span class="bubble-box">{{likeCount}}</span>
</span>
<span class="info">
<span class="activity-info">
<span ng-show="likers.length"><a target="_blank" href="{{profileUrl}}" title="{{currentUser.display_name}}" ng-show="you">{{you | translate}} </a>
{{and | translate}} {{others | translate}} {{likeThis | translate}}</span>
<span ng-hide="likers.length">{{firstOne | translate}}</span>
</span>
</span>
</span>
<div class="like-profiles-container" style="margin: 0 0 0 92px; clear: both;">
<a ng-repeat="user in likers" class="small profile-pic" target="_blank" href="{{user.profileUrl}}" title="{{user.displayName}}">
<img class="tib-like-profile-images" ng-src="//{{host}}/users/{{currentUser.id}}/profile_image">
</a>
</div>
</div>
//tibbr services (wrappers around XDM/ajax for now) and filters
(function () {
'use strict';
/* Services - most of these are just stubs or wrappers for now*/
//grab an angular reference to global TIBR (for now)
angular.module('Tibbr', []).factory('$_tibbr', function ($window) {
var PB = $window.TIB;
return PB;
});
// get tibbr api resource info either passed in by param or scrapped from meta tags
angular.module('EventsBus', ['Tibbr']).factory('$_events', function ($_tibbr) {
var evts = $_tibbr.__events;
return evts;
});
// get tibbr api resource info either passed in by param or scrapped from meta tags
angular.module('PageBus', ['Tibbr']).factory('$_pagebus', function ($_tibbr) {
var pagebus = $_tibbr.PageBus;
return pagebus;
});
//grab an angular reference to global TIBR.api
angular.module('TibbrAPI', ['Tibbr', 'PageBus']).factory('$_tibbrAPI', function ($_tibbr, $_pagebus, $q) {
var api = function (args) {
if (!args) {
return function () {
console.warn('No argument object passed to tibbr.api()')
};
}
var handlerId = args.handlerId = "api:response:" + Math.floor(Math.random() * 1111);
args.url = $_tibbr._protocal() + $_tibbr._host + "" + args.url;
// assign client id in each api call if not provided by end user
if (args.params && !args.params.client_id && $_tibbr.client_id) {
args.params.client_id = $_tibbr.client_id;
}
var params = {
u: args.url,
m: args.method,
t: args.type,
d: args.params,
hid: handlerId
};
$_pagebus.publish("api:call", params);
$_pagebus.subscribe(handlerId, function (data) {
args.onResponse(data.r, data.rc);
$_pagebus.unsubscribe(handlerId);
});
};
return api;
});
// get tibbr api resource info either passed in by param or scrapped from meta tags
angular.module('ResourcePrep', ['Tibbr']).factory('$_resourcePrep', function ($_tibbr, $window) {
return function (resourceConfig) {
resourceConfig = resourceConfig || null;
// add code to pull the info from meta tags or use the config or build a default
var resource = resourceConfig;
return resource;
};
});
//possible combined sevices definition
//angular.module('PluginServices', ['Tibbr', 'TibbrAPI', 'ResourcePrep', 'EventsBus']).factory('plugins', function ($_tibbr, tibbrAPI, resourcePrep, events, $location) {
// var services = function () {};
// return services;
//});
//filters like i18n
angular.module('TibbrFilters', ['Constants']).filter('translate', function ($_constants) {
return function (input) {
var out = ($_constants.I18N[input]) ? $_constants.I18N[input] : input;
return out;
}
});
//TIBR strings (don't inject any dependancies here)
angular.module('Constants', []).factory('$_constants', function () {
var isNotIE8 = (function () {
var rv = 100;
if (navigator.appName == 'Microsoft Internet Explorer') {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null) {
rv = parseFloat(RegExp.$1);
}
}
return (rv >= 9) ? true : false;
})();
var JS_PATH = "/connect/js/";
var TIBBR_PATH = "";
return {
//javascript
PAGEBUS: JS_PATH + ((isNotIE8) ? 'pagebus-ie8plus-min' : 'pagebus'),
TIB_MIN: JS_PATH + "tib-min",
PLUGINS: JS_PATH + "plugins",
PARENT_CONNECTOR: JS_PATH + 'parent_connector',
//html, css
PLUGIN_CSS: "/connect/stylesheets/tibbr_share-min.css",
CONNECT_PROXY: '/connect/connect_proxy_min',
//api urls
USERS_URL: "/users/find_by_session",
I18N_URL: "/plugins/connect_translate",
I18N: {
share: "Share",
follow: "Follow",
unfollow: "Unfollow",
like: "Like",
unlike: "Unlike",
first_one: "Be the first one to like this",
you: "You",
and: " and ",
like_this: " like this",
likes_this: " likes this",
one_other: " and 1 other like this",
others: " others like this",
not_logged_in: "You need to login to tibbr.",
login: " Click <a href='#' class='tib-login-link'>here</a> to login."
},
URLS: {
share: "plugins/#!/share",
shareCount: "/plugins/share_count",
comments: "plugins/#!/comments",
post_like: "/plugins/like",
followResource: "/resources/follow",
unfollowResource: "/resources/unfollow",
getResourcePermissions: "/resources/user_permissions",
get_like: "/plugins/get_like"
}
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment