Angular: Communication between controller and directive in a testable way? - javascript

I just started learning Angular and I'm trying to use it to build a file uploader which allows me to drag files to it and they will be uploaded. Relatively simple. But I'm kind of unsure on how to communicate between my controller and my directive and have it be testable. My first thought was to do something like this:
angular.module('UploadApp', [])
.controller('UploadController', ['$scope', function($scope) {
$scope.files = [];
$scope.$watch('files', function() {
//TODO: Upload the files somehow, I haven't written the service to do this yet
}, true);
}])
.directive('UploaderDropzone', function() {
return {
restrict: 'A',
scope: {
files: '=UploaderFiles'
},
link: function(scope, element) {
function addFile(file) {
if (file.type && (file.type.indexOf('image/') === 0 ||
file.type.indexOf('video/') === 0)) {
scope.$apply(function() {
scope.files.push(file);
});
}
}
scope.watch('files', function() {
//TODO: Do something to display the status of the uploads
}, true);
element.on('dragenter dragover', function(event) {
element.addClass('hover');
event.preventDefault();
});
element.on('dragleave drop', function(event) {
var files = event && ((event.target && event.target.files) ||
(event.dataTransfer && event.dataTransfer.files));
if (files) {
[].forEach.call(files, function(file) {
addFile(file);
});
}
element.removeClass('hover');
event.preventDefault();
});
}
};
});
And then in the template:
<div ng-controller="UploadController">
<div uploader-dropzone uploader-files="files"></div>
</div>
I was planning to just wrap the files in an object that would let me communicate the upload progress, success status back to the directive too, etc. But I don't know how I'm going to test the this code since it seems like it's going to be a pain to trigger the code inside the $watch? Is there a better way to do this?

What you can do is you can use ng-model in your directive, from your point I can understand that what you want to catch values of files.
Code will be like
.directive('UploaderDropzone', function() {
return {
restrict: 'A',
scope: {
files: '=UploaderFiles'
},
link: function(scope, element,ngmodel) {
or you can use some service to communicate between the two like below
myModule.directive('myComponent', function(mySharedService) {
return {
restrict: 'E',
controller: function($scope, $attrs, mySharedService) {
I have found one example for the above approach, may be that can help you in a more better way. try this JS code.
var myModule = angular.module('myModule', []);
myModule.factory('mySharedService', function($rootScope) {
var sharedService = {};
'sharedService.message = '';
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
myModule.directive('myComponent', function(mySharedService) {
return {
controller: function($scope, $attrs, mySharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'Directive: ' + mySharedService.message;
});
},
replace: true,
template: '<input>'
};
});
function ControllerZero($scope, sharedService) {
$scope.handleClick = function(msg) {
sharedService.prepForBroadcast(msg);
};
$scope.$on('handleBroadcast', function() {
$scope.message = sharedService.message;
});
}

Related

Better approaches than using $broadcast to update input in directive?

Currently I have a list of contacts on controller A. When I click on one of the contacts, it is broadcasting the contact info to controller B and to the datepicker directive in controller B. This is working but is there a better way to update the input on the datepicker directive?
app.directive('datePickerDirective', [function () {
return {
restrict: 'AE',
require: 'ngModel',
scope: {
datepickerNgModel: '=',
datepickerId: '#'
},
templateUrl: 'Content/app/directives/templates/DatePicker.html',
link: function ($scope, element, attrs, ngModel) {
$scope.$watch(function () {
ngModel.$setViewValue($scope.datepickerNgModel);
return ngModel.$modelValue;
});
$scope.$on('data-from-component-a', function (event, data) {
$('#' + $scope.datepickerId).val(data.date);
})
}
}
}]);
I would avoid using events ($broadcast) here. You can do it by using a nice factory which handles the data for your components. You did not gave any information about your datepicker and controllers, so I created an abstract example which delivers you the basic handling.
> Share data via factory between controllers - demo fiddle
View
<div ng-controller="MyCtrl">
<button ng-click="publishData()">
Publish data
</button>
<button ng-click="resetData()">
Reset data
</button>
</div>
<div ng-controller="MyOtherCtrl">
<my-directive my-model="data.getData()"></my-directive>
</div>
AngularJS application
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope, myFactory) {
$scope.publishData = function() {
myFactory.publishData();
}
$scope.resetData = function() {
myFactory.resetData();
}
});
myApp.controller('MyOtherCtrl', function($scope, myFactory) {
$scope.data = myFactory;
});
myApp.directive('myDirective', function () {
return {
restrict: 'E',
template: '{{myModel}}',
scope: {
myModel: '='
},
link: function (scope, element, attrs) {
scope.$watch('myModel', function (newValue, oldValue) {
console.log(newValue);
// $('#' + $scope.datepickerId).val(newValue);
});
}
}
});
myApp.factory('myFactory', function() {
return {
contactInfo: '',
publishData: function() {
this.contactInfo = 'Sdfsdfsdf';
},
resetData: function() {
this.contactInfo = null;
},
getData: function () {
return this.contactInfo;
}
}
});

Undefined function in AngularJS directive unit test

I am trying to write a unit test for the toggleDetails function defined inside the following AngularJS directive:
angular.module('hadoopApp.cluster.cluster-directive', [])
.directive('cluster', [function() {
return {
templateUrl:'components/cluster/cluster.html',
restrict: 'E',
replace: true,
scope: {
clusterData: '=',
showDetails: '='
},
link: function(scope, element, attrs) {
scope.toggleDetails = function() {
console.log('Test');
scope.showDetails = !scope.showDetails;
};
},
// Default options
compile: function(tElement, tAttrs){
if (!tAttrs.showDetails) { tAttrs.showDetails = 'false'; }
}
};
}]);
And this is the unit test:
'use strict';
describe('hadoopApp.cluster module', function() {
// Given
beforeEach(module('hadoopApp.cluster.cluster-directive'));
var compile, mockBackend, rootScope;
beforeEach(inject(function($compile, $httpBackend, $rootScope) {
compile = $compile;
mockBackend = $httpBackend;
rootScope = $rootScope;
}));
var dummyCluster;
beforeEach(function() {
dummyCluster = {
id:"189",
name:"hadoop-189",
exitStatus:0
};
mockBackend.expectGET('components/cluster/cluster.html').respond(
'<div><div ng-bind="clusterData.name"></div></div>');
});
it('should toggle cluster details info', function() {
var scope = rootScope.$new();
scope.clusterData = dummyCluster;
// When
var element = compile('<cluster' +
' cluster-data="clusterData" />')(scope);
scope.$digest();
mockBackend.flush();
// Then
var compiledElementScope = element.isolateScope();
expect(compiledElementScope.showDetails).toEqual(false);
// When
console.log(compiledElementScope);
compiledElementScope.toggleDetails();
// Then
expect(compiledElementScope.showDetails).toEqual(true);
});
afterEach(function() {
mockBackend.verifyNoOutstandingExpectation();
mockBackend.verifyNoOutstandingRequest();
});
});
The test fails when calling compiledElementScope.toggleDetails() because the toggleDetails function is undefined:
TypeError: undefined is not a function
Printing the content of the isolated scope inside compiledElementScope I can see that in fact the function is not included in the object.
So, it looks like the toggleDetails function is not included in the isolated scope but I don't know why.
If you use the compile function within a directive, the link function is ignored. You should return the function within the compile method:
compile: function (tElement, tAttrs) {
if (!tAttrs.showDetails) {
tAttrs.showDetails = 'false';
}
return {
post: function (scope, element, attrs) {
console.log('Test');
scope.toggleDetails = function () {
console.log('Test');
scope.showDetails = !scope.showDetails;
};
}
};
}
Also, in order to make the test work, you should add:
scope.showDetails = false;
And the binding to the directive (because you require two values):
var element = compile('<cluster' +
' cluster-data="clusterData" show-details="showDetails" />')(scope);
Jsfiddle: http://jsfiddle.net/phu7sboz/

How to implement a disable when loading directive

Hi I have a variable on my scope named loadingdata. It will have the values true or false to determine if data is loading or not. I would like to put an attribute on an element to disable it if data is loading. Here is the code I already have but it is not working:
module.directive('disableWhenLoadingData', function () {
return {
restrict: 'A',
scope: {},
link: function ($scope, element, attrs) {
$scope.$watch('loadingData', function(newValue, oldValue) {
element.attr('disabled', newValue);
});
}
};
});
any ideas
You can use Angular's own ngDisabled directive instead of writing your own.
Service:
module.factory('GetDataService', function ($http) {
return {
getCustomers: function() {
return $http({ url: '/someurl', method: 'GET'});
}
}
});
Directive:
module.directive('disableWhenLoadingData', function (GetDataService) {
return {
restrict: 'A',
scope: {},
link: function ($scope, element, attrs) {
$scope.loadingData = true;
GetDataService.getCustomers().success(function (data) {
$scope.loadingData = false;
});
}
};
});
Generally I set $scope.loading in my controller, and my button or whatever i set ng-disabled.
In my controller:
$scope.loadData = function () {
$scope.loading = true;
$http
.get('url')
.success(function (ret) {
$scope.loading = false;
});
}
In my view:
<button ng-disabled="loading" ng-click="loadData()">{{loading? 'loading Data' : 'Submit'}}</button>

How to get data from controller to Factory in angularjs

I am newbie to Angularjs World. I want to fetch some data from angularjs controller to Angularjs Factory.Controller and factory are defined in different js files.
code from Factory(File name Application.js)
var app=angular.module('myApp',[])
app.factory('autoCompleteDataService', [function(MyController) {
return {
getSource: function() {
return MyController.getFirstName();
}
}
}]);
app.directive('autoComplete', function(autoCompleteDataService) {
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
$(elem).autocompleteArray(autoCompleteDataService.getSource(), {
minLength: 2
});
}
}})
Controller code (File Name Controller.js)
function MyController($scope){
this.getFirstName= function ()
{
var arrayFName=[];
for(var k=0;k< $scope.MyData.length;k++)
{
arrayFName.push($scope.MyData[k].fname);
}
return arrayFName;
}
MyData is array containing some hard coded value for 'fname'
When i ran this code got error 'Error: MyController is undefined'. Is it possible to fetch data from controller if yes then how?
You should have source, or firstName, defined in the factory, and then set it from the controller. It will then be accessible from other controllers which use the factory.
var app=angular.module('myApp',[])
app.factory('autoCompleteDataService', [function () {
var _source;
return {
getSource: function () {
return _source;
},
setSource: function (source) {
_source = source;
}
}
}]);
And then subsequent controllers might be like:
app.controller('someController', ['$scope', 'autoCompleteDataService',
function ($scope, autoCompleteDataService) {
$scope.source = autoCompleteDataService.getSource();
// or even...
// $scope.getFirstName = autoCompleteDataService.getSource;
}]);

angularjs: access controller of directive

I'm unit testing one of my directives. Its basic structure is like this
angular.module('MyApp')
.directive('barFoo', function () {
return {
restrict: 'E',
scope: {},
controller: function ($scope, $element) {
this.checkSomething = function() { .... }
},
link: function(scope, element) { .... }
}
});
In my unit test I want to test the function 'checkSomething', so I tried
var element = $compile('<barFoo></barFoo>')(scope);
var controller = element.controller()
...
However, controller is undefined. Is it possible to access the controller of the directive ?
the glue is your scope so you can do
controller: function ($scope, $element) {
this.checkSomething = function() { .... }
$scope.controller = this;
},
but i think it would be best practice to attach every function to the scope like
controller: function ($scope, $element) {
$scope.checkSomething = function() { .... }
},
and then its
var element = $compile('<barFoo></barFoo>')(scope);
var checksomthingResult = scope.checkSomething ()

Categories