Writing Testable Application in AngularJS – Part 5


Throughout the series, we’ve seen how powerful AngularJS is and how quickly you can write unit tests for your application. In this post, we’ll take a look at grunt .

To create a build, simply execute:

$ grunt build

Above command does a lot of things such as run all tests, compile sass/compass files, minification of css and js, etc.

In our case, we’ve to do slight modifications in Gruntfile.js. If you want to include directories/files which were not created by the generator but to be needed in dist then just locate copy option in Gruntfile.js and add it in src array.

    copy: {
      dist: {
        files: [{
          .
          .
          .
          .
          src: [
            '*.{ico,txt}',
            '.htaccess',
            'components/**/*',
            'images/{,*/}*.{gif,webp}',
            'styles/fonts/*',
            // including data/ directory
            'data/*'
          ]
        }]
      }
    }

And at the bottom under grunt.registerTask('build') option, you can comment/remove tasks to be omitted during the build.

  grunt.registerTask('build', [
    'clean:dist',
    // Do not require below tasks
    // 'jshint',
    // 'test',
    // 'coffee',
    'compass:dist',
    'useminPrepare',
    'imagemin',
    'cssmin',
    'htmlmin',
    'concat',
    'copy',
    'cdnify',
    'ngmin',
    'uglify',
    'rev',
    'usemin'
  ]);

Grab source code

I really enjoyed writing this series and hope you like it. You can grab the source code from github. or checkout the working demo.

Writing Testable Application in AngularJS – Part 4


In the last post, we’ve seen how to leverage services in Angular to avoid duplicating code and how easy it is to test them. At the moment, our template does not interact with the player. In FIB template created previously, the submit button should get enabled once all inputs are filled, tryagain button will be activated after submitting, and if all 3 attempts lapse, we’ll enable showans button.

As you can see that footer bar does/can not come under FibCtrl so a hackish approach for this problem is to define Boolean variables on the $rootScope to toggle those buttons. Off-course It’ll work but its not elegant because all variables will be in global state.

Getting out of $rootScope hell

A better approach for such problem is to create a custom service and inject it into a controller. You can even share data between controllers.
Lets create a new service named checker. As usual, following command would generate checker.js and its test file.

$ yo angular:service checker

Then we’ll modify it as:

angular.module('eShellApp').factory('checker', function ($location) {
  return {
    // Enable/Disable submit button
    isSubmit: false,

    // Enable/Disable try again button
    isTryagain: false,

    // Enable/Disable show answer button
    isShowans: false,

    // Track attempt. Should increase on every try
    attempt: 1,

    // Max attempts
    totalAttempts: 3,

    // Click handler for submit button
    submit: function () { },

    // Click handler for try again button
    tryagain: function () { },

    // Click handler for show answer button
    showAns: function () { },

    // Getter for all above properties/methods
    getProperty: function(property) {
      return this[property];
    },

    // Setter for all above properties/methods
    setProperty: function(property, value) {
      this[property] = value;
    },

    // Invoke click handlers submit()/tryagain()/showans() after clicking buttons
    invoke: function(property) {
      this[property]();
    },

    // reset all per template, will be called from controllers
    reset: function($scope) {
      this.setProperty('isSubmit', false);
      this.setProperty('isTryagain', false);
      this.setProperty('isShowans', false);
      this.setProperty('submit', $scope['submit']);
      this.setProperty('tryagain', $scope['tryagain']);
      this.setProperty('showans', $scope['showans']);
      this.setProperty('attempt', 1);
      this.setProperty('currentTemplate', parseInt($location.path().substring(1), false));
    }
  };
});

We’ll quickly test the above service to make it future proof. Here jamine’s spyOn method creates a spy for checker.submit() method. And when we call checker.invoke(‘submit’), it calls checker.submit() from within. Finally we just have to check whether checker.submit() has been called or not.

describe('Service: checker', function () {

  // load the service's module
  beforeEach(module('eShellApp'));

  // instantiate service
  var checker;
  beforeEach(inject(function (_checker_) {
    checker = _checker_;
  }));

  it('should get and set properties', function () {
    expect(checker.getProperty('isSubmit')).toBeFalsy();
    checker.setProperty('isSubmit', true);
    expect(checker.getProperty('isSubmit')).toBeTruthy();
  });

  it('should call method properties', function() {
    spyOn(checker, 'submit').andCallFake(function() { });
    checker.invoke('submit');
    expect(checker.submit).toHaveBeenCalled();
  });
});

Creating a controller without route

Finally we’ll create a new controller named footer which will consume checker service.

$ yo angular:controller footer

lets modify footer.js. isEnable() method enables/disables buttons. invoke() method calls checker.invoke() which then call methods defined in the template controller.

angular.module('eShellApp').controller('FooterCtrl', function ($scope, checker) {
  $scope.isEnable = function(button) {
    return checker.getProperty(button);
  };

  $scope.invoke = function(button) {
    checker.invoke(button);
  };

  $scope.attempt = function() {
    return checker.getProperty('attempt');
  };

  $scope.totalAttempts = function() {
    return checker.getProperty('totalAttempts');
  };
});

We’ll update footer bar in index.html to use above methods. ng-disabled enables/disables a button based on expression. ng-click lets you attach click handler the natural way.

<div class="navbar navbar-inverse navbar-fixed-bottom ng-cloak" ng-controller="FooterCtrl">
  <div class="navbar-inner">
    <div class="container">
      <div class="btn-group pull-left">
        <button class="btn btn-inverse input-small" value="Attempt {{attempt()}} of {{totalAttempts()}}" disabled></button>
      </div>

      <div class="btn-group pull-left">
        <button class="btn btn-primary" type="button" ng-disabled="!isEnable('isSubmit')" ng-click="invoke('submit')">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Submit&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</button>
        <button class="btn btn-warning" type="button" ng-disabled="!isEnable('isTryagain')" ng-click="invoke('tryagain')">&nbsp;&nbsp;&nbsp;Try Again&nbsp;&nbsp;&nbsp;</button>
        <button class="btn btn-success" type="button" ng-disabled="!isEnable('isShowans')" ng-click="invoke('showans')">Show Answer</button>
      </div>
      <div class="btn-group pull-right">
        <button class="btn">&laquo;</button>
        <button class="btn">1 of 4</button>
        <button class="btn">&raquo;</button>
      </div>
    </div>
  </div>
</div>

You may remember that we’d attached validate() method on ng-change to all input boxes in FIB template to toggle submit button. Lets define that method:

$scope.validate = function(isValid) {
   checker.setProperty('isSubmit', isValid);
};

Now define click handlers for submit, tryagain and show answer buttons. In submit handler, we just validate input boxes and make them readonly. For example, every input box has input_0 as model, input_0_valid tracks the validity of the model and input_0_readonly toggles readonly html attribute on input boxes. Try again and show answer handlers remove invalid and show valid answers respectively.

  eywa.worship('data/fib.json').then(function(data) {
    $scope.data = data.data;
  });

  $scope.validate = function(isValid) {
    checker.setProperty('isSubmit', isValid);
  };

  $scope.setClass = function(param) {
    if (!angular.isDefined(param)) return '';
    return param === true ? 'success' : 'error';
  };

  $scope.submit = function() {
    var isValid,
        totalValid = 0,
        attemptsLapse = checker.getProperty('attempt') >= checker.getProperty('totalAttempts');

    $scope.data.choices.forEach(function(choice, index) {
      isValid = choice.val2.split(',').indexOf($scope['input_' + index]) !== -1;

      if (isValid) totalValid++;

      $scope['input_' + index + '_valid'] = isValid;
      $scope['input_' + index + '_readonly'] = true;
    });

    checker.setProperty('isTryagain', totalValid !== $scope.data.choices.length && !attemptsLapse);
    checker.setProperty('isShowans', totalValid !== $scope.data.choices.length && attemptsLapse);
    checker.setProperty('isSubmit', false);
  };

  $scope.tryagain = function() {
    $scope.data.choices.forEach(function(choice, index) {
      if (!$scope['input_' + index + '_valid']) {
        $scope['input_' + index + '_valid'] = undefined;
        $scope['input_' + index + '_readonly'] = false;
        $scope['input_' + index] = '';
      }
    });
    checker.setProperty('isTryagain', false);
    checker.setProperty('attempt', checker.getProperty('attempt') + 1);
  };

  $scope.showans = function() {
    $scope.data.choices.forEach(function(choice, index) {
      if (!$scope['input_' + index + '_valid']) {
        $scope['input_' + index + '_valid'] = true;
        $scope['input_' + index + '_readonly'] = true;
        $scope['input_' + index] = choice.val2.split(',')[0];
      }
    });
    checker.setProperty('isShowans', false);
  };

  // Reset footer bar on Template load
  checker.reset($scope);

checker.reset() method simply sets attempt to 1, disables all buttons, etc. when a template loads.
If an input has a valid model value, it will be highlighted in green or in red otherwise. For that purpose, we’ve setClass() method defined above and need to use in ng-class directive inside parseBracket.js directive.

angular.module('eShellApp').directive('parseBracket', function ($compile) {
  return {
    .
    .
    new RegExp(choice.val1.replace(/\[/g, '\\[').replace(/\]/g, '\\]'), 'g'),
    '<span class="control-group" ng-class="setClass(input_' + index + '_valid)">' +
    '<input type="text" ' +
      'class="input-medium" ' +
      'autocorrect="off" ' +
      'autocapitalize="off" ' +
      'autocomplete="off" ' +
      'ng-change="validate(frmFIB.$valid)" ' +
      'ng-readonly="input_' + index + '_readonly" ' +
      'ng-model="input_' + index + '"' +
      'required />' +
     '</span>'
    .
    .
  };
});

As you could see in checker.js service, we’d already defined currentTemplate and totalTemplates variables for a reference. Now in footer.js, we’ll add 4 more methods:

App.controller('FooterCtrl', function ($scope, checker, $location) {
  $scope.currentTemplate = function() {
    return checker.getProperty('currentTemplate');
  };

  $scope.totalTemplates = function() {
    return checker.getProperty('totalTemplates');
  };

  $scope.prevTemplate = function() {
    $location.path('/' + (checker.getProperty('currentTemplate') - 1));
  };

  $scope.nextTemplate = function() {
    $location.path('/' + (checker.getProperty('currentTemplate') + 1));
  };
});

The $location service parses the URL in the browser address bar and makes the URL available to your application.

And update index.html:

   <div class="navbar navbar-inverse navbar-fixed-bottom ng-cloak" ng-controller="FooterCtrl">
      <div class="navbar-inner">
        <div class="container">
          .
          .
          .
          .   
          <div class="btn-group pull-right">
            <button class="btn" ng-disabled="currentTemplate() == 1" ng-click="prevTemplate()">&laquo;</button>
            <button class="btn" ng-bind-template="{{currentTemplate()}} of {{totalTemplates()}}">? of ?</button>
            <button class="btn" ng-disabled="currentTemplate() == totalTemplates()" ng-click="nextTemplate()">&raquo;</button>
          </div>
        </div>
      </div>
    </div>

Here, ng-bind-template directive specifies that the element text should be replaced with the template provided. Unlike ngBind the ngBindTemplate can contain multiple {{ }} expressions.

Wrap up

In the next post, we’ll see how to make the application production ready using grunt build.. Checkout Part 5.

Writing Testable Application in AngularJS – Part 3


In the last post, we’d used $http service inside a controller to read JSON data which will be quite redundant once we’ll have more templates. So to avoid the redundant code, we’ll abstract it into a custom service which then can be injected in any controller.

Creating a Service

Fire up a terminal and run following command:

$ yo angular:service eywa

We’ve to modify the existing code for eywa service to suit our need. The function worship() takes a JSON file url to read the data from and returns a promise. Notice that we’ll need $http service to be injected here now.

angular.module('eShellApp').factory('eywa', function ($http) {
  return {
    worship: function (request) {
      return $http.get(request);
    }
  };
});

Now we’ll update our controller by injecting eywa and removing $http service from FibCtrl. You can see that nothing has changed but the code is now more robust and testable.

App.controller('FibCtrl', function ($scope, eywa) {
  eywa.worship('data/fib.json').then(function(data) {
    $scope.data = data.data;
  });
});

Testing a Service

In test directory, you may find a test file for the service automatically generated by yeoman generator. What is happening here is that $injector will strip leading and trailing underscores when inspecting function’s arguments to retrieve dependencies. This is useful trick since we can save the variable name without underscores for the test itself. This way we can keep a reference of the service, to use in the test, using the variable named exactly like a service in the $injector.

You can see that the $httpBackend mock allows us to specify expected requests and prepare fake responses. By using the flush() method we can simulate a HTTP response arriving from a back-end at a chosen moment. The verifyNoOutstandingExpectation method verifies that all the expected calls were made ($http methods invoked and responses flushed) while the verifyNoOutstandingRequest call makes sure that code under test didn’t trigger any unexpected XHR calls. Using those 2 methods we can make sure that the code under test invoked all the expected methods and only the expected ones. (Shamelessly copied from AngularJS Web Application Development)

describe('Service: eywa', function () {

  // load the service's module
  beforeEach(module('eShellApp'));

  // instantiate service
  var eywa, $httpBackend;
  beforeEach(inject(function (_eywa_, _$httpBackend_) {
    eywa = _eywa_;
    $httpBackend = _$httpBackend_;
  }));

  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  it('should read JSON data', function () {
    $httpBackend.whenGET('data/fib.json').respond('{"text":"AngularJS is [[1]].", "choices":[{"val1":"[[1]]", "val2":"awesome,powerful"}]}');

    eywa.worship('data/fib.json').success(function(data) {
      expect(data.text).toBe('AngularJS is [[1]].');
    });
    $httpBackend.flush();
  });
});

beforeEach() and afterEach() execute before and after every it() statement respectively. Notice that the request url passed to worship() method should match up with the one in whenGet().

Wrap up

In the next post, we’ll make all buttons from footer bar interact with the template. Checkout Part 4.

Writing Testable Application in AngularJS – Part 2


In the last post, we’ve setup yeoman and karma, and then scaffold our application. If you look at app/app.js which currently contains the routing information. Yeoman generator has automagically mapped #/main route and created its associated files mainly main.js and main.html – which we do not need for this application.

We’ll do some cleanup first.

Cleanup

In index.html, remove the unnecessary Google Analytics code and main.js included. Delete following files:

$ cd eShell
$ rm app/scripts/controllers/main.js app/views/main.html test/spec/controllers/main.js

And finally replace

<div class="container"></div>

with

<div class="navbar navbar-inverse navbar-fixed-top">
  <div class="navbar-inner">
    <div class="container"><a class="brand" href="#">eShell - A player for eLearning Courses</a></div>
  </div>
</div>
<div style="margin-top: 40px;"><div ng-view></div></div>
<div class="navbar navbar-inverse navbar-fixed-bottom ng-cloak">
  <div class="navbar-inner">
    <div class="container">
      <div class="btn-group pull-left"><button class="btn btn-inverse input-small" type="text" disabled="disabled" value="Attempt 1 of 3"></button></div>
      <div class="btn-group pull-left">
        <button class="btn btn-primary" type="button">     Submit      </button>
        <button class="btn btn-warning" type="button">   Try Again   </button>
        <button class="btn btn-success" type="button">Show Answer</button>
      </div>
      <div class="btn-group pull-right">
        <button class="btn">«</button>
        <button class="btn">1 of 4</button>
        <button class="btn">»</button>
      </div>
    </div>
  </div>
</div>

First Route

Using yeoman, it takes a split second to create a new route. Just run the following command in a Terminal:

$ yo angular:route fib
create app/scripts/controllers/fib.js
create test/spec/controllers/fib.js
create app/views/fib.html

As you could see, this has created 3 files, a controller, a view and a test. If you look at app.js now, you will see a new route information has automagically been inserted without breaking a thing. Love it!

Lets now update our app.js from:

angular.module('eShellApp', []).config(function ($routeProvider) {
  $routeProvider
    .when('/', {
    templateUrl: 'views/main.html',
    controller: 'MainCtrl'
  })
  .when('/fib', {
    templateUrl: 'views/fib.html',
    controller: 'FibCtrl'
  })
  .otherwise({
    redirectTo: '/'
  });
});

to:

angular.module('eShellApp', []).config(function ($routeProvider) {
  $routeProvider
  .when('/1', {
    templateUrl: 'views/fib.html',
    controller: 'FibCtrl'
  })
  .otherwise({
    redirectTo: '/1'
  });
});

Go and check out http://localhost:9000/

Creating our First Template – Fill in the blanks

This template creates a fill in the blank question backed by JSON data including valid answers. To read the JSON file, angular gives a nifty little service, $http that facilitates communication with the remote HTTP servers via the browser’s XMLHttpRequest object or via JSONP. The reason why I prefer to use $http service because it takes single argument (object) and returns a promise with two specific methods, success and error as it is based on Deferred/Promise APIs.

so, lets create fib.json and put data:

$ touch app/data/fib.json
{
  "text":"AngularJS is originally created in [[1]] by [[2]] and [[3]].",
  "choices":[
    {
      "val1":"[[1]]",
      "val2":"2009,09"
    },
    {
      "val1":"[[2]]",
      "val2":"misko,Misko Hevery"
    },
    {
      "val1":"[[3]]",
      "val2":"adam,adam abrons"
    }
  ]
}

Now, lets modify our fib.js controller to read the above JSON data. We’ll first expose $http object to our controller and $compile service to process the data before injecting into a view.

angular.module('eShellApp').controller('FibCtrl', function ($scope, $http, $compile) {
  $http.get('data/fib.json').then(function(data) {
    $scope.data = data.data;
    var text = '<div>' + data.data.text + '</div>';

    // A bit complex logic to replace every instance of [] with input boxes
    data.data.choices.forEach(function(choice, index) {
      text = text.replace(
        new RegExp(choice.val1.replace(/\[/g, '\\[').replace(/\]/g, '\\]'), 'g'),
        '<span class="control-group">' +
        ' <input type="text" class="input-medium" ' +
        '   autocorrect="off" ' +
        '   autocapitalize="off" ' +
        '   autocomplete="off" ' +
        '   ng-change="validate(frmFIB.$invalid)" ' +
        '   required ' +
        '   data-index="' + index + '" ' +
        '   ng-model="input_' + index + '" />' +
        '</span>'
      );
    });
    $scope.text = $compile(text)($scope).html();
  });
});

We’re just looping through choices we’ve in JSON and replacing the occurrences of [] with input boxes. Notice that I’ve used ng-change directive that will call validate() method on every keypress to toggle submit button. You may be wondering why have I wrapped data.data.text in a div? That’s because $compile service requires it, otherwise you will get some nasty error. Finally I’ve bound compiled text to $scope in order to use it in a view below.

<div class="contentWrapper">
  <div class="content">
    <h3 class="pagination-centered">Fill in the Blanks</h3>
    <form class="form-inline" name="frmFIB" novalidate="">
      <div class="pagination-centered" ng-bind-html-unsafe="text"></div>
    </form>
  </div>
</div>

Again, ng-bind-html-unsafe evaluates the expression (text, in this case) and put it inside the element it bound to (div, in this case). In addition, it is also helpful to get rid of FOUC or precompiled values issue with angular (where you see {{expr}} before the evaluation, mostly in IE).

Enter the Dragon: Directive

Its very bad to do the DOM manipulation inside controller and directive is the best option Angular provides. Lets create our first directive.

$ yo angular:directive parseBracket
create app/scripts/directives/parseBracket.js
create test/spec/directives/parseBracket.js

Now we’ll move the entire parsing logic into above directive. But before that lets replace ng-bind-html-unsafe we’d used earlier with parse-bracket.

<div class="contentWrapper">
  <div class="content">
    <h3 class="pagination-centered">Fill in the Blanks</h3>
    <form class="form-inline" name="frmFIB" novalidate="">
      <div class="pagination-centered" parse-bracket="{{data}}"></div>
    </form>
  </div>
</div>

We’re sending JSON object (data) being parsed to the directive.

According to the author, Directive is a way to teach HTML a new trick. In this case, I’m restricting the directive to be an attribute only and inside link function, $observe observes the changes of parseBracket attribute which contains interpolation. Rest is same borrowed from the controller ;-). You can learn more about directive on adobe or onehungrymind.

angular.module('eShellApp').directive('parseBracket', function ($compile) {
  return {
    restrict: 'A',
    link: function postLink(scope, element, attrs) {
      attrs.$observe('parseBracket', function(val) {
        var params = angular.fromJson(val),
            text = '<div>' + params.text + '</div>';

        if (angular.isDefined(params.choices)) {
          params.choices.forEach(function(choice, index) {
            text = text.replace(
              new RegExp(choice.val1.replace(/\[/g, '\\[').replace(/\]/g, '\\]'), 'g'),
              '<span class="control-group">' +
                '<input type="text" ' +
                  'class="input-medium" ' +
                  'autocorrect="off" ' +
                  'autocapitalize="off" ' +
                  'autocomplete="off" ' +
                  'ng-change="validate(frmFIB.$valid)" ' +
                  'ng-model="input_' + index + '"' +
                  'required />' +
              '</span>'
            );
          });
        }

        element.html(text);
        $compile(element.contents())(scope);
      });
    }
  };
});

Lets Test ’em

You have noticed that when we created the directive using yeoman generator, it also created the test file for the same. We’re going to modify it a bit to pass through the test.


describe('Directive: parseBracket', function () {
  beforeEach(module('eShellApp'));

  var element, $scope;

  it('should replace [] with input boxes', inject(function ($rootScope, $compile) {
    $scope = $rootScope.$new();
    $scope.data = {
      text    : 'AngularJS is [[1]]',
      choices : [{
        "val1": "[[1]]",
        "val2": "awesome,powerful"
      }]
    };
    element = angular.element('<div parse-bracket="{{data}}"></div>');
    element = $compile(element)($scope);
    $rootScope.$digest();

    expect(element.html()).toBe('<div class="ng-scope">AngularJS is <span class="control-group" ng-class="setClass(input_0_valid)"><input type="text" class="input-medium ng-pristine ng-invalid ng-invalid-required" autocorrect="off" autocapitalize="off" autocomplete="off" ng-change="validate(frmFIB.$valid)" ng-readonly="input_0_readonly" ng-model="input_0" required=""></span></div>');
  }));
});

Wrap up

In a next post, We’ll clean up our controller and write some tests for it too. Checkout Part 3.

Writing Testable Application in AngularJS – Part 1


This will be a 5-Part series to showcase how easy and fun it is to write an AngularJS application. It is a first article of the series where I’m going to give you an overview of the whole application and the utilities we’ll need to get going.

Application Overview

The application we’ll build is called an eLearning player – a building block for more conventional eLearning courses to be run on created with authoring tools. You can learn more about it on upsidelearning and wikipedia. Such players basically consist of different kinds of templates/activities backed by XML/JSON data (produced by Authoring Tools).

LIVE DEMO

Application Overview
Application Overview

Prerequisites

These are some of the tools we’ll use to make the development more enjoyable and sexy. I’m not going to fill this post with all the installation stuff so visit below sites and have it installed for you.

  • Node.js – Server side Javascript that below tools are based on
  • Yeoman – The Workflow Tool
  • Karma Runner – Spectacular Test Runner for JavaScript

Scaffolding

I assume that you have setup above mentioned tools so lets create a scaffold for our application. Type following commands in a terminal:

$ mkdir eShell && cd eShell
$ yo angular
Error angular 

Invalid namespace: angular (or not yet registered).
 Registered: 4 namespace(s). Run with the `--help` option to list them.

In case you get above error, run following commands:

$ sudo npm install generator-angular
$ sudo npm install generator-karma
$ yo angular
Would you like to include Twitter Bootstrap? (Y/n) Y
If so, would you like to use Twitter Bootstrap for Compass (as opposed to vanilla CSS)? (Y/n) Y
Would you like to include angular-resource.js? (Y/n) Y
Would you like to include angular-cookies.js? (Y/n) n
Would you like to include angular-sanitize.js? (Y/n) Y

Finally, run following command as per the instruction to install the required dependencies.

$ sudo npm install && bower install
$ grunt server

If you see, Allo, Allo in a browser that means we’ve achieved our first target. Good Job!!!

Karma Testrunner Setup

In case you want Karma to suit your need, you can simply edit karma.conf.js and karma-e2e.conf.js. Mainly we need to change a port, runnerPort (if ports are in use) and browsers to run tests against. By default, it’s Chrome.

// Karma configuration

// base path, that will be used to resolve files and exclude
basePath = './';

// enable / disable watching file and executing tests whenever any file changes
autoWatch = true;

Finally, run below command which should capture browsers for testing as per karma.conf and will evaluate all tests on every file save (in your choice of IDE).

$ karma start

Checkout Part 2.

AngularJS module for jQueryUI draggable and droppable


Last year, I played a lot with jQueryUI and mostly draggable/droppable along with AngularJS and it was a bit of a pain so I decided to write a directive for it which should make it easy for others to implement such functionalities.

Demos and much more

http://codef0rmer.github.com/angular-dragdrop/#/

Fork it on Github

https://github.com/codef0rmer/angular-dragdrop

and written test cases also 🙂
https://github.com/codef0rmer/angular-dragdrop/blob/master/test/index.html

Good Night!