Consider
angular.module('App').directive('errors',function() {
return {
restrict: 'A',
controller:function() {
var self = this;
self.closeErrors = function() {
self.errors = [];
self.hasErrors = false;
}
},
controllerAs: 'errorsCtrl',
templateUrl: 'errors.html'
}
when called with
<div errors="otherCtrl.errors"></div>
the object errors comes from another controller.
I know i can add
scope: {errors:"="},
and then access it in my controller via
$scope.errors;
but when I assign it to
self.errors = $scope.errors.
self.errors never gets updated when it is changed in the parent.
So my question is, how can I let this work that whenerver my parentcontroller changes the errors object it is also changed in the errorsCtrl.
(Also I do know I can access errors directly in my template without the controller, but I simply want to use my errorsCtrl)
Add bindToController: true to your directive.
http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
Angular 1.3 introduces a new property to the directive definition
object called bindToController, which does exactly what it says. When
set to true in a directive with isolated scope that uses controllerAs,
the component’s properties are bound to the controller rather than to
the scope.
That means, Angular makes sure that, when the controller is
instantiated, the initial values of the isolated scope bindings are
available on this, and future changes are also automatically
available.
Related
I think there's something I'm missing about two-way binding in my directives.
In the page's controller (ScheduleEditCtrl) there's an array of objects (scheduleList) (and some other properties).
This is used in the view thusly:
<undo-support watch-on="scheduleEdit.scheduleList"
can-undo="scheduleEdit.canUndo"
can-redo="scheduleEdit.canRedo"
is-watch-on-initialized="scheduleEdit.isScheduleInitialized"></undo-support>
The directive:
angular.module('xnuapp')
.directive('undoSupport', function () {
return {
restrict: 'E',
controller : 'UndoSupportCtrl',
controllerAs : 'undoSupport',
scope: {
watchOn: '=watchOn',
canUndo: '=canUndo',
canRedo: '=canRedo',
isWatchOnInitialized: '=isWatchOnInitialized'
}
};
});
In the directive's controller (UndoSupportCtrl) it may be that an item in the array (watchOn) gets updated.
$scope.watchOn = angular.copy(commands[--pointer]);
My understanding is that when that watchOn gets updated, it should reflect back to the scheduleEdit.scheduleList, but through various logging triggers, I've found that the scheduleList property of the ScheduleEditCtrl never changes, even when watchOn does. Where should to see what prevents this from happening?
In trying to get a grasp on creating my own AngularJS directives, I have an example that does everything I need, but realize that in borrowing from various examples, I now can create functionality for the directive's view in both the controller as well as the link.
It seems that I could get rid of the controller all together and just put everything into link, or is there something that I can do with the controller that I can't do with link?
http://jsfiddle.net/edwardtanguay/gxr49h96/6
.directive('itemMenu', function () {
var controller = function ($scope) {
var vm = this;
vm.addItem = function () {
$scope.add();
vm.items.push({
'kind': 'undefined',
'firstName': 'Joe',
'lastName': 'Newton',
'age': Math.floor(Math.random() * 60) + 20
});
};
// DOES THE SAME AS THE FUNCTION DEFINED BELOW IN LINK
// $scope.convertToInternal = function(item) {
// item.internalcode = 'X0000';
// item.kind = 'internal';
// };
};
return {
restrict: 'A',
scope: {
item: '=',
add: '&'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
template: '<div ng-include="getTemplateUrl()"></div>',
link: function (scope, element, attrs) {
scope.getTemplateUrl = function () {
switch (scope.item.kind) {
case 'external':
return 'itemMenuTemplateExternal';
case 'internal':
return 'itemMenuTemplateInternal';
default:
return 'itemMenuTemplateUndefined';
}
};
scope.convertToInternal = function(item) {
item.internalcode = 'X0000';
item.kind = 'internal';
};
},
};
})
You may find a lot of watery rants about controller vs link, most of them contain the information from $compile service documentation.
Answering the question directly,
controllers from other modules/files can be plugged into the directive via Angular DI with controller: 'Controller'
controller can be injected with dependencies while link has fixed arguments list of and gets by with directive's dependencies
controller kicks in before link, so it can prepare the scope for linking or recompile the element on some condition ASAP
controller function has this, its code appearance complies to other OOP-like ES5 code, and the methods can be easily transferred between other code parts, e.g. service service or third-party code
as the result, controllers are suited to be defined as ES2015 or TS classes.
directive's controller can be required by child directive and provides convenient one-way interaction between those two
controller makes use of bindToController: true + controllerAs: 'vm' and implements $scope.vm recipe (particularly useful to fight JS prototypal inheritance) while keeping this syntax
bindToController object value provides attribute bindings for inherited scope: true scope, no more $attr.$observe
bindToController object value provides further granularity for isolated scope. If there are certain attributes that should be bound to the controller and accessed with require, it can be done now
Which code goes to controller and which to link is more delicate question.
It is conventional to use Angular controllers as view models, as in MVVM (hence the controllerAs: 'vm' convention), so if there is a job for controllers (i.e. binding the services to scope values or setting up scope watchers) give it to them, leave the rest to link.
Since $attrs, $element and $transclude local dependencies should be injected into controller explicitly, the one may consider it a sign to skip them ($scope should be injected too for $scope.$ methods, but we will just ignore this fact).
There are some non-religious concerns about the job that should be done by link and not by controller. required controllers aren't available in controller itself, so this kind of directive interaction takes place in link. Since controller launches at earlier compilation phase than link, bound attribute values won't be interpolated yet, so the code that depends on these scope values goes to link. The same applies to other DOM-related code, it goes to link for a reason.
It is mostly the matter of proper code style and not real necessity. As of the code in the example, all scope stuff is controller-friendly, I don't think that link should be there at all.
I'm trying to figure out how to include scope with a directive that I add to the dom on a click event in a controller.
Step 1. On a click event, I call a function in my controller that adds a directive like this
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$(e.currentTarget).append($compile("<my-directive mydata='instanceOfAnObjectPassedInClickEvent'/>")($scope));
}
//I'm trying to take the `instanceOfAnObjectPassedInClickEvent` and make it available in the directive through `mydata`
The above, part of which I got from this SO answer, successfully adds the directive (and the directive has a template that gets added to the dom), however, inside the directive, I'm not able to access any of the scope data mydata it says it's undefined.
My directive
app.directive('myDirective', function(){
return {
restrict: 'AE',
scope: {
mydata: '='
//also doesn't work if I do mydata: '#'
},
template: '<div class="blah">yippee</div>',
link: function(scope,elem,attrs) {
console.log(scope) //inspecting scope shows that mydata is undefined
}
}
}
Update
I changed the name of datafromclickedscope in the OP to make it more clear. In the controller action addMyDirective (see above) instanceOfAnObjectPassedInClickEvent is an instance of an object passed into the controller method on a click event that I try to pass into the directive as mydata='instanceOfAnObjectPassedInClickEvent'. However, even if I change = to # in the directive and I try to access scope.mydata in the link function of the directive, it just shows a string like this "instanceOfAnObjectPassedInClickEvent", not the actual object data that is available to me in my method that handles the click event
When you use mydata='instanceOfAnObjectPassedInClickEvent' in a template you need instanceOfAnObjectPassedInClickEvent to defined in $scope. So before compiling you should assign a variable in $scope. I will rename this variable in code below, so that same names would not confuse you and it would be clear that a formal parameter of a function cannot be visible in a template.
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$scope.myEvent = instanceOfAnObjectPassedInClickEvent;
$(e.currentTarget).append($compile("<my-directive mydata='myEvent'/>")($scope));
}
EDIT: slightly adapted jsfiddle not using JQuery no manipulate DOM
The problem
Given the following directive
angular.module('sandbox.directive.controllers', [])
.directive('simpleDirective', function() {
return {
restrict: 'E',
replace: true,
scope: {user: '='},
controller: 'simpleController',
templateUrl: 'directive-controllers/templates/simpleDirective.html'
};
})
.controller('simpleController', function($scope, $element) {
$scope.title = "Hello from the controller!";
});
Why does this test fail?
it('should allow us update when we change the title', function() {
element.scope().title = "Marco Polo!";
element.scope().$digest();
expect(element.html()).toContain('Marco Polo!');
});
My full implementation can be seen here and the tests here.
My understanding
I am currently trying to get a better understanding of scope within directives. I have written a few tests which illustrate most of the use cases quite clearly. However I am having issues with how a directives controller relates to the isolated scope.
My understanding was that the scope accessible within the controller is a reference to the scopes directive.
Please help
I understand the issue with two-way bindings on primitives, however this is within the isolated scope and not the parent scope. Any help to get a better understanding of this would be appreciated.
There appears to be an element.isolateScope() property.
The two-way binding assigned for a user was passing as the parent scope was returned from element.scope(), therefore when user was changed the binding was in affect and the value was updated within the isolated scope.
The element.isolateScope() returns the scope when it is isolated(as it is in this case) and allows me to update this value correctly.
The element.scope() and element.isolateScope() functions are explained here.
In this example plunker, http://plnkr.co/edit/k2MGtyFnPwctChihf3M7, the nested directives compile fine when calculating the DOM layout, but error when the directive tries to reference a variable to bind to and says the variable is undefined. Why does this happen? The data model I am using is a single model for many nested directives so I want all nested directives to be able to edit the top level model.
I havn'et got a clue as to what you're trying to do. However, your comment 'so I want all nested directives to be able to edit the top level model' indicates you want your directive to have scope of your controller. Use
transclude = true
in your directive so that your directives can have access to your the parent scope.
http://docs.angularjs.org/guide/directive#creating-a-directive-that-wraps-other-elements
I don't know why you are doing it this way exactly, it seems like there should be a better way, but here goes a stab at getting your code working. First you create an isolated scope, so the scopes don't inherit or have access to anything but what is passed in the data attribute. Note that you can have your controller set dumbdata = ... and say <div data="dumbdata" and you will only have a data property on your isolated scope with the values from dumbdata from the parent in the data property. I usually try to use different names for the attribute and the data I'm passing to avoid confusion.
app.directive('project', function($compile) {
return {
template: '<div data="data"></div>',
replace: true,
scope: {
data: '=' // problem
},
Next, when you compile you are passing variables as scopes. You need to use real angular scopes. One way is to set scope: true on your directive definition, that will create a new child scope, but it will inherit from the parent.
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
scope: true, // new child scope inherits from parent
compile: function compile(tEle, tAttr) {
A better way is probably to create the new child scope yourself with scope.$new(), and then you can add new child properties to pass for the descendants, avoiding the problem of passing values as scopes and still letting you have access to the individual values you're looping over (plunk):
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
compile: function compile(tEle, tAttr) {
return function postLink(scope,ele,attrs) {
angular.forEach(scope.outer.middles, function(v, i) {
var x = angular.element('<div middle></div>');
var s = scope.$new(); // new child scope
s.middle = v; // value to be used by child directive
var y = $compile(x)(s); // compile using real angular scope
ele.append(y);
});
};
}
};
return r;
});