I ran across a rather un-intuitive aspect of AngularJS directives and I'm curious if there's a logical reason for this behavior. When I specify a controller in a directive's definition:
angular.module('MyApp.directives', [])
.directive('myDirective, function () {
templateUrl: "my-template.html",
controller: "MyController"
};
the scope of the directive's template is the same for all instances of the directive.
However, if I don't specify a controller in the directive's definition:
angular.module('MyApp.directives', [])
.directive('myDirective, function () {
templateUrl: "my-template.html"
};
and instead define the controller directly on the directive using ng-controller:
<my-directive ng-controller="MyController"/>
each instance of MyController receives its own child scope.
I expected these approaches to yield the same results, but apparently Angular treats the controller's scope differently depending on how the controller is declared. Is there a reason for this difference?
The reason for this, I believe, is the fact that the directive as a definition is a singleton. So when you enter a controller as a property on singleton's return object, it is shared for all the instances.
But when you define the markup with ng-controller, the ng-controller directive instantiates that controller on each element, so it creates a new controller (and thus a new scope) for every directive instance.
Related
I got a directive that loads a different template depending on the variable type that is passed to it. I pass to the isolated scope of the directive the variables patient and service too.
.directive('serviceCharts', serviceCharts);
function serviceCharts() {
return {
restrict: 'E',
link: function (scope, element, attrs) {
if(attrs.type) {
scope.template = 'views/' + type + '.html';
}
attrs.$observe('type', function (type) {
if(attrs.type) {
scope.template = 'views/' + type + '.html';
}
});
},
template: '<div ng-include="template"></div>',
scope:{
patient:'=',
service:'='
}
};
}
In the template (views/myservice.html for example) I load the controller:
<div ng-controller="myCtrl as vm">
VIEW
</div>
And in the controller (myCtrl) I access to the patient and service this way:
service = $scope.$parent.$parent.service;
patient = $scope.$parent.$parent.patient;
This works fine, but I don't like this way of accessing the variables via the $parent.$parent. This is messing with my tests too.
Is there another (better) way to do this?
Thank you!
You could create a wrapper object for patient & service properties. That can be named as model & then provide that model object to your directive.
Then problem with your current approach is, ng-include create a child scope for template which it renders in it. So as your passing primitive type object binding to directive, If you are changing any of child primitive type binding in child scope. It loses a binding that's why tend to using $parent.$parent notation exactly bind to original source object.
$scope.model = {
patient:'My Patient',
service:'My Service'
};
By making above object structure will ensure you're following Dot Rule. Usage of Dot Rule will avoid $parent.$parent explicit scope annotation.
Directive scope binding will changed down to below
scope:{
model:'='
}
And directive usage will look like below
<service-charts type="{{'sometype'}}" model="model"></service-charts>
The other alternative than Dot Rule to such kind of scoping related issue is follow controllerAs pattern. But then as you are gonna use isolated scope with controllerAs you should make bindToController: true option to true for making sure all the scope are merged down to controller context.
scope:{
patient:'=',
service:'='
},
controllerAs: '$ctrl',
bindToController: true
And then use $ctrl before each directive scoped variable.
Yes, there is a better way to do this. You should use services and store variables in those services (in your case you should create a factory for storing data). Then you can inject those services and access their data.
Sidenote:
You can use { ..., controller: 'controllerName', controllerAs: 'vm' } syntax in your directive so you do not need to declare those in your html.
Method 1
In HTML:
<my-directive>
</my-directive>
In Scripts:
function myDirective() {
var ddo = {
[LOTS OF OTHER VERY IMPORTANT DIRECTIVE PROPERTIES HERE]
controller: myController
bindToController: true
};
return ddo;
}
Method 2
In HTML:
<my-directive ng-controller="myController">
</my-directive>
In Scripts:
function myDirective() {
var ddo = {
[LOTS OF VERY IMPORTANT DIRECTIVE PROPERTIES HERE]
};
return ddo;
}
Both cases there is a directive with some other properties (template, etc.) but I am just changing where I put controller. Are the two methods analogous?
No, first method is preferred.
Difference is in how angular treats scopes.
Second method makes directive dependent on external scope (created by ng-controller="myController") which is against the point of creating directive (code isolation).
Second method will work the same when you use parents scope (by default, when you don't set scope property in directive DDO - Ref: What is default Angular directive scope)
Second method won't work if you have isolated scope in directive, created like this:
scope: {
param1: "="
}
As you won't be able access properties from myController.
Edit:
Directives rules might be complicated to understand all cases, consider using .component() as it has much simpler and follows best practices - Introduction to Angular's components.
Further reading:
Binding to Directive Controllers in Angular 1.3+
"use strict";
angular.module("foo")
.directive("breadcrumbs", function($rootScope, $timeout) {
return {
replace: true,
scope: false,
templateUrl: '/js/shared/directives/breadcrumbs.tpl.html',
link: function(scope, element, attrs, controller, transcludeFn){
console.debug(scope.parent);
...
Sometimes scope.parent is the scope of the current controller, sometimes it's $rootScope. I've discovered that when my custom directive is used like this:
<breadcrumbs></breadcrumbs>
it receives the controller scope, but when used with ng-if:
<breadcrumbs ng-if="searchData"></breadcrumbs>
it receives a new scope nested within the controller scope.
How do I write my directive to always receive the controller scope without resorting to any hackery (such as adding ng-if="true")?
Update: I've corrected the description of the problem. The issue is that I get scopes at different levels of nesting depending on whether ng-if is used, regardless of whether scope: false or scope: true was used.
First of all, if the directive is in context of a controller scope, you'll never receive $rootScope as the scope of directive.
When you use ng-if, you are receiving a prototypically inherited child scope (of the immediate parent scope, in this case the controller) created by ng-if becuase your directive has scope: false (which means pass in the closest scope instead of creating one)
You can always request a prototypically inherited child scope by setting scope:true or an isolated scope via scope:{}.
What ng-if=true dose is create a new scope for your element.
You could achive that by changing the parameter
From
scope: false
To
scope: true
I am trying to understand $parse, based on the documentation. But I am having trouble to get my test code working. Am I using $parse service the right way?
The main part of the code is:
app.directive('try', function($parse) {
return {
restrict: 'E',
scope: {
sayHello: "&hello"
},
transclude: true,
template: "<div style='background:gray;color:white'>Hello I am try: <span ng-transclude></span><div>",
link: function($scope, $elem, $attr) {
var getter = $parse($attr.sayHello);
// var setter = getter.assign;
$elem.on('click', function() {
getter($scope);
$scope.$apply();
});
}
};
});
See my code at: http://plnkr.co/edit/lwV5sHGoCf2HtQa3DaVI
I haven't used the $parse method, but this code achives what you are looking for:
http://plnkr.co/edit/AVvxLR4RcmWhLo8eqYyd?p=preview
As far as I can tell, the $parse service is intended to be used outside of an isolate scope.
When you have an isolate scope, like in your directive, you can obtain a reference to the parent scope's function using the 'sayHello': '&' as proposed in Shai's answer. The $parse service might still work as expected even with an isolate scope, if you are able to pass in the parent scope instead of the directive's scope when calling getter($scope), but I haven't tested that.
Edit: This is indeed the case - using getter($scope.$parent) works fine. When an isolate scope is used in your directive, the $scope variable no longer refers to the correct context for the getter function returned by the $parse service. Access the correct one by using $scope.$parent.
However, if you are avoiding an isolate scope, your approach works well. Try removing the scope: { ... } section out of your directive definition entirely and you'll see it works fine. This is handy if you are creating a directive for event binding that might be applied to an element in conjunction with another directive that has an isolate scope, say a dragenter directive (which isn't provided by Angular). You couldn't use Shai's method in that case, since the isolate scopes would collide and you'd get an error, but you could use the $parse service.
Here's an updated plunker with the scope removed from the directive definition: http://plnkr.co/edit/6jIjc8lAK9yjYnwDuHYZ
I'm trying to dynamically assign a controller for included template like so:
<section ng-repeat="panel in panels">
<div ng-include="'path/to/file.html'" ng-controller="{{panel}}"></div>
</section>
But Angular complains that {{panel}} is undefined.
I'm guessing that {{panel}} isn't defined yet (because I can echo out {{panel}} inside the template).
I've seen plenty of examples of people setting ng-controller equal to a variable like so: ng-controller="template.ctrlr". But, without creating a duplicate concurrant loop, I can't figure out how to have the value of {{panel}} available when ng-controller needs it.
P.S. I also tried setting ng-controller="{{panel}}" in my template (thinking it must have resolved by then), but no dice.
Your problem is that ng-controller should point to controller itself, not just string with controller's name.
So you might want to define $scope.sidepanels as array with pointers to controllers, something like this, maybe:
$scope.sidepanels = [Alerts, Subscriptions];
Here is the working example on js fiddle http://jsfiddle.net/ADukg/1559/
However, i find very weird all this situation when you might want to set up controllers in ngRepeat.
To dynamically set a controller in a template, it helps to have a reference to the constructor function associated to a controller. The constructor function for a controller is the function you pass in to the controller() method of Angular's module API.
Having this helps because if the string passed to the ngController directive is not the name of a registered controller, then ngController treats the string as an expression to be evaluated on the current scope. This scope expression needs to evaluate to a controller constructor.
For example, say Angular encounters the following in a template:
ng-controller="myController"
If no controller with the name myController is registered, then Angular will look at $scope.myController in the current containing controller. If this key exists in the scope and the corresponding value is a controller constructor, then the controller will be used.
This is mentioned in the ngController documentation in its description of the parameter value: "Name of a globally accessible constructor function or an expression that on the current scope evaluates to a constructor function." Code comments in the Angular source code spell this out in more detail here in src/ng/controller.js.
By default, Angular does not make it easy to access the constructor associated to a controller. This is because when you register a controller using the controller() method of Angular's module API, it hides the constructor you pass it in a private variable. You can see this here in the $ControllerProvider source code. (The controllers variable in this code is a variable private to $ControllerProvider.)
My solution to this issue is to create a generic helper service called registerController for registering controllers. This service exposes both the controller and the controller constructor when registering a controller. This allows the controller to be used both in the normal fashion and dynamically.
Here is code I wrote for a registerController service that does this:
var appServices = angular.module('app.services', []);
// Define a registerController service that creates a new controller
// in the usual way. In addition, the service registers the
// controller's constructor as a service. This allows the controller
// to be set dynamically within a template.
appServices.config(['$controllerProvider', '$injector', '$provide',
function ($controllerProvider, $injector, $provide) {
$provide.factory('registerController',
function registerControllerFactory() {
// Params:
// constructor: controller constructor function, optionally
// in the annotated array form.
return function registerController(name, constructor) {
// Register the controller constructor as a service.
$provide.factory(name + 'Factory', function () {
return constructor;
});
// Register the controller itself.
$controllerProvider.register(name, constructor);
};
});
}]);
Here is an example of using the service to register a controller:
appServices.run(['registerController',
function (registerController) {
registerController('testCtrl', ['$scope',
function testCtrl($scope) {
$scope.foo = 'bar';
}]);
}]);
The code above registers the controller under the name testCtrl, and it also exposes the controller's constructor as a service called testCtrlFactory.
Now you can use the controller in a template either in the usual fashion--
ng-controller="testCtrl"
or dynamically--
ng-controller="templateController"
For the latter to work, you must have the following in your current scope:
$scope.templateController = testCtrlFactory
I believe you're having this problem because you're defining your controllers like this (just like I'm used to do):
app.controller('ControllerX', function() {
// your controller implementation
});
If that's the case, you cannot simply use references to ControllerX because the controller implementation (or 'Class', if you want to call it that) is not on the global scope (instead it is stored on the application $controllerProvider).
I would suggest you to use templates instead of dynamically assign controller references (or even manually create them).
Controllers
var app = angular.module('app', []);
app.controller('Ctrl', function($scope, $controller) {
$scope.panels = [{template: 'panel1.html'}, {template: 'panel2.html'}];
});
app.controller("Panel1Ctrl", function($scope) {
$scope.id = 1;
});
app.controller("Panel2Ctrl", function($scope) {
$scope.id = 2;
});
Templates (mocks)
<!-- panel1.html -->
<script type="text/ng-template" id="panel1.html">
<div ng-controller="Panel1Ctrl">
Content of panel {{id}}
</div>
</script>
<!-- panel2.html -->
<script type="text/ng-template" id="panel2.html">
<div ng-controller="Panel2Ctrl">
Content of panel {{id}}
</div>
</script>
View
<div ng-controller="Ctrl">
<div ng-repeat="panel in panels">
<div ng-include src="panel.template"></div>
</div>
</div>
jsFiddle: http://jsfiddle.net/Xn4H8/
Another way is to not use ng-repeat, but a directive to compile them into existence.
HTML
<mysections></mysections>
Directive
angular.module('app.directives', [])
.directive('mysections', ['$compile', function(compile){
return {
restrict: 'E',
link: function(scope, element, attrs) {
for(var i=0; i<panels.length; i++) {
var template = '<section><div ng-include="path/to/file.html" ng-controller="'+panels[i]+'"></div></section>';
var cTemplate = compile(template)(scope);
element.append(cTemplate);
}
}
}
}]);
Ok I think the simplest solution here is to define the controller explicitly on the template of your file. Let's say u have an array:
$scope.widgets = [
{templateUrl: 'templates/widgets/aWidget.html'},
{templateUrl: 'templates/widgets/bWidget.html'},
];
Then on your html file:
<div ng-repeat="widget in widgets">
<div ng-include="widget.templateUrl"></div>
</div>
And the solution aWidget.html:
<div ng-controller="aWidgetCtrl">
aWidget
</div>
bWidget.html:
<div ng-controller="bWidgetCtrl">
bWidget
</div>
Simple as that! You just define the controller name in your template. Since you define the controllers as bmleite said:
app.controller('ControllerX', function() {
// your controller implementation
});
then this is the best workaround I could come up with. The only issue here is if u have like 50 controllers, u'll have to define them explicitly on each template, but I guess u had to do this anyway since you have an ng-repeat with controller set by hand.