A confusing case when using controller and controllerAs in a directive - angularjs

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.

Related

not able to call a function defined in directive into my controller

(function () {
'use strict';
angular
.module('app')
.directive('documentList', documentList);
documentList.$inject = ['$window'];
function documentList($window) {
var directive = {
restrict: 'E',
controller: controller,
controllerAs: "dl",
templateUrl: 'directives/document/document-list.html',
transclude: false,
scope: {
productRef: "=",
productSerialNumber: "=",
title: "#",
eid: "#"
},
};
return directive;
function controller($scope, $state, $element, documentService, ngProgressFactory, registrationService) {
var self = this;
self.goToDetailPage=goToDetailPage;
function goToDetailPage(docId) {
return "a";
}
})();
(function() {
'use strict';
angular
.module('app')
.controller('DetailCtrl', detailCtrl);
// Implementation of controller
detailCtrl.$inject = ['$scope', '$state', '$stateParams', '$rootScope'];
function detailCtrl($scope, $state, $stateParams, $rootScope) {
var self=this;
//alert($stateParams.docId)
self.a=$scope.dl.goToDetailPage();
}
})();
Above is the code in my directive and I have a controller where I want to call goToDetailPage function . But when I am trying to access it through var a=$scope.goToDetailPage() , I am getting error in console.
Not able to rectify.
Any help is appreciated!!!
Thanks
//DetailPage
.state('app.sr.detail', {
name: 'detail',
url: '/detail',
templateUrl: 'views/detail/detail1.html',
controller: 'DetailCtrl',
controllerAs: 'vm'
})
A cool pattern you can use when you want to be able to invoke a function that lives in a directive but you need to be able to invoke from your controller is to pass in an object using two way binding and then extend that object with a function inside the directive. Inside your directive pass in an additional value:
scope: {
productRef: "=",
productSerialNumber: "=",
title: "#",
eid: "#",
control: '=', // note that this is passed in using two way binding
}
Then extend that object inside your directive's controller by attaching a function to it:
// this is in your directive's controller
$scope.control.goToDetailPage = function() {
// function logic
}
Now define the control object in your Controller and pass it into the directive. Because it is two way bound the function will be applied and available to be called in either scope.
// in your controller, assuming controller as syntax
var vm = this;
vm.control = {};
// somewhere later you can invoke it
vm.control.goToDetailPage();
Maybe try,
$scope.goToDetailPage = function (docId) {
return "a";
}
Or, to make use of the "controller as" syntax,
var a = dl.goToDetailPage();
As it looks like from the snippets, DetailCtrl is parent of documentList directive. Functions in a child scope can't be accesses from parent controller. Consider defining goToDetailPage in DetailCtrl and inject that into the directive's scope using '&'
EDIT
If you have something like this:
<div ng-controller="DetailCtrl">
<document-list ...></document-list>
</div>
Then the controller DetailCtrl 's scope is the parent scope and the documentList directive's is the child. And since you are defining an isolate scope in documentList directive using scope: {...} parent and child scopes are different. And a method defined in the isolate scope can't be accessed directly in the parent scope using $scope.methodName().
You can however do the other way around, i.e. define the method in the DetailCtrl and inject into the directive's scope using this:
<div ng-controller="DetailCtrl">
<document-list .... detail-page="goToDetailPage()"></document-list>
</div>
And change your directive's scope to:
scope: { ...,
detailPage:'&'
},
Now you can make a call to the function in the directive controller by calling detailPage()
See the Angular Guide
Hope that helps

Directives in 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.)

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

In angular.js, can a directive controller access data in a page controller that loaded it?

In angular.js, can a directive controller access data in a page controller that loaded it?
/**
* Profile directive
*/
.directive('profile', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/partials/users/_profile.html',
scope: {
user: '=',
show: '=?'
},
controller: function($scope, $rootScope){
$scope.show = angular.isDefined($scope.show) ? $scope.show : { follow: true, link: true };
$scope.currentUser = $rootScope.currentUser;
//do stuff here and then set data in UserShowCtrl
}
};
});
The <profile user="user"></profile> method is called from ./users/show.html which uses the UserShowCtrl controller.
Is there anyway I can use scope on the profile directive with its own controller and still be able to pass data to the UserShowCtrl?
Even though the profile can be isolated to its own functionality, it still needs to set some data on the page level in the UserShowCtrl controller.
Here is where _user.html is loading the <profile> directive. The data for the page is served by the UserShowCtrl and has some collections that get updated when things happen, like following a user.
<ol class="following" ng-show="showConnections == 'following'">
<li ng-repeat="following in user.following">
<profile user="connections[following]"></profile>
</li>
</ol>
Right now there is an ng-click="follow(user)"> that is happening in the _profile.html. I would like to be able to have the directive handle this but also update the collections in the UserShowCtrl.
Edit: here is a plunker demonstrating what I'm trying to do:
http://plnkr.co/edit/9a5dxMVg9cKLptxnNfX3
You need to use a service in order to share any information between controllers, directives, services
something like
angular.module('myapp',[]).
service('myservice',function(){
return {a:'A',b:'B'}
}).
controller('mycontroller',['myservice',function(myservice){
//do someting with myservice
}]).
directive('mydirective',['myservice',function(myservice){
//do someting with myservice
}]);
there controller and directive access the same data through the service
You can access the parent scope from your directive with $scope.$parent.myvar.
myvar will be resolved in parent scope, which means prototypical scope inheritance is used to resolve the variable.
However, this does not guarantee that myvar is coming from the same scope as UserShowCtrl since its possible that any scope in between the 'profile' directive and UserShowCtrl's scope may override 'myvar'.
A better solution would be to use directive-to-directive communication. There are generally two ways for directives to communicate:
Through attributes passed into your directive. You've already used this method to import 'user' and 'show' from parent scope into your directive's isolated scope.
Requiring another directive. When you use 'require: ^UserShow', you are specifying that your 'profile' directive requires another directive as a dependency. The '^' means that it will search for the directive on the current element, or any parent element further up the DOM tree. UserShow's controller is then passed to your link function:
.directive('UserShow', function () {
return {
restrict: 'E',
controller: function($scope){
$scope.myvar = 'test';
this.setMyVar = function(var) {
$scope.myvar = var;
}
}
};
});
.directive('profile', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/partials/users/_profile.html',
require: '^UserShow',
scope: {
user: '=',
show: '=?'
},
controller: function($scope, $rootScope){
},
link: function(scope, element, attr, UserShowCtrl) {
UserShowCtrl.setMyVar('hello world!);
}
};
});
HTML:
<user-show>
<profile>...</profile>
</user-show>
I am not quite sure what your after.
You are already having 2 two-way data bindings, which means that if you change user in your directive, that will also flow to the outside scope.
So you already have a solution in front of you...
So if that is not "good enough", there is something missing in your question.
Here is an illustration: http://plnkr.co/edit/qEH2Pr1Pv7MTdXjHd4bD?p=preview
However, if you use something in your outside template that creates a child scope, binding it as "value" there is NOT enough, you need to have a . in there.
But that is where there is missing something to the question, if you share your show.html I may be able to find where the scope breaks apart and explain why...
Relevant Source from demo.js:
app.directive('profile', function () {
return {
restrict: 'E',
replace: true,
template: '<div><input type="text" ng-model="user"></input></div>',
scope: { //defines an isolate scope.
user: '=',
show: '=?'
},
controller: function($scope, $rootScope){
$scope.show = angular.isDefined($scope.show) ? $scope.show : { follow: true, link: true };
$scope.currentUser = $rootScope.currentUser;
$scope.user = "Changed by scope!";
//do stuff here and then set data in UserShowCtrl
}
};
});
app.controller('UserShowCtrl', function($scope) {
$scope.value = "Value set outside!";
$scope.alertValue = function() {
alert($scope.value);
}
});
Relevant Source from home.html:
<div ng-controller="UserShowCtrl">
{{ value }}
<profile user="value"></profile>
<button ng-click="alertValue()">ALERT!</button>
</div>

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

Resources