How can I add new directive information from a database? - angularjs

I have an interesting situation, which I fully admit I may not be handling correctly.
I want to load directives in to an Angular app using AngularAMD, but the information on the directives is coming from a database/API call. Here's how I'm handing it at the moment:
app.run(function ($rootScope, $state, $http) {
$rootScope.$state = $state;
$rootScope.rootPages = JSON.parse(localStorage.pages);
$http.get('api/Directives').then(function (resp) {
for (var i = 0; i < resp.data.length; i++) {
var dirInfo = resp.data[i];
var dirConfig = JSON.parse(dirInfo.Configuration);
angularAMD.directive(dirInfo.Name, function () {
return {
restrict: 'E',
controller: eval(dirConfig.controller),
templateUrl: dirConfig.templateUrl,
scope: true,
}
});
}
}, function (error) {
alert('error');
});
});
At the moment this generates two directives, and I have two links on my page, one for each module (using angular-ui-router and router-ui-extras to load those based on another database table/API call). Theoretically clicking link 1 should bring up directive 1, and link 2 should bring up directive 2. However, when I click link 1, directive 2 comes up. It's almost as if the only directive that registers is directive 2.
Here's the information I get from the database:
dirInfo1: {
Name: directiveOne,
templateUrl: /html/directiveOne.html,
controller: controllerOne
}
dirInfo2: {
Name: directiveTwo,
templateUrl: /html/directiveTwo.html,
controller: controllerTwo
}
In this case, only directiveTwo.html is shown, and only controllerTwo fires.
Any help would be greatly appreciated.

It't a usual problem using for loop with delayed/async operations.
By the moment that first angularAMD.directive is resolved var dirInfo = resp.data[i]; inside of the loop is is equal to the last item in the list.
You need to use either Promises or anonymous function inside of a loop to keep the link to a right index inside of async function. Check simple example of what's happening in your case here: https://stackoverflow.com/a/13977142/405623
for ( i = 0; i < items.length; i++) {
(function(thisItem) {
setTimeout(function() {
console.log(thisItem);
}, i * 1000);
)(items[i]);
}

Related

Controller 'Init' Function Executed More Than Once

This is my controller
//init
var init = function () {
$scope.getAlbumList();
};
$scope.getAlbumList = function () {
AlbumService.getAlbumList()
.then(function (data) {
$scope.albumList = data;
});
};
$scope.viewAlbum = function () {
AlbumService.getAlbum()
.then(function (data) {
$scope.album = data;
$location.path("/album");
});
};
init();
and this is my routeProvider
when('/albums', {
templateUrl: 'WebApp/albums.html',
controller: 'AlbumController'
}).
when('/album', {
templateUrl: 'WebApp/albumview.html',
controller: 'AlbumController'
}).
So the AlbumController handles both the albums and albumview pages.
When the controller is created, the init function is called which in turns calls the getAlbumList function.
The problem is that the controller is created again when the user clicks an album to go to its albumview page. This ends up executing the init function again which in turns causes an undesired second call to getAlbumList.
So basically, I need getAlbumList to be called when the controller is created for '/albums' but not '/albumview'.
How can I achieve this?
Obviously, I could solve this problem by creating a second controller for /albumview but this seems unnecessary and I'd rather have everything regarding albums in one controller.
You have 2 solutions:
in your controller test if location.path() == 'albums'
in the html page of albums ng-init="getAlbumList()"
Probably the quickest option is to check the path to see if you're in album or albums. Here is sample code of how to do that:
if ($location.url().toLowerCase() === '/albums') {
// call to getAlbumList here
}
It will happen if you have controller: 'AlbumController' in router and your html have ng-controller = "AlbumController". Use only one to avoid double initilization.

Attach Data to Pages $scope During AngularJS App Startup in .run

I'm dynamically creating my routes in .run here:
.run(function($http, $rootScope) {
var mainInfo = null;
$http.get('thegoods.json').success(function(data) {
$rootScope.HomeGateway = data.HomeGateway;
var pages = $.merge($rootScope.HomeGateway.questions, $rootScope.HomeGateway.messages);
for (var i = 0; i < pages.length; i++) {
$routeProviderReference.when(pages[i].url, {
templateUrl: pages[i].view,
controller: pages[i].controller,
data: pages[i].data
})
}
});
How can I access the data (defined as data: pages[i].data above). Note, I'm not sure if this is completely correct -- but it should serve as a demonstration of what I'm trying to accomplish.
Basically, I grab a JSON file and store it in $rootScope. I then use the $rootScope to dynamically create all the URLs. (Please see http://blog.brunoscopelliti.com/how-to-defer-route-definition-in-an-angularjs-web-app/ if you are interested in doing this yourself) I want to be able to access data specific to certain pages without having to search through the JSON data again. I understand that it is an inexpensive operation but I'd like to do it the right way if possible.
Another way of phrasing my question would be.. how can I define each pages $scope at .run when the app first starts?
If all, written in article, you share is true, and I correctly understand all you want, then you should use resolve option of second parameter of .when function as described here. And write smth like:
$routeProviderReference.when(pages[i].url, {
templateUrl: pages[i].view,
controller: pages[i].controller,
resolve: {
data: function(){
return pages[i].data;
}
}
});
And then in controllers:
function QuestionController($scope, data) {
$scope.data = data.someProp;
}

Updated isolated scope on multiple directives

I've got a ng-repeat creating multiple directives, each with an isolated scope. I want to be able to call a single function that calls a function on each directive, so it resets a variable on each isolated scope. I can't work out anyway to do this? I realise this might not be best practice, but I just need a solution at the moment.
Another option that i use in a similar vein is angulars event system.
I'm creating a dashboard of widgets. My widget is a directive contained within a ng-repeat.
I emit events in my controller $scope.$broadcast then listen in my directive with $scope.$on. I use the $index of the ng-repeat to be able to target specific widgets.
quick example fiddle: http://jsfiddle.net/adyjm9g4/
EDIT: Forgot to mention you can also pass data: http://jsfiddle.net/adyjm9g4/1/
A way to do it is to provide a callback to the directive (i.e. scope: { xxx: '&' }) that will execute some functionality. Could be:
<the-directive callback="ctrl.command(action, argument)" />
And the directive looks like:
app.directive('theDirective', function() {
return {
...
scope: {
callback: '&',
...
},
controller: ['$scope', function($scope) {
this.resetTheVariable = function() {
// DO WHAT YOU WANT HERE
};
$scope.callback({ action: 'register', argument: this });
$scope.$on('$destroy', function() {
scope.callback({ action: 'deregister', argument: this });
});
}]
};
})
Now the controller invoking this directive would look like:
function TheController() {
var registeredDirectives = [];
this.command = function(action, argument) {
switch(action) {
case 'register':
registeredDirectives.push(argument);
break;
case 'deregister':
var index = registeredDirectives.indexOf(argument);
if( index >= 0 ) {
registeredDirectives.splice(index, 1);
}
break;
}
};
this.resetAll = function() {
registeredDirectives.forEach(function(registeredDirectiveController) {
registeredDirectiveController.resetTheVariable();
});
};
}

Controllers and directives, precedence

I've been with Angularjs a few days and I'm struggling with a few aspects of it. I'll do my best to try and explain what the issue is, and I'd really appreciate any help anyone can give me about it.
My situation (simplified) is this:
I have a service which loads some info from a json and stores it in an object. It also have some functions to be used for other controllers to retrieve that information.
var particServices = angular.module('particServices', []);
particServices.service('particSrv', function() {
var data = {};
this.updateData = function(scope) {
data = // http call, saves in data
}
this.getName = function(code) {
return data.name;
}
});
I have an html page assisted by a controller, which uses a directive board (no params, really simple). This is the controller:
var bControllers = angular.module('bControllers', []);
bControllers.controller('bController', ['$scope', 'particSrv', function ($scope, particSrv) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
particSrv.updateData($scope);
}]);
As you can see, the controller makes the call to initialize the object in the service. As this is a singleton, I understand once that info is loaded no other call needs to be make to updateData and that info is available to others using the getters in the service (getName in this case).
I have a really simple directive board (which I simplified here), which uses another directive bio.
angular.module('tsDirectives', [])
.directive('board', ['dataSrv', 'particSrv', function(dataSrv, particSrv) {
return {
restrict: 'E',
replace: true,
scope: true,
controller: function($scope) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
dataSrv.updateData($scope, 'board', 'U');
},
templateUrl: '<div class="board"><div bio class="name" partic="getName(code)"/></div></div>'
};
}]);
And this is the bio directive:
angular.module('gDirectives', [])
.directive('bio', function() {
return {
scope: {
partic: '&'
},
controller: function($scope) {
$scope.name = $scope.partic({code: $scope.athid});
},
template: '<a ng-href="PROFILE.html">{{name}}</a>'
};
})
Now, what I expected is that in the bio directive the info retrieved from party was displayed, but apparently this directive is processed before the partic is initialized in the main controller.
I was under the impression that even though this information was still not loaded when the directive is processed, as soon as the service finishes and the info is ready, automagically it would appear in my directive, but that does not seem to work like that. I've been reading about $watch and $digest, but I fail to see why (and if) I would need to call them manually to fix this.
Any hint will be much appreciated. I could provide more technical details if needed.
Directive will initialise when app is loaded and user opens the page where that directive is, if you have some property that is set later (from api for example), it will update that property in directive but that directive will not be reinitialised ($scope.partic({code: $scope.athid}) wont be called).
If you want for directive to wait for initialisation you should use ng-if. Something like this:
<div data-directive-name data-some-property="someProperty" data-ng-if="someProperty"></div>
In this case directive will be initialised when (if) you have some value in $scope.someProperty. But this is not very good if you can have false values for someProperty.
In that case you would need to use some kind of loaded flag.
You have not included "particServices" as a dependency in other modules which use the services of "particServices". Your modules should look like:
var bControllers = angular.module('bControllers', ['particServices']);
angular.module('tsDirectives', ['particServices']);
angular.module('gDirectives', ['particServices']);

updating data between controller, service and directive

I've written a pretty simple test app as follows:
angular.module('tddApp', [])
.controller('MainCtrl', function ($scope, $rootScope, BetslipService) {
$scope.displayEvents = [
{
id: 1,
name: 'Belarus v Ukraine',
homeTeam: 'Belarus',
awayTeam: 'Ukraine',
markets: {home: '2/1', draw: '3/2', away: '5/3'},
display: true
}
];
$scope.betslipArray = BetslipService.betslipArray;
$scope.oddsBtnCallback = BetslipService.addToBetslip;
$scope.clearBetslip = BetslipService.clearBetslip;
})
.directive('oddsButton', function () {
return {
template: '<div class="odds-btn">{{market}}</div>',
replace: true,
scope: {
market: '#',
marketName: '#',
eventName: '#',
callback: '&'
},
link: function (scope, element) {
element.on('click', function() {
scope.callback({
name: scope.eventName,
marketName: scope.marketName,
odds:scope.market
});
});
}
};
})
.factory ('BetslipService', function ($rootScope) {
var rtnObject = {};
rtnObject.betslipArray = [];
rtnObject.addToBetslip = function (name, marketName, odds) {
rtnObject.betslipArray.push({
eventName: name,
marketName: marketName,
odds: odds
});
};
rtnObject.clearBetslip = function () {
rtnObject.betslipArray = [];
};
return rtnObject;
});
I've assigned an array to a controller variable. I've also assigned functions to modify the array. To add an object to the array the callback is called by a directive with isolate scope. There's some strange behaviour happening that I don't quite understand:
=> clicking the directive runs the callback in the service. I've done some debugging and it seems that the controller variable is updated but it doesn't show in the html.
=> clicking the button to clear the array isn't working as expected. The first time it's causing an element to display, after which it has no effect.
I think that this may have to do with the nested ng-repeats creating their own scopes
NB
I fixed the array not clearing by changing the function in the service to:
while (rtnObject.betslipArray.length > 0) {
rtnObject.betslipArray.pop();
}
// instead of
rtnObject.betslipArray = [];
This makes sense as the service variable was being pointed at a new object while the old reference would persist in the controller.
I got the html to update by wrapping the callback call in the directive in a scope.$apply().
This part I dont really understand. How can scope.$apply() called in the directive have an effect on the controller scope when the directive has an isolate scope? updated fiddle: http://jsfiddle.net/b6ww0rx8/7/
Any thought's greatly appreciated
jsfiddle: http://jsfiddle.net/b6ww0rx8/5/
C
I got it working here: http://jsfiddle.net/b6ww0rx8/8/
Added $q, $scope.$emit and $timeout clauses to help with communications between your directive / service and controller.
I would like to also say that I wouldn't assign service functions to a controller $scope, You should define functions in the controller that call service functions.
Instead of this:
$scope.clearBetslip = BetslipService.clearBetslip;
Do this:
$scope.clearBetslip = function(){
BetslipService.clearBetslip().then(function(){
$scope.betslipArray = BetslipService.getBetslipArray();
});
};

Resources