Issues with controllers in Directives - javascript

I'm trying to update some code in a small personal project which uses angular to conform to better practices, and I have heard that the future of Angular can be mimicked in a way by putting a lot of functionality into controllers of directives. I'm not sure how correct my understanding is but it seems like a clean way of organizing code.
Anyways, to get to the point of my issue, I can't seem to get the isolate scope to work when I give my directive a controller. I've been googling my brains out trying to figure out what the issue is, and I saw many topics about it, but none which solved my issue.
Here's a code fragment:
angular.module('myCongresspersonApp')
.directive('congressPersonPane', function () {
var controller = [function() {
}];
return {
templateUrl: 'app/congressPersonPane/congressPersonPane.html',
restrict: 'EA',
scope: {
congressPerson: '=info'
}//,
// controller: controller,
// controllerAs: 'paneCtrl',
// bindToController: true
};
});
This is really just a way to test before I move functionality around, but when I uncomment those lines, I no longer have access to the isolate scope I pass in and all the data accessed through that is gone (it is an array object in a ng-repeat).
I also have a similar problem in a directive which sits inside this directive. That problem makes me even more confused, as I can correctly use a method if I define it under the $scope, but when I use controllerAs, I cannot use that method. So I am pretty stumped as I pulled this implementation (to remove scope) from this website (mentioned by Lauren below)
here's the code for that:
'use strict';
angular.module('myCongresspersonApp')
.directive('voteRecord', function () {
var controller = ['$scope', 'sunlightAPI', function ($scope, sunlightAPI) {
var voteCtrl = this;
voteCtrl.voteInfo = [];
voteCtrl.test = 'Test';
voteCtrl.pageNumber = 1;
voteCtrl.repId = '';
console.log('inside controller definition');
voteCtrl.getVotingRecord = function(repId) {
console.log('inside method');
voteCtrl.repId = repId;
var promiseUpdate = sunlightAPI.getVotes(repId, pageNumber);
promiseUpdate.then(function(votes) {
console.log('fulfilled promise');
voteCtrl.voteInfo = votes;
console.log(voteCtrl.voteInfo);
}, function(reason) {
console.log('Failed: ' + reason);
}, function(update) {
console.log('Update: ' + update);
});
};
voteCtrl.nextPage = function() {
voteCtrl.pageNumber++;
voteCtrl.getVotingRecord(voteCtrl.repId, voteCtrl.pageNumber);
};
voteCtrl.previousPage = function() {
voteCtrl.pageNumber--;
voteCtrl.getVotingRecord(voteCtrl.repId, voteCtrl.pageNumber);
};
}];
return {
restrict: 'EA',
scope: {
repId: '=representative'
},
controller: controller,
contollerAs: 'voteCtrl',
bindToController: true,
templateUrl: 'app/voteRecord/voteRecord.html',
};
});
I'm not sure if that issue is related to this issue or not, but they seem similar. Any help or directions to resources which could help would be really appreciated, as I don't want to be writing code where my conventions are constantly changing because I don't fully understand why one thing works.
Thanks!

I'm not sure if I fully understand what your problem is, but it sounds like you are having problems accessing $scope from the controller. You can actually pass in the scope to the controller, like this:
angular.module('myCongresspersonApp')
.directive('congressPersonPane', function () {
var myController = function($scope) {
// use $scope in here
};
return {
templateUrl: 'app/congressPersonPane/congressPersonPane.html',
restrict: 'EA',
scope: {
congressPerson: '=info'
},
controller: ['$scope', myController]
};
});
This blog post details how to use controllers in your directives. Also the Angular documentation will explain more too. Good luck!

Related

How to setup $emit in directive in my case

I am trying to use $emit in my code for a directive.
I have
(function () {
angular
.module('myApp')
.directive('testDirective', testDirective);
function testDirective() {
var directive = {
link: link,
restrict: 'A',
controller: testCtrl,
controllerAs: 'vm'
};
return directive;
function link(scope, element, attrs) {
}
}
function testCtrl() {
var vm = this;
// do something
vm.$emit('someEvent', {'id': '123'})
}
})();
However, I am getting 'TypeError: vm.$emit is not a function'. I am not sure how to fix this. Can anyone help? Thanks a lot!
controllerAs just means that scoped variables are attached directly to the controller -- it doesn't mean that the controller is an instance of an angular scope itself. In this case, you'll need to inject the scope into the controller and then emit the event from on the scope:
function testCtrl($scope) {
// do something
$scope.$emit('someEvent', {'id': '123'})
}
Also beware, the normal injection rules apply -- If you're going to minify this, you'll probably need something like:
testCtrl['$inject'] = ['$scope'];

AngularJS private function unit testing confusion

I have read a couple articles and SO questions, but am still a little fuzzy on what I can and can't unit test.
I have a directive which returns a controller that has a couple of functions. Some of these functions have return statements, and others don't. I can see in the code coverage report that all of the functions are available to test, but only the functions with return statements are covered. Is it possible to unit test the controller's functions that don't have return statements? If yes, then how would I go about doing so?
directive.js snippet
app.directive('directive', function() {
var theController = ['$scope', function($scope) {
$scope.add = function() {
// no return statement, not covered, but is available
};
$scope.disableAddButton = function() {
// has a return statement and is fully covered
};
}];
return: {
scope: {
args: '='
},
templateUrl: ...,
restrict: 'A',
controller: theController
};
});
directive.spec.js
describe('The directive', function() {
var element,
$scope,
controller;
beforeEach(module('app'));
beforeEach(module('path/to/template.html'));
beforeEach(inject(function($compile, $controller, $rootScope, $templateCache) {
template = $templateCache.get('path/to/template.html');
$templateCache.put('path/to/template.html', template);
$scope = $rootScope;
controller = $controller;
var elm = angular.element('<div directive></div>');
element = $compile(elm)($scope);
$scope.$digest();
theController = element.controller('directive', {
$scope: $scope
});
}));
it('should compile', function() {
expect(element.html()).not.toBeNull();
});
describe('$scope.add', function() {
beforeEach(inject(function() {
add = theController.add();
}));
it('should be defined', function() {
expect(add).toBeDefined(); // passes
});
// Now what???
});
});
The "unit" you're testing is the entire directive/controller, not individual functions. Rather than trying to test each function in isolation, test that the results of calling the function are what you expect.
For example, what does add do? Presumably it has an effect on something - ensure that that has taken place.
Your title also mentions private functions. These are what the implementor of the "unit" has decided are necessary to get their job done. They aren't part of the public interface of the object, so you shouldn't need to worry about testing them - just ensure that the unit does what it's public interface says it should do - there could be any number of private functions actually doing that work.

Angular Reusing Directives and controllers

I have written a custom drop down option selector, all well and good, it has functions to go and get data (from a passed in url) to populate a list.
Now what I want to do is reuse this component but...
When I add it into another part of my application, but use a different data set, it duplicates the data and runs the controllers functions multiple times.
As far as I can understand 1 have two problems, services are singletons so when I run the function to populate some data, because there is only one instance of the service it just adds it to the current data set.
then the other problem is that controllers do have instances, so now there are two of them, its running the functions in each one.
So the easy solution would be to copy the component and call it a different name, while this might fix the problem, if I wanted to reuse it 10 times, that's 10 copies of the same component, not good.
I come from a OOP Java background, so I'm probably trying to use those techniques in a language that doesn't support it ;)
So I know I have to rethink how to do this, but I've hit a bit of a wall, how is it best to approach this?
Here is (hopefully) a JSFiddle that illustrates what I'm running itno
var app = angular.module('myApp',[]);
app.directive('mySelector', function () {
return {
scope: {
mydata: '='
},
restrict: 'EA',
template:'<select ng-model="timePeriodSelection" ng-options="timePeriodOption.name for timePeriodOption in timePeriodOptions"><option value="">Choose time period</option></select>',
controller: function($scope, myService) {
//$scope.name = 'Superhero';
console.log('test',$scope.mydata);
myService.setData($scope.mydata);
$scope.timePeriodOptions = myService.getData();
console.log('test2',myService.getData());
}
};
});
app.factory('myService', function() {
var _data=[];
return {
setData: function(value){
for (var a=0;a<value.length;a++){
_data.push(value[a]);
}
},
getData: function(){
return _data
}
}
});
https://jsfiddle.net/devonCream/ess9d6q6/
I can't show you the code I have for commercial reasons, but imagine what I'm passing in is actually a url and I have a service that gets the data then stores it in the array in the service/factory, each time it runs it just keeps adding them up! The code is a mock up demo.
Something like a custom drop down should be a directive and nothing else. That said, there are a ton of ways you could achieve what you're trying to do with a directive. Checkout the directive walkthroughs, they're really helpful.
In some way or another you'll probably want to have an isolate scope, use a template, and add a link function.
Example where the items are always the same:
angular.module('myApp')
.directive('myDropdown', function () {
return {
scope: {},
template: '' +
'<div class="my-dropdown">' +
'<div class="my-dropdown-item" ng-repeat="item in items">{{item.text}}</div>' +
'</div>',
link: function (scope, element, attrs) {
scope.items = ['Item 1', 'Item 2', 'Item 3'];
}
};
});
Example where you pass the items to each instance:
angular.module('myApp')
.directive('myDropdown', function () {
return {
scope: {
items: '='
},
template: '' +
'<div class="my-dropdown">' +
'<div class="my-dropdown-item" ng-repeat="item in items">{{item.text}}</div>' +
'</div>'
};
});
--UPDATE--
Example where you get the data once in a service:
angular.module('myApp')
.service('dataService', function ($http) {
var items;
$http.get('http://ab.com/dropdown-items')
.then(function (res) {
items = res.data;
});
return {
items: items
};
})
.directive('myDropdown', function (dataService) {
return {
scope: {},
template: '' +
'<div class="my-dropdown">' +
'<div class="my-dropdown-item" ng-repeat="item in items">{{item.text}}</div>' +
'</div>',
link: function (scope, element, attrs) {
scope.items = dataService.items;
}
};
});
So unless someone can tell me differently, the only way I can see to solve problem this to hold the data in the controller, therefore isolating the data to its own controller instance., holding the data anywhere else just causes instance issues.
I've refactored my 'fiddle' to show 2 different data sources (in this example I've used 2 factories as models), that bring the data back into the controller to be processed and then stored.
Normally if I wasn't reusing the component I would put this logic into the factory, but doing so gives me the problem I had to start with.
Also in my 'real' project I check the variable to 'see' what 'instance' have been triggered and call methods from that, all seems a bit clunky but it seems the only reliable way I can get it to work maybe Angular 2 will resolve these issues.
Anyway my link to my jsfiddle
var app = angular.module('myApp',[]);
app.directive('mySelector', function () {
return {
scope: true,
bindToController: {
mydata: '#mydata',
timePeriodOptions: '='
},
controllerAs: 'selCtrl',
restrict: 'EA',
template:'<select ng-model="timePeriodSelection" ng-options="timePeriodOption.name for timePeriodOption in selCtrl.timePeriodOptions"><option value="">Choose time period</option></select>',
controller: 'selCtrl'
};
});
app.controller('selCtrl', function($scope, myService) {
var selCtrl = this;
selCtrl.timePeriodOptions = [];
if (angular.isString(selCtrl.mydata)){
processData();
}
function processData(){
var value = myService.getData(selCtrl.mydata);
for (var a=0;a<value.length;a++){
selCtrl.timePeriodOptions.push(value[a]);
}
};
});
app.factory('myService', function(dataModel1,dataModel2) {
return {
getData: function(model){
var _data = []
if (model === "1"){
_data = dataModel1.getData();
}else{
_data = dataModel2.getData();
}
console.log('data ',_data);
return _data
}
}
});
app.factory('dataModel1', function() {
var _data=[{"name":1,"value":1},{"name":2,"value":2},{"name":3,"value":3}];
return {
getData: function(){
return _data
}
}
});
app.factory('dataModel2', function() {
var _data=[{"name":4,"value":4},{"name":5,"value":5},{"name":6,"value":6}];
return {
getData: function(){
return _data
}
}
});

Saving/Restoring angularjs several scope variables

I have two different views but for both of them there is only one controller as they have several things in common. But when the second view is loaded all the data stored in the scope of the first view is lost.
For this problem I found this very nice solution here
This solution looks good only if I have few number of data in scope, which is not the case of mine. I have several data stored in the controller scope. So I wanted to have a way to iterate over the data(only data saved by me not angular's data), but I am not sure how do I iterate over these value. Any thoughts?
i had somewhat similar requirement and i have created a directive for the same purpose
csapp.directive("csDataStore", ["$location", "$sessionStorage", function ($location, $sessionStorage) {
var controllerFunction = ["$scope", function ($scope) {
var enter = function () {
var storedValue = $sessionStorage[$scope.key];
if (angular.isDefined(storedValue)) $scope.value = storedValue;
$scope.onLoad();
};
enter();
var exit = function () {
$sessionStorage[$scope.key] = $scope.value;
};
$scope.$on("$destroy", function () { exit(); });
}];
return {
restrict: "E",
scope: { key: '#', value: '=', onLoad: '&' },
controller: controllerFunction
};
}]);
i use the directive like this
<cs-data-store key="stkh.view" value="filter" on-load="loadData()"></cs-data-store>
so what we have here are 3 parameters: key/value and then what to do on loading that value in the scope... so a callback function on-load, which would be called after the the key has been loaded in the $scope
i used $sessionStorage, to keep values even between page refresh...

Sharing scope between Directives - missing $http

Here are mine two directives. I basically want to share the scope between them. However with this I get an undefined $http error. I know I need to put $http somewhere, but where?
aresAnalytics.directive('market', function($http) {
return {
restrict: 'E',
controller: function ($scope, Data) {
$scope.data = Data;
},
link: function(scope, element, attrs) {
element.bind('click', function() {
console.log("A1 " + attrs.market);
scope.temp = attrs.market;
$http.get('get_markets').success(function(markets) {
Data.markets = markets;
});
})
}
}
});
aresAnalytics.directive('events', function($http) {
return {
restrict: 'E',
controller: function ($scope) {
scope = $scope;
},
link: function(scope, element) {
element.bind('click', function() {
console.log(scope.temp);
});
}
}
});
HTML:
<market ng-repeat="market in data.markets" market="{{ market }}">
{{ market }}
</market>
Also, I think the way I am doing this
$http.get('get_markets').success(function(markets) {
Data.markets = markets;
});
is not correct, what can I replace with it.
And, should I use Isolate Scope '#' instead? How will that look like?
Thanks for reading this!
You need to inject the $http service like so.
app.directive('something', function( $http ) { ... });
Angular Dependency Injection
To access attributes like that there are many ways, but this is a simple way.
change html to
<market ng-repeat="market in data.markets" market="market">
{{ market }}
</market>
Require parse like this
app.directive('something', function( $http, $parse ) { ... });
And get your attribute like so
scope.temp = $parse(attrs.market)(scope);
This way you're getting it directly from the scope while the other way, angular hasn't rendered the attribute yet.
The problem is with your dependency injection. Try this:
aresAnalytics.directive('market', ['$http', function($http) {
// (you code)
}]);
or if you don't use code minifiers/uglifiers:
aresAnalytics.directive('market', function($http) {
// (you code)
});
I don't know, but I simply had to append a $parent to my scope to always use the parent scope. (like use scope.$parent instead of scope).
REF: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Categories