Trouble using $attrs in directives controller - javascript

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

Related

Using service inside directive?

I am learning how to create custom directives.
My service looks like that:
myApp.service('myService',function(){
this.myFunction=function(myParam){
// do something
}
});
Here is my directive:
myApp.directive('myDirective',function(myService){
return {
restrict: 'E',
scope: {
param: '=myParam',
},
template: '<button ng-click="myService.myFunction(param)">Do action</button>',
}
});
In HTML, when I use <my-directive my-param="something"></my-directive> it properly renders as a button. However when I click it, myService.myFunction, doesn't get executed.
I suppose I am doing something wrong. Can someone give me a direction?
I guess this has something to do with the directive's scope.
The service wont be available directly inside the template. You'll have to use a function attached to the directive's scope and call the service function from within this function.
myApp.directive('myDirective',function(myService){
return {
restrict: 'E',
scope: {
param: '=myParam',
},
template: '<button ng-click="callService(param)">Do action</button>',
link: function(scope, element, attrs) {
scope.callService = function() {
myService.myFunction();
}
}
}
});
It doesn't work because in your example a directive doesn't actually know what is myService. You have to explicitly inject it e.g.:
myApp.directive('myDirective', ['myService', function(myService){ ... }]);
See also this question or this question.
You should use a controller to do all DOM-modifications.
See this plunkr: https://plnkr.co/edit/HbfD1EzS0av5BG6NgtIv?p=preview
.directive('myFirstDirective', [function() {
return {
'restrict': 'E',
'controller': 'MyFirstController',
'controllerAs': 'myFirstCtrl',
'template': '<h1>First directive</h1><input type="text" ng-model="myFirstCtrl.value">'
};
}
You can inject the service in the controller and then call that function inside your template:
Inject myService into controller:
myApp.controller("ctrl", function($scope, myService) {
$scope.doService = function(myParam) {
return myService.myFunction(myParam);
};
});
Call doService method of the controller inside your template:
myApp.directive('myDirective',function(){
return {
restrict: 'E',
scope: {
param: '=myParam',
},
template: '<button ng-click="doService(param)">Do action</button>',
}
});

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

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

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.

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: '=',

Any way to dynamically load angular directives?

Here's a short fiddle:
http://jsfiddle.net/aSg9D/
Basically, neither <div data-foo-{{letterA}}></div> nor <div data-ng:model="foo-{{letterB}}"></div> are interpolated.
I'm looking for a way to dynamically load one of several inline templates.
Pardon me if this has already been asked before, but I searched and couldn't find it.
I believe Radim Köhler has the correct answer. Just before it was posted, I hacked together something to load directives from another directive like this:
angular.module('myApp', []).directive('loadTmpl', function($compile) {
return {
restrict: 'A',
replace: true,
link: function($scope, $element, $attr) {
$element.html("<div data-card-"+$attr.loadTmpl+"></div>");
$compile($element.contents())($scope);
}
};
});
And:
<div data-load-tmpl="{{directiveName}}"></div>
I think that's the minimalist approach, but there's probably something wrong with it, so just look at the answer below.
Let's adjust it this way (the udpated fiddle). The view:
<div my-selector name="letterA"></div>
<div my-selector name="letterB"></div>
the controller:
function myCtrl($scope) {
$scope.letterA = 'bar';
$scope.letterB = 'baz';
}
And here is new directive mySelector, containing the selector
.directive('mySelector',
[ '$templateCache','$compile',
function($templateCache , $compile) {
return {
scope: {
name: '='
},
replace: true,
template: '',
link: function (scope, elm, attrs) {
scope.buildView = function (name) {
var tmpl = $templateCache.get("dir-foo-" + name);
var view = $compile(tmpl)(scope);
elm.append(view);
}
},
controller: ['$scope', function (scope) {
scope.$watch('name', function (name) {
scope.buildView(name);
});
}],
};
}])
.run(['$templateCache', function ($templateCache) {
$templateCache.put("dir-foo-bar", '<div data-foo-bar></div>');
$templateCache.put("dir-foo-baz", '<div data-foo-baz></div>');
}])
In case you like it, all credits goes to Render a directive inside another directive (within repeater template) and AngularJS - Directive template dynamic, if you don't, blame me.

Categories