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+
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.
Since Angular v1.4, it's possible to do this:
scope: {},
bindToController: {
name: "="
}
instead of old way of doing:
scope: {
name: "="
},
bindToController: true
Except being more intuitive, is there any difference between them?
Think about bindToController as a migration path for future version of Angular.
We prefer to write directives (or components) with isolated scope and bind to controller the properties you want to pass.
Binding variables from scope will gradually disappear.
In the new release of angular (1.5) you don't need to use scope or bindToController, because the scope is isolated for default and for bind variables to controller you can use bindings.
This is also useful for preventing $scope use. Read this article if you want more info about that: https://toddmotto.com/no-scope-soup-bind-to-controller-angularjs/
I am passing a custom scope object to the $compile and creating a custom template. If I apply a directive on the elements inside the template, scope that is changing is the one that is passed to the $compile, and that's really what I wanted.
However, I just thought that it might be good to also have a controller on some elements inside the template,
<div ng-controller="controllerName" >
</div>
but ng-controller doesn't set data on the passed scope but creates its own and uses that one. Is there a way to make ngController to use existing scope and not create a new one ?
We create our controllers and wrap them in factories to make them accessible. We apply or controllers through directives (also going away). This gives you a controller that is scoped to the directive, which has better control for scope, this works for us as the directives where we do this for are usually components.
I don't know if this will be an option given the road you are down now. I would suggest trying to stop using ng-controller. You may want to look at angular 2 now just to keep it in mind as a migration path, it is coming in the fairly near future. They have removed ng-controller, a lot of what they are doing in angular 2 can be done now.
This is a good resource on why these things are a bad idea
https://www.youtube.com/watch?v=gNmWybAyBHI&t=9m10s
If you look at the source code for ng-controller, you will see it is very simple:
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
You can actually create an almost identical alternate directive that just defines scope: false (or omits the scope key altogether, same thing):
app.directive('controllerNoScope', function () {
return {
restrict: 'A',
scope: false,
controller: '#',
priority: 500 // same as ng-controller
}
});
(You may want to give it a better name).
See this Plunkr for a demo that shows the scope has the same $id as the outer one, meaning it is the same scope.
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 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.