Sometimes you may find yourself that the common patterns in web development cannot support your UI requirements in terms of how dynamic a specific view can be. Let me explain what I mean by a simple example: Assume that you have a route in your Single Page Application that displays all the azure services that a user is registered at. Each registered service has its own section on the template an of course specific functionality such as unregister, add or remove features. The question is which is the proper way to implement such a view? You could create a template such as the following:
<div ng-controller="servicesCtrl"> <section ng-show="hasServiceOne"> <div ng-click="addFeatureToServiceOne">Service One Content</div> </section> <section ng-show="hasServiceTwo"> <div ng-click="addFeatureToServiceTwo">Service Two Content</div> </section> <section ng-show="hasServiceThree"> <div ng-click="addFeatureToServiceThree">Service Three Content</div> </section> </div>
Indeed with this template you could display or hide specific sections depending on if the user is registered or not respectively but there are some drawbacks:
- The servicesCtrl controller needs to implement the functionality for all the services even if the user isn’t registered at all of them.
- The template isn’t sufficient in case we wanted to display the registered services in a specific order (dynamic behavior)
The solution: Dynamic compiled directives
The solution to the problem is to use custom directives for each service and add it to the template dynamically only if the user is registered. With this pattern you could add each service section in any order you want, assuming this info comes from an api. More over each directive would encapsulate its own functionality and only in the relative controller, template and css file.
Show me some code
I have created an SPA that solves the scenario we presented before related to azure services. Each post is an opportunity to learn new things so I found this one to also present you a proper way to create your own AngularJS 3rd party library. By 3rd party library i mean that you can encapsulate any custom angularJS elements or API you want in a single my-library.js file and use it in any angularJS application. The source code of the application is available for download on Github. Let’s take a look at the architecture first:
We can see that for each service we have a respective directive with all the required files along. More specifically we have directives for 4 azure services, Active Directory, BizTalk, RedisCache and Scheduler. Here is the code for the RedisCache directive which is responsible to display and handle RedisCache service related functionality.
(function() { 'use strict'; angular.module('azure.redisCache', []); })();
(function() { 'use strict'; angular .module('azure.redisCache') .directive('redisCache', redisCache); function redisCache() { //Usage: //<redis-cache></redis-cache>"/> var directive = { controller: 'RedisCache', restrict: 'E', templateUrl: 'external-lib/redisCache/redisCache.html' }; return directive; } })();
(function() { 'use strict'; angular .module('azure') .controller('RedisCache', RedisCache); RedisCache.$inject = ['$scope']; function RedisCache($scope) { activate(); function activate() { } } })();
<!-- Page Content --> <div class="container rediscache"> <!-- Page Heading/Breadcrumbs --> <div class="row"> <div class="col-lg-12"> <h1 class="page-header">{{service.title}} <small>service</small> </h1> </div> </div> <!-- /.row --> <!-- Projects Row --> <div class="row"> <div class="col-md-6 img-portfolio"> <a href="portfolio-item.html"> <img class="img-responsive img-hover" ng-src="{{service.image01}}" alt=""> </a> <h3> <a href="portfolio-item.html">About</a> </h3> <p>Azure Redis Cache is based on the popular open-source Redis cache. It gives you access to a secure, dedicated Redis cache, managed by Microsoft and accessible from any application within Azure.</p> <p>Azure Redis Cache is easy to use. Just provision a cache using the Microsoft Azure portal and call into its end point using any client that supports Redis. If you’ve used Redis before, you already know how to use Azure Redis Cache.</p> </div> <div class="col-md-6 img-portfolio"> <a href="portfolio-item.html"> <img class="img-responsive img-hover" ng-src="{{service.image02}}" style="height:207px" alt=""> </a> <h3> <a href="portfolio-item.html">High performance</a> </h3> <p>Azure Redis Cache helps your application become more responsive even as user load increases. It leverages the low-latency, high-throughput capabilities of the Redis engine. This separate, distributed cache layer allows your data tier to scale independently for more efficient use of compute resources in your application layer.</p> </div> </div> <!-- /.row --> </div> <!-- /.container -->
.rediscache { background-color: #eee; }
Notice in the redisCache.html the bindings through a service object. But where does this $scope object came from? The answer is behind the azurePanel directive which works as a container to display the azure services. Let’s examine its code:
(function() { 'use strict'; angular.module('azure', [ 'azure.core', 'azure.activeDirectory', 'azure.bizTalk', 'azure.redisCache', 'azure.scheduler' ]); })();
The azure.core module holds the api that fetches any data required to feed the azure service directives. We ‘ll take it a look later on.
(function() { 'use strict'; angular .module('azure') .directive('azurePanel', azPanel); function azPanel() { //Usage: //<azure-panel user='selectedUser'></azure-panel>"/> var directive = { scope: { 'user': '=' }, controller: 'AzurePanel', restrict: 'E', templateUrl: 'external-lib/azurePanel/azurePanel.html' }; return directive; } })();
This directive will take a user’s id parameter in order to fetch user’s services through the api..
(function() { 'use strict'; angular .module('azure') .controller('AzurePanel', AzurePanel); AzurePanel.$inject = ['$scope', 'azureapi']; function AzurePanel($scope, azureapi) { $scope.servicesLoaded = false; activate(); function activate() { azureapi.getAzureServices($scope.user.id) .then(function(data) { $scope.user.services = data; $scope.servicesLoaded = true; }); } $scope.$watch('user', function(newValue, oldValue) { activate(); }); } })();
If the user change, we make sure to re-activate the azure directives for that user. Let’s see now how those azure directives are rendered.
<div ng-if="servicesLoaded"> <div class="row"> <div class="col-md-12" ng-repeat="service in user.services"> <azure-service service="service"> </azure-service> <hr/> </div> </div> </div>
Each service found for the selected user is passed to an azureService directive.
(function() { 'use strict'; angular .module('azure') .directive('azureService', azureService); azureService.$inject = ['$compile']; function azureService($compile) { //Usage: //<azure-service service='service'></azure-service>"/> var directive = { scope: { 'service': '=' }, link: link, restrict: 'E' }; return directive; function link(scope, element, attrs) { var newElement = angular.element(scope.service.type); element.append(newElement); $compile(newElement)(scope); } } })();
The only thing that this directive has to do is to angularJS compile the specific directive found in the scope.service.type. I believe now is the right moment to view the api..
(function() { 'use strict'; angular .module('azure.core') .factory('azureapi', azureapi); azureapi.$inject = ['$q', '$timeout']; function azureapi($q, $timeout) { var users = [ { id: 1, services: [ { type: '<biz-talk></biz-talk>', title: 'BizTalk Azure', image: 'images/bizTalk.png' }, { type: '<scheduler></scheduler>', title: 'Scheduler Azure', image: 'images/azure-scheduler.png' }, { type: '<redis-cache></redis-cache>', title: 'Redis Cache Azure', image01: 'images/redis-cache-01.png', image02: 'images/redis-cache-02.jpg' } ] }, { id: 2, services: [ { type: '<active-directory></active-directory>', title: 'Active Directory', image: 'images/activeDirectory.png' }, { type: '<redis-cache></redis-cache>', title: 'Redis Cache Azure', image01: 'images/redis-cache-01.png', image02: 'images/redis-cache-02.jpg' }, { type: '<biz-talk></biz-talk>', title: 'BizTalk Azure', image: 'images/bizTalk.png' }, ] } ]; var service = { getAzureServices: getAzureServices }; return service; function getAzureServices(userId) { var deferred = $q.defer(); var services = []; $timeout(function() { // amazing implementation services = users[userId - 1].services; deferred.resolve(services); }, 1000); return deferred.promise; } } })();
As you can see each service has definitely a type which is being mapped to certain azure-directives. Depending on the service type you may add any custom property that azure-service directive may need (dynamic behavior).
How to consume this external-lib?
The final product of such a library should be a JavaScript plus a css file. In order to achieve this you need to write grunt or gulp tasks that will concatenate, minify and generally optimize and package your library. I have done this using Gulp and you can see those tasks here.
In the SPA app’s side the only thing you need to declare in order to view a user’s services is the following:
<div class="container"> <azure-panel user="selectedUser"></azure-panel> </div>
The selectedUser is just an id which will be used from the lib’s api to fetch user’s services. In the app I have declared two users’s that have been registered in azure services in a different order. You can switch the selected user and check how this library works.
That’s it we have finished! We saw how to create a re-usable angularJS library and how to create dynamic views using AngularJS. You can download the project I created from here where you will also find instructions on how to run it.
In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.
.NET Web Application Development by Chris S. | |||