Alternatives to using $scope.$watch in directive to handle undefined variable - javascript

I'm developing an application using angularjs and I'm also using directives because some UI is re-used across multiple pages. When I have a directive which is dependent on a value from a promise I have to use $scope.$watch and an if condition to check for undefined, because the directive compiles before the promise is completed. Here is an example:
myAppModule.directive('topicDropdown', function () {
return {
templateUrl: 'Scripts/app/shared/dropdown/tmplTopic.html',
restrict: 'AE',
scope: {
subjectId: '=',
setDefault: '=',
topicId: '='
},
controller: [
'$scope', 'service', function ($scope, service) {
$scope.$watch('subjectId', function () {
if ($scope.subjectId != undefined)
$scope.getTopics();
});
$scope.getTopics = function () {
service.get("section/topics/" + $scope.subjectId).then(function (data) {
$scope.listOfTopics = data;
if ($scope.setDefault) {
$scope.subjectId = $scope.listOfTopics[0].UniqueId;
}
});
}
}
]
}
});
The subjectId will eventually come from a promise but without the $watch I'll get an undefined error because it will fire getTopics without an ID.
scope: {
subjectId: '=',
setDefault: '=',
topicId: '='
},
Currently, this code works but I'm having to invoke the digest cycle every time subjectId changes which will loop through everything that scope is watching. I only care when subject ID changes at this point.
I've also seen some suggestions to use ng-if for the template html. Something like this:
<div ng-if="subjectId != undefined">
<topic-dropdown subject-id="subjectId"></topic-dropdown>
</div>
With this, I don't have to use $scope.$watch however I'm not sure if this is the best approach either.
Does anyone have a good solution to this problem? Are there any directive properties that I could be using which I am unaware of?
Nick

Pass a promise for the subjectId and make getTopics wait for it to resolve. It would look something like this:
$scope.getTopics = function () {
$scope.subjectIdPromise.then(function (subjectId) {
$scope.subjectIdPromise = service.get("section/topics/" + subjectId)
.then(function (data) {
$scope.listOfTopics = data;
if ($scope.setDefault) {
return data[0].UniqueId;
} else { return subjectId; }
});
});
};
Doing it this way all access to the subjectId is done in a then success function. If you then want to change the subjectId do it by replacing the promise with a new one.

Try using promise patterns,
myAppModule.directive('topicDropdown', function () {
return {
templateUrl: 'Scripts/app/shared/dropdown/tmplTopic.html',
restrict: 'AE',
scope: {
subjectId: '=',
setDefault: '=',
topicId: '='
},
controller: [
'$scope', 'service', function ($scope, service) {
$q.when($scope.subjectId).then(
service.get("section/topics/" + $scope.subjectId).then(function (data) {
$scope.listOfTopics = data;
if ($scope.setDefault) {
$scope.subjectId = $scope.listOfTopics[0].UniqueId;
}
});
)
]
}
});
OR
myAppModule.directive('topicDropdown', function ($q) {
return {
templateUrl: 'Scripts/app/shared/dropdown/tmplTopic.html',
restrict: 'AE',
scope: {
subjectId: '=',
setDefault: '=',
topicId: '='
},
link: function(scope, element, attrs){
$q.when(subjectId).then(
service.get("section/topics/" + scope.subjectId).then(function (data) {
scope.listOfTopics = data;
if (scope.setDefault) {
scope.subjectId = scope.listOfTopics[0].UniqueId;
}
});
)
}
}
});
but I think one way or another you are going to use $watch. Using $watch do not harm anything, its angular way to keep things connected.

Related

Multiple directives asking for template

I know this might be a common problem but i havent been able to find a viable solution to the issue.
Say i have the following directive:
angular.module('Api').directive('contentSpinner', function (responseInterceptor,$timeout) {
return {
restrict: 'AE',
templateUrl: 'js/helpers/Api/directives/content-spinner/content-spinner.html',
scope: {
boundRoute: '#',
timeout: '=',
timeoutms: '#'
},
link: function (scope, element, attr) {
scope.shouldSpin = true;
if (scope.boundRoute) {
responseInterceptor.subScribeToRoute(scope.boundRoute, function () {
//Route has been completed and the spinner is ready to stop!
scope.shouldSpin = false;
})
}
if (scope.timeout) {
$timeout(function () {
scope.shouldSpin = false;
}, scope.timeoutms);
}
}
}
});
Please do note the AE restriction.
Then i have another directive:
angular.module('LBTable').directive('lbTable', ['$state', 'alertService', 'usSpinnerService', function ($state, alertService,usSpinnerService) {
return {
restrict: 'E',
roles: 'all',
templateUrl: 'js/helpers/LBTable/directives/lb-table/lb-table.html',
scope: {
tableData: '=',
tableFields: '=',
actionElement: '=',
search: '=',
tableDataFilter: '='
},
link: function (scope, element, attrs) {
}
}
}]);
As you can see both directives has a template.
Now on all of my <lb-table> elements i wish to add the attribute content-spinner (my other directive) so that it prepends the template of contentSpinner.
However this poses the following error:
Error: [$compile:multidir] Multiple directives [contentSpinner (module: Api), lbTable] asking for template on: <lb-table
Which is theory makes sence it doesnt know which template to use.
However is there a way to come around this without wrapping one directive around the other like:
<content-spinner><lb-table></lb-table></content-spinner>

Trouble using $attrs in directives controller

My directive has an attribute id-model that is used to pass an array to the directive. The directives controller should execute a function using that array as an argument. I can access the attribute in my controller and log the array but my function never runs and I'm not sure why.
If I don't use the attribute and change placesFact.getDetails(idModel) to placesFact.getDetails($scope.model) everything works fine but I want there to be more flexibility with the directive.
*Update:
To clarify on the placesFact.getDetails() method, It looks up google place info with a place id and returns the results as a promise. It has been tested and works fine.
Directive
.directive('gPlaces', function() {
return {
restrict: 'E',
scope: '#',
link: function(scope, element, attrs) {
scope.tempUrl = attrs.tempUrl;
},
controller: function($scope, $attrs, placesFact) {
var idModel = $attrs.idModel;
$attrs.$observe('idModel', function(value) {
idModel = value;
console.log(idModel); // This logs the array
placesFact.getDetails(idModel).then(function(results) {
console.log(results); // This logs nothing
$scope.places = results;
});
});
},
template: '<div ng-include="tempUrl"></div>'
}
})
Controller
angular.module('myApp').controller('PlacesCtrl', function($scope) {
$scope.model = [
'ChIJR4dOl_hYwokRApSCaQiBidk',
'ChIJv-Ghof5YwokRMtWLEV12hJI',
'ChIJjyX2GqRZwokRT-gdcGoPuSI',
'ChIJqSurReFYwokRec7JFACToas',
'ChIJn8dCo-NYwokRC_4nRUQWbNE',
'ChIJszmN0-JYwokRk-XCDbO6X_Y',
'ChIJt4TrE_1YwokRVedrKxaqYoo',
'ChIJiW0WvwJZwokRIWyzCvo3o5k',
'ChIJsS1xLQJZwokRGfXJPMwXA1A',
'ChIJI5xCX6NZwokR3jdSQwsw2DI',
'ChIJuVE5aLtZwokR-K75OxUEtzI',
'ChIJ7R4tgLtZwokRM8thlhlzE2o',
'ChIJxbWTG_pYwokRPgtFVKi-Cuc',
'ChIJKZVnwFVYwokRgDw_sxw3NCo',
'ChIJw_JUgvhYwokR91EMxVDhB8M',
'ChIJZ3oXOVZYwokRNnAXaDRKAzg',
'ChIJP9idxlZYwokRLH-I1mNfzYQ',
'ChIJndGJ5FNYwokRricJvhT0t1s',
'ChIJhZ0Sn1ZYwokRRA1MZJZHrHA',
'ChIJhTdv51NYwokR7V105uVzf8g',
'ChIJvfVwDFJYwokRtWobbwOMEVM',
'ChIJpY9Tg01YwokRCr_aQpDrqgk',
'ChIJ-fRuLFdYwokR0KKQ6Av_WhQ',
'ChIJh-DIsE1YwokRhuFrdM1ge5E'
];
});
View
<g-places temp-url="pages/places/multiTemp.html" id-model={{model}}></g-places>
As estus mentioned in the comments, my scope: '#' syntax was wrong and was the root of the problem. This is my first directive so a little trial and error and a lot of reading lead me to figure out what to do though.
Here is how the directive looks now that it is working.
.directive('gPlaces', function() {
return {
restrict: 'E',
scope: {
idModel: '=',
tempUrl: '#'
},
controller: function($scope, placesFact) {
placesFact.getDetails($scope.idModel).then(function(results) {
$scope.places = results;
});
},
template: '<div ng-include="tempUrl"></div>'
}
})

Proper "Angular" way to pass behavior to directive?

When looking for information regarding Angular directives and passing behavior to directives, I get ended up being pointed in the direction of method binding on an isolate scope, i.e.
scope: {
something: '&'
}
The documentation for this functionality is a bit confusing, and I don't think it'll end up doing what I want.
I ended up coming up with this snippet (simplified for brevity), that works by passing a scope function in HomeCtrl, and the directive does it's work and calls the function. (Just incase it matters, the real code passes back a promise from the directive).
angular.module('app', []);
angular.module('app')
.directive('passingFunction',
function() {
var changeFn,
bump = function() {
console.log('bump() called');
internalValue++;
(changeFn || Function.prototype)(internalValue);
},
internalValue = 42;
return {
template: '<button ng-click="bump()">Click me!</button>',
scope: {
onChange: '<'
},
link: function(scope, element, attrs) {
if (angular.isFunction(scope.onChange)) {
changeFn = scope.onChange;
}
scope.bump = bump;
}
};
})
.controller('HomeCtrl',
function($scope) {
$scope.receive = function(value) {
console.log('receive() called');
$scope.receivedData = value;
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.4/angular.min.js"></script>
<div ng-app="app" ng-controller="HomeCtrl">
<passing-function on-change="receive"></passing-function>
<p>Data from directive: {{receivedData}}</p>
</div>
Is this a proper "Angular" way of achieving this? This seems to work.
What you need is to pass the function to the directive. I'll make a very small example.
On controller:
$scope.thisFn = thisFn(data) { console.log(data); };
In html:
<my-directive passed-fn="thisFn()"></my-directive>
On directive:
.directive('myDirective', [
() => {
return {
restrict: 'E',
scope: {
passFn: '&'
},
template: '<div id="myDiv" ng-click="passFn(data)"></div>',
link: (scope) => {
scope.data = "test";
}
}
}]);

Adapting navigation derictive in AngularJS

Hej,
I am looking for a method to change the navigation links in the navigation bar according to whether a user is logged in or not. Obviously, a logged in user should be able to navigate to more pages.
I programmed a UserService which wraps the ng-token-auth module (https://github.com/lynndylanhurley/ng-token-auth) and adds a boolean variable holding the login state. The service looks similar to this:
angular.module('app')
.run(function($rootScope, $location, userService) {
$rootScope.$on('auth:login-success', function(ev, user) {
userService.setLoggedIn(true);
$location.path('/on/dashboard');
});
})
.factory('userService', function($auth) {
var service = {};
service.loggedIn = false;
service.submitLogin = submitLogin;
service.setLoggedIn = setLoggedIn;
service.getLoggedIn = getLoggedIn;
return service;
function submitLogin(params) {
$auth.submitLogin(params)
}
function setLoggedIn(bool) {
service.loggedIn = bool;
}
function getLoggedIn() {
return service.loggedIn;
}
});
My directive looked like this:
.directive('agNavigation', function(userService) {
if(userService.getLoggedIn()){
return {
restrict: 'AE',
replace: true,
templateUrl: '/views/userNavigation.html'
}
}else{
return {
restrict: 'AE',
replace: true,
templateUrl: '/views/navigation.html'
}
}
});
When logged in, the view changes to the dashboard but the directive does not update, which is logical because the directive isn't called again. Now, I am trying to use the $watch function but I cannot figure it out. See here:
.directive('agNavigation', function(userService) {
var navUrl = '';
if(userService.getLoggedIn()){
navUrl = '/views/userNavigation.html';
}else{
navUrl = '/views/navigation.html';
}
return {
restrict: 'AE',
replace: true,
templateUrl: navUrl,
link: function postLink(scope, element, attrs, controller) {
scope.$watch(function() {
return userService.getLoggedIn();
}), function(newValue, oldValue) {
if(newValue){
navUrl = '/views/userNavigation.html';
}else{
navUrl = '/views/navigation.html';
}
}
}
}
});
I am very new to AngularJS, thus I am very happy for any advice, or if I am approaching this in a wrong way.
userService.getLoggedIn() will fails in compile time of directiveas you write the code in your first example code. You should have really need to move that code to the templateUrl function & return the URL on basis of condition.
Code
.directive('agNavigation', function(userService) {
return {
restrict: 'AE',
replace: true,
templateUrl: function(){
if(userService.getLoggedIn()){
return '/views/userNavigation.html';
}
return '/views/navigation.html';
}
}
});

AngularJS directive to directive communication throwing an error about controller not found

I have 2 directives, one for searching and one for pagination. The pagination directive needs to access the search directive to find out what property we're currently searching by. When I load the page though, it throws an error saying Error: [$compile:ctreq] Controller 'search', required by directive 'pagination', can't be found!. However I have a controller setup in my search directive.
Here is my search directive:
angular.module('webappApp')
.directive('search', function ($route) {
return {
templateUrl: 'views/search.html',
restrict: 'E',
scope: {
searchOptions: '=',
action: '=',
currentProperty: '=',
currentValue: '='
},
controller: function($scope) {
$scope.searchBy = $scope.searchOptions[0].text;
$scope.searchByProperty = $scope.searchOptions[0].property;
$scope.setSearchBy = function(event, property, text) {
event.preventDefault();
$scope.searchBy = text;
$scope.searchByProperty = property;
};
$scope.search = function() {
$scope.searching = true;
$scope.currentProperty = $scope.searchByProperty;
$scope.currentValue = angular.element('#searchCriteria').val();
$scope.action($scope.searchByProperty, $scope.currentValue, function() {
$scope.searching = false;
});
};
$scope.reload = function() {
$route.reload();
};
}
};
});
Here is my pagination directive:
angular.module('webappApp')
.directive('pagination', function () {
return {
templateUrl: 'views/pagination.html',
restrict: 'E',
require: '^search',
scope: {
basePath: '#',
page: '=',
sort: '='
},
link: function(scope, element, attrs, searchCtrl) {
console.debug(searchCtrl);
scope.searchByProperty = searchCtrl.searchByProperty;
}
};
});
In order for one directive to use another's controller by use of require, it needs to either share the same element as the controller containing directive, or it has to be a child of it.
You can't use require in the way you have, where the elements are siblings.
Angular docs about directives, including require
If it doesn't make sense to rearrange the DOM in the way I've described, you should inject a service into both directives which contains the data/methods you wish to share between the two.
Note: you could also experiment with the $$nextSibling / $$prevSibling properties of the directives' scopes, but this would present only a very fragile solution
You cannot use require in directive like that, however , since the only thing you need to pass between directives is a string , just bind them to the same property in parent controller (it can be parent directive controller):
...
<div ng-app='app' ng-controller='MyCtrl as ctrl'>
<my-dir-one s1='ctrl.message'></my-dir-one>
<my-dir-two s2='ctrl.message'></my-dir-two>
and first directives:
app.directive('myDirOne', function ($route) {
return {
templateUrl: 'views/my-dir-one.html',
restrict: 'E',
scope: {
s1: '=',
second directive
app.directive('myDirTwo', function ($route) {
return {
templateUrl: 'views/my-dir-one.html',
restrict: 'E',
scope: {
s2: '=',

Categories