Angular Reusing Directives and controllers - javascript

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
}
}
});

Related

Issues with controllers in Directives

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!

Unit testing a directive - Unable to force a fake data

I'm trying to understand how to unit test my directive in my situation below.
Basically I'm trying to unit test a directive which has a controller. On the loading of this directive the controller makes a http request by a service which brings some data to the controller again then provides this data to the directive view.
On the scenario below in my understanding I should do:
A $httpBackend to avoid an exception when the http request is done;
Populate the fake data to be able to unit test the directive with diff behaviors
Compile the directive
What I've been trying so far, as you can see, is override the Service with the fake data. What I could not make work so far.
Some doubts come up now.
As you can see in my Controller. I'm providing the whole Service to the view:
$scope.ItemsDataservice = ItemsDataservice;
What makes me believe that my approach to override the Service should work.
My question:
On scenario below I understand that I could override the Service to manipulate the data or even override the controller to manipulate the data by scope.
What's the right thing to do here?
Am I understand wrong?
Am I mixing the unit tests?
In my current unit test code, when I'm applying the fake data(or not), is not make any difference:
ItemsDataservice.items = DATARESULT;
ItemsDataservice.items = null;
Controller:
angular.module('app')
.controller('ItemsCtrl', function ($scope, $log, ItemsDataservice) {
$scope.ItemsDataservice = ItemsDataservice;
$scope.ItemsDataservice.items = null;
$scope.loadItems = function() {
var items = [];
ItemsDataservice.getItems().then(function(resp) {
if (resp.success != 'false') {
for (resp.something ... ) {
items.push({ ... });
};
ItemsDataservice.items = items;
};
}, function(e) {
$log.error('Error', e);
});
};
$scope.loadItems();
});
Service:
angular.module('app')
.service('ItemsDataservice', function ItemsDataservice($q, $http) {
ItemsDataservice.getItems = function() {
var d = $q.defer();
var deffered = $q.defer();
var url = 'http://some-url?someparameters=xxx'
$http.get(url)
.success(function (d) {
deffered.resolve(d);
});
return deffered.promise;
};
return ItemsDataservice;
});
Directive:
angular.module('app')
.directive('items', function () {
return {
templateUrl: '/items.html',
restrict: 'A',
replace: true,
controller: 'ItemsCtrl'
};
});
Unit testing directive:
ddescribe('Directive: Items', function () {
var element, scope, _ItemsDataservice_, requestHandler, httpBackend;
var URL = 'http://some-url?someparameters=xxx';
var DATARESULT = [{ ... }];
// load the directive's module
beforeEach(module('app'));
beforeEach(module('Templates')); // setup in karma to get template from .html
beforeEach(inject(function ($rootScope, ItemsDataservice) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
_ItemsDataservice_ = ItemsDataservice;
requestHandler = httpBackend.when('GET', URL).respond(200, 'ok');
}));
afterEach(function() {
//httpBackend.verifyNoOutstandingExpectation();
//httpBackend.verifyNoOutstandingRequest();
});
it('Show "No Items available" when empty result', inject(function ($compile) {
_ItemsDataservice_.items = null;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(1);
}));
it('Should not show "No Items available" when data available ', inject(function ($compile) {
_ItemsDataservice_.items = DATARESULT;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(0);
}));
});
I sorted out the problem.
Changed this line:
element = $compile(element)(scope);
To this line:
element = $compile(element.contents())(scope);
The only diff is the jquery method .contents()
I did not get yet why. But it solved.
Update:
Another thing I've just discovered and that was really useful for me.
You can use regular expression on you httpBackend:
httpBackend.whenGET(/.*NameOfThePageXXX\.aspx.*/).respond(200, 'ok');
So, you don't need to worry to use exactly the same parameters etc if you just want to avoid an exception.

How do you store a JSON object as a function parameter in Angular

I am using a directive to populate a chart using the AmCharts API. The function that is currently being used lists out the data that is populating the form in JSON format as a parameter in the function. I want to be able to store this as a variable so I can have a separate file for the JSON. I know you probably have to use $http to get the json, but I am unsure how you would connect this to the directive.
var myapp = angular.module('tmn_portfolio', []);
myapp.directive('loadPortfolio',
function () {
return {
restrict: 'E',
replace:true,
template: '<div id="chartdiv" style="min-width: 310px; height: 400px; margin: 0 auto"></div>',
link: function (scope, element, attrs) {
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
var config = scope.config || {};
chart = AmCharts.makeChart("chartdiv", I WANT THIS TO BE A VARIABLE TO JSON FILE);
};
initChart();
}//end watch
}
}) ;
A better solution than scope: true would be to pass the data to the directive in an attribute. The markup will look like this:
<load-portfolio my-data="jsonData" ></load-portfolio>
That element will be replaced by your template. In the directive:
myapp.directive('loadPortfolio',
function () {
return {
restrict: 'E',
replace:true,
scope: {
myData: '='
},
template: '<div id="chartdiv" style="min-width: 310px; height: 400px; margin: 0 auto"></div>',
link: function (scope, element, attrs) {
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
var config = scope.config || {};
chart = AmCharts.makeChart("chartdiv", scope.myData);
};
initChart();
}//end watch
}
});
The equals sign is a two way binding between the property myData on the directive scope, and the property jsonData on the scope of your controller (the "model" in angular terms). Any time the data changes in the controller, it will also change in the directive, and your UI will update to reflect that.
Now you just have to get the json data, right? You are right, you should probably use $http to do this, and what that will look like will depend on your specific implementation, but the important part is that once you populate jsonData, your directive will be updated to reflect that. I typically initialize models that will be populated asynchronously in my controller. A very simple version might look something like this:
myapp.controller('myController', ['$scope', '$http', function($http, $scope) {
$scope.jsonData = {}; // or '' - depending on whether you actually want the JSON string here.
$http({
url: '/data.json',
method: 'GET'
}).
then(function(r) {
$scope.jsonData = r.data;
});
}]);
I think that to do it properly you should look into routing and the resolve property of routes which allows you to retrieve the data before your controller loads, and pass it as an argument to the controller, but that is really up to you.
Update: Like the other answer, I do recommend using a factory/service for calls to your server or API, instead of using $http directly in the controller like in my example. Just trying to keep it simple here. Angular code is more fun when well-organized.
You need a factory or service to first request your JSON file and store it as a variable:
var app = angular.module('app', []);
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];
}
]);
app.factory('data', function($http) {
var data = {
async: function() {
// $http returns a promise, which has a then function, which also returns a promise
var promise = $http.get('file.json').then(function (response) {
// The then function here is an opportunity to modify the response
console.log(response);
// The return value gets picked up by the then in the controller.
return response.data;
});
// Return the promise to the controller
return promise;
}
};
return data;
});
app.controller('MainCtrl', function( data,$scope) {
// Call the async method and then do stuff with what is returned inside our own then function
data.async().then(function(d) {
$scope.jsonData = d; ;
});
});

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...

AngularJS: Watching for async values within directive's linking function

I'm using ng-grid for data display and I want to dynamically adjust height of the grid itself depending on the number of returned results and user's monitor resolution.
Here's the angular code:
angular.module('modulename', [])
.controller('ctrl', function($scope, $http) {
$scope.gridResult = [];
$scope.gridOptions = {
data: 'gridResult'
};
$scope.listData = function() {
//Async call using $http.get which on success callback asigns response.data to $scope.gridResult
}
//Get data when page loads
$scope.listData();
})
.directive('tableheight', function() {
return {
restrict: 'A',
scope: {
},
controller: 'ctrl',
link: function(scope, elem, attrs) {
scope.$watchCollection('gridResult', function(n, o) {
console.log(n); //Shows empty array
if (n.length > 0) {
console.log(n) //Never displayed
//Calculate new size for the grid
...
}
});
}
};
});
HTML portion:
<div data-ng-grid="gridOptions" tableheight></div>
As you can see 'gridResult' is always empty array even after 'listData' success callback. If I move whole $watchCollection method to controller body everything is working as expected. What I'm trying to achieve is somehow run directive's linking function after DOM has been rendered which includes rendering data from async call.
I guess I'm doing something wrong here or my approach is wrong, however I would appreciate if someone could offer a solution to this.
On a subject of calling directive's linking function after DOM is rendered and ready I tried putting the code inside angular's $timeout with 0 delay but that didn't do anything for me. And speaking of this is there any way to call directive's linking function after DOM rendering since I believe some of my problems are coming from this issue?
Just now there is no communication between the controller and directive. Here you find basic scenarios how such communication can be organized.
The most secure and generic option would be to create data bind like that:
angular.module('modulename', [])
.controller('ctrl', function($scope, $http) {
$scope.gridResult = [];
...
})
.directive('tableHeight', function() {
return {
require: 'dataGrid', // it would be better to use this directive only alongside data-grid
restrict: 'A',
scope: {
gridResult: '=tableHeight'
},
// controller: 'ctrl', this is unnecessary -- it points to directive's controller
link: function(scope, elem, attrs) {
scope.$watchCollection('gridResult', function(n, o) {
...
});
}
};
});
<div data-ng-grid="gridOptions" table-height="gridResult"></div>

Categories