Directives in AngularJS - angularjs

Hi I am trying to learn AngularJS Directives and I came really close but would like to extend my learning by cleaning and de-coupling my directive code.
Directive:
app.directive('ngSparkline', function () {
var url = "http://api.openweathermap.org/data/2.5/forecast/daily?mode=json&units=imperial&cnt=14&callback=JSON_CALLBACK&q=";
return {
restrict: 'A',
require: '^ngCity',
transclude: true,
scope: {
ngCity: '#'
},
templateUrl: 'app/partials/weatherTemplate.html',
controller: ['$scope', '$http', function($scope, $http) {
$scope.getTemp = function(city) {}
}],
link: function (scope, iElement, iAttrs) {
scope.getTemp(iAttrs.ngCity);
scope.$watch('weather', function (newVal) {
if (newVal) {
var highs = [];
angular.forEach(scope.weather, function (value) {
highs.push(value.temp.max);
});
chartGraph(iElement, highs, iAttrs);
}
});
}
};
});
As you can see I am not trying to write inline template rather use templateUrl. Now the problem is for the controller when I try using a .js controller instead of writing the controller code inline, I receive an error. How do I achieve this.
I tried:
I tried passing
controller: '#',
name: 'ctrl'
and I pass the 'ctrl' as:
<div ng-sparkline ng-city="San Francisco" ctrl="weatherController"></div>
it gives me controller not found. My project structure is something like below.
What am I doing wrong?
Is there a better/correct way of doing this?
Please suggest.
Note: I am learning this exercise from "http://www.ng-newsletter.com/posts/directives.html"

Why not just specify ng-controller on your element? If WeatherController is defined somewhere else then it doesn’t affect your directive definition, just leave the controller out of there.
<div ng-sparkline ng-city="San Francisco" ng-controller="WeatherController"></div>
Provided somewhere you do have the controller defined like
app.controller('WeatherController', ['$scope', '$http', function ($scope, $http) {
$scope.getTemp = // …
}]);
(BTW, if you noticed, the AngularJS convention is to name controllers in UpperCase fashion.)

Related

A confusing case when using controller and controllerAs in a directive

If a directive is using a controller directly, why is calling a method on the controller by referring the controller by its alias, not doing anything?
Imagine we have the following piece of code:
var app = angular.module('App', []);
app.controller('MyController', ['$scope', function($scope) {
$scope.doAction = function() {
alert("controller action");
}
this.doAction2 = function() {
alert("controller action 2");
}
}]);
app.directive('myDirective', [function() {
return {
restrict: 'E',
scope: {},
controller: 'MyController',
controllerAs: 'myCtrl',
bindToController: true,
template: "<a href='#' ng-click='myCtrl.doAction()'>Click it!<a><br><a href='#' ng-click='myCtrl.doAction2()'>Click it #2!<a> " ,
link: function($scope, element, attrs, controller) {
console.log($scope);
}
}
}]);
While the first link won't work, the second will. To make the the first one work, I'd have to drop the alias, i.e. instead of calling the action by ng-click='myCtrl.doAction()' to call it as: ng-click='doAction()'
Shouldn't it work using the alias too? I mean, you are much more likely to find and reuse a controller, where the developers have attached actions to the $scope object and not to this
ControllerAs exposes the controller instance on the scope under $scope[alias].
In your example, the scope looks (conceptually) like this:
$scope = {
$id: 5,
myCtrl: {
doAction2: function(){...}
},
doAction: function(){...}
}
So, you can see why ng-click="myCtrl.doAction()" doesn't work.
The Controller-As approach has some benefits over directly exposing properties on the scope - one is that it does not pollute the scope (and its descendants) with properties that they may not need. It also inherently provides the dot-approach (.) to work properly with ng-model. You can find more information in this SO question/answer.

How do I use array syntax for dependency injection with a directive controller?

I want to be able to specify the controller of my directive with an inline function, but I also want to use ng-strict-di. What syntax is required to do this?
(function(){
angular.module("myAngularModule")
.directive("myDirective", function(){
return {
restrict: 'E',
templateUrl: "templates/my-template.html",
link: function ($scope, element, attrs) {
// ...
},
// This causes an ng-strict-di exception because I'm using implicit annotation for the dependencies - what is the correct syntax?
controller: function($scope, myService) {
// ...
}
};
})
// This syntax is fine
.controller("myWorkingController",["$scope","myService", function($scope, myService){
// ...
}]);
});
Just because the controller is anonymous doesn't meant the syntax changes. Pass an array like you would any other controller assignment. Angular will understand.
controller: ["$scope","myService", function($scope, myService){
// ...
}]
Inject the service in to your directive as, its something like injecting into the controller,
.directive("myDirective", function(myService){
and remove it from the controller.
.directive("myDirective", function(myService){
return {
restrict: 'E',
templateUrl: "templates/my-template.html",
link: function ($scope, element, attrs) {
// ...
},
controller: function($scope) {
// ...
}
};
})
then myService can be access in the controller of the directive.

Unable to pass controller and directive scope into child directive using require

I am trying to pass the controller scope of parent controller and parent directive into a child directive but facing an error saying that the controller is not available. Here is a plunk for that
http://plnkr.co/edit/aahgOK9oFFjcP2y5VkVa?p=preview
HTML:
<div ng-controller="MainCtrl as mc">
<new-dir>
<data-customer-details customer="mc.customers[0]" logger="mc.logger()" version-age="{{mc.age}}"></data-customer-details>
</new-dir>
</div>
OK, so I tinkered with your plunker a bit. I couldn't get it working using Controller As...I had to change it over to $scope injection on the main controller. Then I created a new scope on newDir by setting scope: true.
You don't actually need to require the MainCtrl because these directives are automatically children of that scope anyway.
I changed your 'MainCtrl' to this:
angular.module('plunker').controller('MainCtrl', function ($scope) {
$scope.name = 'World';
$scope.customers = [{
"name": "angularjs 1.4",
"version": "1.4"
}, {
"name": "angularjs 1.3",
"version": "1.3"
}, {
"name": "angularjs 1.2",
"version": "1.2"
}];
$scope.age = 30;
$scope.logger = function() {
console.log('clicked');
}
$scope.ctrlScopeVariable = 'im in controller scope';
})
Minor change to newDir:
function newDir (){
return {
scope: true, //you need this
controller: function($scope){
$scope.val= 'someval';
console.log($scope.$parent.ctrlScopeVariable)
},
link: function(scope, el, attr, ctrl) {
console.log(scope.$parent.name)
}
}
}
And the last directive:
function CustomerDetails() {
var directive = {
scope: {
customer: '=',
logger: '&',
myNewAge: '#versionAge'
},
restrict: 'EA',
require: ['^newDir'],
controllerAs: 'cd',
templateUrl: 'customer-details.html',
link: linkFunction,
controller: function($scope){
console.log($scope.$parent.$parent.ctrlScopeVariable);
var cd = this;
cd.newval = 'new val';
}
};
function linkFunction(scope, elem, attributes, controllers, transclude) {
console.dir(controllers);
scope.fromMainCtrl = scope.$parent.$parent.ctrlScopeVariable
}
return directive;
}
The Plunker
I added a binding to the customer details template that passes in the $scope.ctrlScopeVariable from the main controller, so you can see the MainCtrl scope is accessible form the child directive.
In regards to require, the relevant documentation is here, I think:
If it is necessary to reference the controller or any functions bound
to the controller's scope in the template, you can use the option
controllerAs to specify the name of the controller as an alias. The
directive needs to define a scope for this configuration to be used.
This is particularly useful in the case when the directive is used as
a component.
Looking back at myPane's definition, notice the last argument in its
link function: tabsCtrl. When a directive requires a controller, it
receives that controller as the fourth argument of its link function.
Taking advantage of this, myPane can call the addPane function of
myTabs.
Essentially, you can use it to reference a parent controller on which you need to access some functions or something. Notably, it becomes available under whatever alias you give it as the fourth argument of your link function.
EDIT:
In this Plunker I added a function to the controller of newDir, required newDir in the CustomerDetail directive, and then called that function in the CustomerDetail link function:
CustomerDetails directive:
//some stuff
require: '^newDir',
//some stuff
link: function(scope, el, attr, newDirCtrl) {
console.log(newDirCtrl.doubleNum(100));
}
newDir controller:
controller: function($scope){
this.doubleNum = function(num) {
return num*2
}
// some stuff
}
First you need to declare a variable as callback function:
var MainCtrlFn = function() { .... }
Then, you can set it as parameter to angularJS:
angular.module('plunker').controller('MainCtrl', MainCtrlFn);

How to use bound value in isolate scope in controller

I have a directive. I want to use an attribute value of that directive in my directive's controller. I attempt to do this by binding the attribute value to my isolate scope. However I run into a problem in that the attribute value seems not to be immediately bound to the isolate scope.
Consider the following code:
angular.module('startup.directives.decision', [])
.directive('decisionMaker', [function () {
return{
restrict: 'E',
templateUrl: 'views/directives/decision.html',
scope: {
decisionType:"#",
},
controller: ['$scope', 'Decisions', function ($scope, Decisions){
//this prints undefined
console.log($scope.decisionType);
//this prints the proper value when called a couple seconds after page load
$scope.getDecisionType = function(){
console.log($scope.decisionType);
};
//this is my motivation for wanting $scope.decisionType to be bound immediately
if($scope.decisionType==='hire'){
//should do some stuff here
}
}]
};
}]);
I call my directive like this:
<decision-maker decision-type="investment"></decision-maker>
<decision-maker decision-type="hire"></decision-maker>
You're going to want to use the $observe function. See the Attributes section of the Directives documentation.
So, something like this:
controller: ['$scope', '$attrs', 'Decisions', function ($scope, $attrs, Decisions){
//this prints undefined
console.log($scope.decisionType);
//this prints the proper value when called a couple seconds after page load
$scope.getDecisionType = function(){
console.log($scope.decisionType);
};
$attrs.$observe('decisionType', function(value) {
//this is my motivation for wanting $scope.decisionType to be bound immediately
if($scope.decisionType==='hire'){
//should do some stuff here
}
});
}]
Instead of trying to access my attributes by binding them with the scope, I can access them more directly via the $attrs object!
angular.module('startup.directives.decision', [])
.directive('decisionMaker', [function () {
return{
restrict: 'E',
templateUrl: 'views/directives/decision.html',
scope: {},
controller: ['$scope', '$attrs', 'Decisions', function ($scope, $attrs, Decisions){
//this prints the correct value
console.log($attrs.decisionType);
if($attrs.decisionType==='hire'){
//should do some stuff here
}
}]
};
}]);

AngularJS : minification issue in directive

I have yet another issue with minification. This time it's because of the $scope service passed to the directive's controller. See below code:
angular.module('person.directives').
directive("person", ['$dialog', function($dialog) {
return {
restrict: "E",
templateUrl: "person/views/person.html",
replace: true,
scope: {
myPerson: '='
},
controller: function ($scope)
{
$scope.test = 3;
}
}
}]);
If I comment out the controller part, then it works fine.
As you can see, I've used the array declaration for the directive, so the $dialog service is known to Angular even after minification. But how am I supposed to do it for the $scope service on the controller ?
You need to declare a controller as follows:
controller: ['$scope', function ($scope)
{
$scope.test = 3;
}]
Full example here:
angular.module('person.directives').
directive("person", ['$dialog', function($dialog) {
return {
restrict: "E",
templateUrl: "person/views/person.html",
replace: true,
scope: {
myPerson: '='
},
controller: ['$scope', function ($scope)
{
$scope.test = 3;
}]
}
}]);
A solution provided by #Sam would work to but it would mean exposing directive's controller to the whole application which is unnecessary.
ok, I ended up creating the controller in a separate file :
angular.module('person.controllers').controller('personCtrl', ['$scope', function ($scope) {
$scope.test = 3;
}]);
then in the directive, I assign the controller by name:
controller: 'personCtrl'
Not sure it's the best way. It looks clean though. What do you think ?

Resources