I have a directive which I want to tightly couple with a controller as a component. I assumed I was following best practice by explicitly passing ion my functions even though I was declaring the controller to use. Here is an example:
app.js
var app = angular.module('plunker', [])
app
.controller('myCtrl', function($scope) {
$scope.output = '';
$scope.foo = function () {
$scope.output = 'foo';
}
$scope.bar = function () {
$scope.output = 'bar';
}
})
.directive('myDirective', function() {
return {
scope: {
output: '=',
foo: '&',
},
templateUrl: 'template.html',
replace: true,
controller: 'myCtrl',
};
})
template.html
<div>
<button ng-click="foo()">Click Foo</button>
<p>You clicked: <span style="color:red">{{output}}</span></p>
</div>
index.html
<body>
<my-directive
output="output"
foo="bar()"> <!-- pass in the *bar* function instead of the *foo* function -->
</my-directive>
</body>
Plunkr: http://plnkr.co/edit/Y4lhxuXbK9YbjAklR7v1?p=preview
Here, even though I'm passing in the bar() function, the output is 'foo' when the button is clicked. If I uncouple the controller by commenting out controller: 'myCtrl' in the directive, the output becomes 'bar'.
I thought I could declare the controller but still be free to pass in which functions I desire to the directive. It also seems that explicitly passing these functions in is a little redundant if the directive just looks up to the controller to find it (I can pass nothing into the directive and it still works).
This is especially problematic when testing as I would like to pass in my own stub functions to the directive, which at the moment I cannot do.
Is there some way to achieve what I want or am I doing something fundamentally wrong?
EDIT I meant to not have the controller declared in the HTML.
Remove the controller property on the directive:
.directive('myDirective', function() {
return {
scope: {
output: '=',
foo: '&',
},
templateUrl: 'template.html',
replace: true,
// controller: 'myCtrl',
};
})
You're wiring up the same controller to the directive as the parent, which is overwriting all the properties you're trying to pass in via isolate scope. The controller is wired up twice, once on the parent scope and then again on the directive. Removing this will allow you to pass in the function bar() and it will not be overwritten.
Here's the Plunker Demonstration
When running inside a directive, the $scope is initialized with output and foo variables before the controller constructor is called. Your controller is essentially overwriting these properties.
A simple check in your controller
if(!$scope.foo)
{
$scope.foo = function () {
$scope.output = 'foo';
}
}
Would work.
PS. I'm assuming your example is a simplification of your problem. If it's not, then the other answer's advice to simply remove the controller from the directive is the best approach.
Related
Still new in Angular world and I have a categoriesController wrapping a custom directive categories-select like this:
<div ng-controller="categoriesController" ng-init="init()">
<categories-select></categories-select>
</div>
angular.module('app')
.directive('categoriesSelect', [function () {
return {
restrict: "E",
templateUrl: 'categoriesSelectTemplate',
controller: ['$scope', function ($scope) {
console.log($scope)
console.log($scope.categories)
}],
}
}])
The categoriesController has an array of categories.
The strange behavior I get inside the directive is:
So, whenever I try to get the categories array to do something with it, I find that it's empty, but it's -at the same time- populated in the $scope!
I tried also to isolate the directive scope and pass the array object to it, but I got the same strange behavior, but anyway, I want to know why this simple code doesn't work.
I always pass necessary parameters inside directive, never share parent scope.
So looking on each directive you see what is required, never got unknown behaviour.
so Angular directive:
angular.module('app')
.directive('categoriesSelect', [function () {
return {
restrict: "E",
scope: {
categories: '='
},
templateUrl: 'categoriesSelectTemplate',
controller: ['$scope', function ($scope) {
console.log($scope)
console.log($scope.categories)
}],
}
}])
and HTML:
<categories-select categories="categories"></categories-select>
Edited:
I think your directive controller is getting initialised before categories coming from parent scope. try to draw categories on directive HTML and you will get them, or use $scope.$watchCollection inside directive controller
$scope.$watchCollection('categories', function (newVal, oldVal) {
console.log(newVal);
});
Changes to my scope variable foo are getting updated in the html. When that value is change inside the scope of a directive's controller, it isn't updating in the html.
What do I need to do to make it update?
I have a simple example:
app.js
var app = angular.module('app', []);
app.controller('ctrl', function($scope) {
$scope.foo = 99;
$scope.changeValue = function() {
$scope.foo = $scope.foo + 1;
}
});
app.directive('d1', function(){
return {
restrict: 'E',
scope: {
theFoo: '='
},
templateUrl: 'd1.html',
controller: 'd1Ctrl',
}
});
app.controller('d1Ctrl', function($scope) {
$scope.test = $scope.theFoo;
});
d1.html
<div>
<p>The value of foo is '{{theFoo}}'.</p>
<p>The value of test is '{{test}}'.</p>
</div>
inside index.html
<d1 the-foo='foo'>
</d1>
<button ng-click='changeValue()'>change value</button>
So in summary, {{theFoo}} is updating, but {{test}} isn't. Why?
The reason is that $scope.foo value is a primitive.
In the directive controller you only assign $scope.test once when controller initializes. Primitives have no inheritance the way objects do so there is nothing that would change $scope.test after that initial assignment
If you used an object instead to pass in ... inheritance would be in effect and you would see changes...otherwise you would need to watch $scope.theFoo and do updates to $scope.test yourself
The code you have in your controller only initializes to that value if it is indeed set at the time the controller is linked. Any subsequent changes are not going to work.
If you want to bind any subsequent changes, then you need to set a $watch statement either in your controller or a link function.
$scope.$watch( 'theFoo', function(val){ $scope.test = val; })
updated plunker - http://plnkr.co/edit/eWoPutIJrwxZj9XJu6QG?p=preview
here you have isolated the scope of the directive, so test is not visible to the d1.html, if you need to change test along with the theFoo you must first make it visible to the directive by
app.directive('d1', function(){
return {
restrict: 'E',
scope: {
theFoo: '=',
test : '=' //getting test as well
},
templateUrl: 'd1.html',
controller: 'd1Ctrl',
}
});
and in index.html you should pass the value to the test by
<d1 the-foo='foo' test='foo'></d1>
in the above code your controller is not much of a use , code will work fine even without this part controller: 'd1Ctrl'.
with this example you dont have to use $watch.
(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
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.
I am trying to expand on the bootstrap ui library with my own custom control. This control will be used in an AngularJS app. Currently, I'm getting stuck on the scoping.
My plunker is here
This plunker is a simplified version of a more complex control. The concept that I'm trying to highlight is the scoping. You will notice that the custom control, my-query, is pre-populated with the value of myController.$scope.query. You will also see that the query is put in the page underneath the custom control. As I type, the value does NOT get updated. Why? My code looks like the following:
myApp.directive('myQuery', [function() {
return {
restrict:'E',
transclude: true,
scope: {
query: '='
},
template: '<div ng-controller="myQueryController"><input type="text" ng-model="query" /><button ng-click="go_Click()">go</button></div>'
};
}]);
myApp.controller('myQueryController', ['$scope', function($scope) {
$scope.go_Click = function() {
$scope.$emit("goClicked");
};
}]);
What am I doing wrong?
In your directive template, you are adding an additional controller which is adding in another scope. That is what is causing the problem. Instead of doing it that way, move the controller logic into either a controller function or a link function defined on your directive, either will work.
Try this. Here's an example using a controller function. Note that I moved your original myQueryController inside the directive and removed the ng-controller directive from the myQuery directive's template.
'use strict';
var myApp = angular.module('myApp', []);
myApp.controller('myController', ['$scope', function($scope) {
$scope.queryValue = 'test';
$scope.$on('goClicked', function() {
$scope.performAction();
});
$scope.performAction = function() {
alert('Using ' + $scope.queryValue);
};
}]);
myApp.directive('myQuery', [function() {
return {
restrict:'E',
transclude: true,
scope: {
query: '='
},
template: '<div><input type="text" ng-model="query" /><button ng-click="go_Click()">go</button></div>',
controller : function ($scope) {
$scope.go_Click = function() {
$scope.$emit("goClicked");
};
}
};
}]);
<div ng-controller="myQueryController">
A controller creates a new scope. So <input type="text" ng-model="query" /> doesn't use query from the directive's scope but from the controller's scope. Instead of using a controller you can define the go_Clickfunction in the directive's link method.
Do you need this?:
http://plnkr.co/edit/6IrlnXvsi2Rneee0hGC8?p=preview
scope: {
model: '='
}
The problem was that you used a primitive type which was passed by value into your directive. Always use complex types which are passed by reference.