This post describes how to integrate Giphy's API with AngularJS framework. As an example I'll show implementation of a AngularJS's directive which will provide random Giphy's image specified by tags to the end user with preload.
From Giphy's API documentation one can determine requirements for the environment of the API. Following items are worth noticing:
- apiUrl = http://api.giphy.com/v1/gifs/
- apiKey = dc6zaTOxFJmzC -> public beta key for testing
- apiRandomEndpoint = random?api_key=dc6zaTOxFJmzC&tag=tag1+tag2
- notification about CORS requirement for making requests to the endpoints.
Please note that the apiUrl uses http and redirects to another addres also in http. Because we want to use the https we will configure the redirect URL from the original api URL to be via https. The plan is straightforward - the directive for displaying the random gif, provider for obtaining giphy's image details given the request, embedded as $resource using jsonp for CORS. In my setup I use grunt, bower, requirejs together with angular so the code included might be strange at first sight but easily transplantable.
ApplicationConstants config for the angular's app's module - nothing fancy here - just the config properties of the giphy's API and default search tags.
define(['factories/factories', 'angular'], function (factories, angular) {
'use strict';
factories.factory('ApplicationConstants', ['$window', function ApplicationConstants($window) {
return {
'env': $window.location.href.indexOf('localhost') > -1 ? 'dev' : 'release',
'giphyDefaultRandomQuery': 'bicycle',
'giphyApiUrl': $window.location.href.indexOf('localhost') > -1 ? 'http://tv.giphy.com/v1/gifs/' : 'https://tv.giphy.com/v1/gifs/',
'giphyApiKey': 'dc6zaTOxFJmzC',
};
}]);
});
The provider for the data of the giphy's image given the configuration and using jsonp for CORS. Notice callback=JSON_CALLBACK added to the endpoint's URL and other configuration options for idividual objects. Our aim is to obtain $resource as the output to be manipulated later using angular's promises API in the directive.
define(['providers/providers'], function (providers) {
'use strict';
providers.provider('giphyRandom', function giphyRandomProvider() {
this.$get = ['$resource', function ($resource) {
return {
query: '',
setQuery: function (query) {
this.query = query;
},
invoke: function (giphyApiUrl, giphyApiKey, giphyDefaultRandomQuery) {
var query = this.query;
var queryToRun;
if (!query) {
queryToRun = encodeURIComponent(giphyDefaultRandomQuery);
} else {
queryToRun = encodeURIComponent(query);
}
return $resource(
giphyApiUrl + 'random?api_key=' + giphyApiKey + '&tag=' + queryToRun + '&callback=JSON_CALLBACK',
{},
{
jsonpQuery: {
method: 'JSONP',
transformResponse: function (data, headers) {
//Here is possible to restict the data with only required properties but we want whole giphy's model.
//console.log(data);
return data;
},
responseType: 'json',
headers: { 'Content-Type': 'application/json' }
},
}
);
}
};
}];
});
});
Next, we want to specify our directive for the giphy's image. It'll utilize mainly the provider and the application's constants to configure the provider. The data is read from the obtained $resource using angular's promises API. As you can see, I'm using lodash library here.
define(['directives/directives', 'lodash'], function (directives, _) {
'use strict';
directives.directive('giphyRandomImage', ['$log', 'giphyRandom', 'ApplicationConstants', '$timeout', '$compile', function ($log, giphyRandomProvider, ApplicationConstants, $timeout, $compile) {
return {
restrict: 'E',
transclude: true,
templateUrl: "/public/js/directives/templates/giphyRandomImage.html",
scope: {
query: '@'
},
link: function (scope, element, attr) {
//giphy's model
scope.giphy = {};
if (scope.query) {
giphyRandomProvider.setQuery(scope.query);
} else {
giphyRandomProvider.setQuery(ApplicationConstants.giphyDefaultRandomQuery);
}
var giphyRandom = giphyRandomProvider.invoke(
ApplicationConstants.giphyApiUrl,
ApplicationConstants.giphyApiKey,
ApplicationConstants.giphyDefaultRandomQuery).jsonpQuery();
giphyRandom.$promise.then(function (giphyData) {
$timeout(function(){
_.assign(scope.giphy, { 'fixed_height_downsampled_height': giphyData.data.fixed_height_downsampled_height });
_.assign(scope.giphy, { 'fixed_height_downsampled_url': giphyData.data.fixed_height_downsampled_url });
_.assign(scope.giphy, { 'fixed_height_downsampled_width': giphyData.data.fixed_height_downsampled_width });
_.assign(scope.giphy, { 'fixed_height_small_height': giphyData.data.fixed_height_small_height });
_.assign(scope.giphy, { 'fixed_height_small_still_url': giphyData.data.fixed_height_small_still_url });
_.assign(scope.giphy, { 'fixed_height_small_url': giphyData.data.fixed_height_small_url });
_.assign(scope.giphy, { 'fixed_height_small_width': giphyData.data.fixed_height_small_width });
_.assign(scope.giphy, { 'fixed_width_downsampled_height': giphyData.data.fixed_width_downsampled_height });
_.assign(scope.giphy, { 'fixed_width_downsampled_url': giphyData.data.fixed_width_downsampled_url });
_.assign(scope.giphy, { 'fixed_width_downsampled_width': giphyData.data.fixed_width_downsampled_width });
_.assign(scope.giphy, { 'fixed_width_small_height': giphyData.data.fixed_width_small_height });
_.assign(scope.giphy, { 'fixed_width_small_still_url': giphyData.data.fixed_width_small_still_url });
_.assign(scope.giphy, { 'fixed_width_small_url': giphyData.data.fixed_width_small_url });
_.assign(scope.giphy, { 'fixed_width_small_width': giphyData.data.fixed_width_small_width });
_.assign(scope.giphy, { 'id': giphyData.data.id });
_.assign(scope.giphy, { 'image_frames': giphyData.data.image_frames });
_.assign(scope.giphy, { 'image_height': giphyData.data.image_height });
_.assign(scope.giphy, { 'image_mp4_url': giphyData.data.image_mp4_url });
_.assign(scope.giphy, { 'image_original_url': giphyData.data.image_original_url });
_.assign(scope.giphy, { 'image_url': giphyData.data.image_url });
_.assign(scope.giphy, { 'image_width': giphyData.data.image_width });
_.assign(scope.giphy, { 'type': giphyData.data.type });
_.assign(scope.giphy, { 'url': giphyData.data.url });
scope.$apply();
$compile(element.contents())(scope);
});
}).catch(function (error) {
$log.debug("Error getting Giphy Resource's Data", error);
});
}
};
}]);
});
Now, having the directive it is required in our case to have the template for it (as you can see I'm using AngularUIBootstrap attributes here):
<div>
<preloaded-image
image-id="{{giphy.id}}"
image-src="{{giphy.image_url}}"
placeholder-text="giphy"
placeholder-width="300"
placeholder-height="150"
tooltip="giphy.com"
tooltip-placement="bottom"
tooltip-animation="false"
tooltip-class="default">
</preloaded-image>
</div>
As you probably have noticed, there are two new directives specified in the template, preloaded-image, Preloaded-image directive first:
define(['directives/directives', 'angular'], function (directives, angular) {
'use strict';
directives.directive('preloadedImage', ['$log', '$document', '$timeout', '$window', 'ApplicationConstants', '$rootScope', '$compile',
function ($log, $document, $timeout, $window, ApplicationConstants, $rootScope, $compile) {
return {
restrict: 'E',
scope: {
imageId: '@',
imageSrc: '@',
placeholderWidth: '@',
placeholderHeight: '@',
placeholderText: '@',
},
templateUrl: '/public/js/directives/templates/preloadedImage.html',
link: function (scope, element, attrs) {
scope.imageMimeType = '';
scope.showPlaceholder = false;
scope.showLoadingImage = true;
scope.showProgressBar = false;
scope.progressBarValue = 0;
scope.resolveImageMimeType = function () {
var parts = [];
parts = scope.imageSrc.split('.');
if (parts.length === 0 && !(parts[parts.length - 1] === 'jpg' || parts[parts.length - 1] === 'png' || parts[parts.length - 1] === 'gif')) {
$log.debug('Image error', 'Wrong image url - not returning proper image of jpg, gif, png.');
} else {
switch (parts[parts.length - 1]) {
case 'jpg':
return 'image/jpeg';
case 'gif':
return 'image/gif';
case 'png':
return 'image/png';
}
}
};
scope.onLoadStart = function () {
scope.$apply(function () {
$rootScope.$broadcast(ApplicationConstants.events.imagePreloadStart, scope.imageId);
scope.showLoadingImage = false;
scope.showProgressBar = true;
});
};
scope.onProgress = function (e) {
scope.$apply(function () {
scope.progressBarValue = e.loaded / e.total * 100;
});
};
scope.showImage = function () {
var image = $document[0].createElement('img');
image.className = 'img-responsive img-rounded center-block';
image.src = 'data:' + scope.imageMimeType + ';base64,' + scope.xhrBase64DataEncode(xhr.responseText);
element.append(image);
};
scope.onLoadEnded = function () {
scope.$apply(function () {
$rootScope.$broadcast(ApplicationConstants.events.imagePreloadEnd, scope.imageId);
scope.showProgressBar = false;
scope.showImage();
});
};
scope.onError = function () {
angular.element('#placeholder-' + scope.imageId).attr('ng-src', 'http://placehold.it/' + scope.placeholderWidth + 'x' + scope.placeholderHeight + '&text=' + scope.placeholderText);
scope.$apply(function () {
$rootScope.$broadcast(ApplicationConstants.events.imagePreloadEnd, scope.imageId);
scope.isPreloading = false;
scope.showPlaceholder = true;
});
};
var xhr = new $window.XMLHttpRequest();
xhr.onloadstart = scope.onLoadStart;
xhr.onprogress = scope.onProgress;
xhr.onload = scope.onLoadEnded;
xhr.onerror = scope.onError;
scope.loadImage = function () {
$timeout(function () {
xhr.open("GET", scope.imageSrc, true);
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.send();
});
};
scope.xhrBase64DataEncode = function (inputStr) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var outputStr = "";
var i = 0;
while (i < inputStr.length) {
//all three "& 0xff" added below are there to fix a known bug
//with bytes returned by xhr.responseText
var byte1 = inputStr.charCodeAt(i++) & 0xff;
var byte2 = inputStr.charCodeAt(i++) & 0xff;
var byte3 = inputStr.charCodeAt(i++) & 0xff;
var enc1 = byte1 >> 2;
var enc2 = ((byte1 & 3) << 4) | (byte2 >> 4);
var enc3, enc4;
if (isNaN(byte2)) {
enc3 = enc4 = 64;
}
else {
enc3 = ((byte2 & 15) << 2) | (byte3 >> 6);
if (isNaN(byte3)) {
enc4 = 64;
}
else {
enc4 = byte3 & 63;
}
}
outputStr += b64.charAt(enc1) + b64.charAt(enc2) + b64.charAt(enc3) + b64.charAt(enc4);
}
return outputStr;
};
$timeout(function () {
scope.imageMimeType = scope.resolveImageMimeType();
scope.loadImage();
});
}
};
}]);
});
Now, the template for the preloader directive:
<div id="{{imageId}}">
<loading-image id="loadingImage-{{imageId}}" ng-show="showLoadingImage"></loading-image>
<progressbar id="progressbar-{{imageId}}" ng-show="showProgressBar" value="progressBarValue" typeof="success">{{progressBarValue}}%</progressbar>
<img id="placeholder-{{imageId}}" ng-show="showPlaceholder" class="img-responsive img-rounded center-block" />
</div>
Note: I won't enclose here the loading-image directive because it is just a simple AJAX animated loading image to be chosen from web.
Finally, here is the usage of this all somewhere else, like for example in HomeController's view of the angular's application using bootstrap's panel:
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Food and Diet</h3>
</div>
<div class="panel-body">
<giphy-random-image query="food"></giphy-random-image>
</div>
</div>
Great that you have mentioned clearly about the subject which I have missed at online Angularjs training. Thanks for the clear cut presentation of the information.
ReplyDelete