Directive bindToController can be either boolean or object, the latter is shown here:
myMod.directive('myDirective', {
controller: 'MyDirectiveController',
bindToController: {
name: '#'
}
});
But the fact that it was not documented raises questions. Why bindToController: { ... } feature was made in the first place? Are there useful scenarios for it?
Despite bindToController wasn't primarily intended for that, it is interesting to see how it is utilized now in angular.component as bindings property to fill the gap between 1.5 and 2.0, while scope bindings remain unused.
bindToController was originally just a boolean at inception, but was migrated to allow it to be an object to be more explicit about what items/values you are binding to the controller. With it being a boolean it caused some confusion where this syntax removes that confusion about what you are adding to your controller.
The idea with why this was added was to propagate the usage of the controllerAs syntax to move away from $scope especially with the move towards angular2.
The basis for why this was added was to allow the directive injections/property bindings would now be based upon the controller instance instead of of the scope parameter.
Just stumbled across this PR, it is quite explanatory.
I'm not sure if there is practical benefit in having two different bindings in scope: { ... } and bindToController: { ... }. But it finally brings the bindings to prototypically inherited scope as well:
bindToController: {
text: '#text',
obj: '=obj',
expr: '&expr'
},
scope: true
Related
When building angular directives, I've found there is more than one way to pass a value from the parent scope to the child scope. Some ways I'm aware of are:
Don't use an isolate scope at all, and the child will simply have
access to the parent scope (you can mount a pretty good argument that this is bad).
Use the attributes parameter of a link function.
Use an isolate scope and bind to the attribute (e.g. param: '=')
The codepen here: https://codepen.io/ariscol/pen/WEKzMe shows two similar directives, one done with link and one done with 2-way binding in an isolate scope. Furthermore, it shows how they differ as far as 1-time binding compared to 2-way binding. For reference, here are the two directives:
app.directive("contactWidgetWithScope", function() {
return {
restrict: 'E',
template: '<div ng-bind="contact.name"></div>'
+ '<div ng-bind="contact.title"></div>'
+ '<div ng-bind="contact.phone"></div>',
scope: {
contact: '='
}
};
});
app.directive("contactWidgetWithLink", function() {
return {
restrict: 'E',
template: '<div ng-bind="name"></div>'
+ '<div ng-bind="title"></div>'
+ '<div ng-bind="phone"></div>',
scope: {},
link: function(scope, elem, attrs) {
scope.name = attrs.contactname;
scope.title = attrs.contacttitle;
scope.phone = attrs.contactphone;
}
};
});
Now, if I were trying to decide which way was "better", I might consider how I was going to use this directive. If I was going to have a thousand contacts, and I wanted to use this directive to list all one thousand contacts on a page, in an ng-repeat, for example, I imagine that I would have significantly better performance with link, as it won't add any watchers. On the other hand, if I wanted this directive to be incorporated into a page header, and I wanted the contact details to be updated as you clicked on any given contact in a list, I would want 2-way binding, so that any change to some "selectedContact" property in a parent scope would be automatically reflected in this directive. Are those the proper considerations? Are there others?
To add to my confusion, it is simple to add an observer to a linked attribute and achieve a 1-way binding such that a change in the value of the attribute will be reflected in the child. Would doing this have more or less of a performance impact? Conversely, I imagine you could do a 1-time binding on the value of the scope version and thereby eliminate the performance impact, e.g.: <contact-widget-with-scope contact="::vm.contact">. That should work, right? Seems like that option gives you a lot of flexibility, because it means the person who invokes the directive can decide if they want to pay the performance price to get the benefit of 2-way binding or not. Are these considerations accurate? Are there other things I ought to consider when deciding how to make values available to my directives?
I am new to AngularJS and directives and am seeking some advice or guidance on a directive I have implemented. The directive in question will be used to display pdf's to the user. The directive exposes two attributes, documentPath and documentType that are defined using isolated scope as follows:
var directiveDefinition = {
templateUrl: 'app/views/directives/document.html',
scope: {
documentPath: '#documentPath',
documentType: '#documentType'
},
restrict: 'E',
templateNamespace: 'html',
link: linkFunc
};
In the view that uses the directive, I bind the properties using a model property for the view controller and a string.
<my-document document-path="{{ application.documentpath }}" document-type="Application"></my-document>
When I initially ran this, I found that the directive would sometimes run before the data had been returned by the model. So an empty document would be displayed. Other times, the model would load before the directive ran, so the document path would be present when the link function ran, allowing the document to be displayed.
I determined that one way to resolve this was to use a $watch listener on the documentPath attribute of the directive. This seems to resolve the issue.
Being new to AngularJS and also directive implementation my question is...was this the best solution? Any advice would be greatly appreciated. Thanks!
The $watch solution should work fine but depending on the complexity of your app and the number of total watchers this could lead to a slow $digest cycle.
Another option would be to send the parameters as a reference using the "." dot notation so you it will get updated when the data gets loaded from model (from the API most of the time since this is the slow part) instead of sending a string primitive.
Your directive scope declaration would become:
scope: {
documentPath: '=',
documentType: '='
},
and you will use it from the parent view like this:
<my-document document-path="application.documentpath" document-type="application.documenttype"></my-document>
If the documentType is always the same you may leave it as a string scope parameter.
I was playing around with the bindToController option for directives. I stumbled upon a seemingly strange difference between the behaviour using a child scope compared to an isolated scope. When I use an isolated scope, an new scope is created for the directive, but changes to the bound controller attributes are forwarded to the parent scope. Yet when I use a child scope instead, my example breaks. (Using bindToController using child scopes should be allowed according to http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html#improvements-in-14 )
The code:
{
restrict: 'E',
scope: {},
controller: 'FooDirCtrl',
controllerAs: 'vm',
bindToController: {
name: '='
},
template: '<div><input ng-model="vm.name"></div>'
};
Working demo https://jsfiddle.net/tthtznn2/
The version using a child scope:
{
restrict: 'E',
scope: true,
controller: 'FooDirCtrl',
controllerAs: 'vm',
bindToController: {
name: '='
},
template: '<div><input ng-model="vm.name"></div>'
};
Demo: http://jsfiddle.net/ydLd1e00/
The changes to name are forwarded to the child scope, but not to the parent scope. This in contrast to binding to an isolated scope. Why is this?
This is because you are using the same alias for both controllers (and has nothing to do with object vs string value as mentioned in the comments).
As you might know, the controller as alias syntax is merely creating an alias property on scope and sets it to the controller instance. Since you are using vm as the alias in both cases (MainCtrl and FooDirCtrl) you are "shadowing" the MainCtrl alias in the case of the normal child scope.
(In this context, "normal" means "prototypally inheriting from parent scope".)
Thus, when you are trying to evaluate vm.name (vm for MainCtrl) on the new scope to get the "parent value", it is actually evaluating FooDirCtrl.name (which is undefined) and the same happens when you are trying to assign back to the parent scope.
The isolate scope version is not affected, since the scope is not inheriting from it's parent scope.
Updated fiddle
UPDATE:
Taking a closer look at the source code, this might be a bug (because support for bindToController on non-isolate scopes was added "retroactively").
We seem to have been getting away with this bug, because of the prototypal inheritance, but when the names collide we are out of luck.
I have to take a closer look to make sure if it's indeed a bug and if it's "fixable", but for now you can work around it by using different aliases for your controllers (see fiddle above).
That's (part of) why I don't like using vm as my conroller alias (despite it being suggested by popular style-guides) :)
Let's track this in angular.js#13021.
In my directive I created an isolated scope binding an expression to an attribute:
scope: {
foo: '#'
}
And in the directive if I try to console log scope.foo, I will get the following function:
function (locals) {
return parentGet(scope, locals);
}
And then executing scope.foo() will invoke the expression in the parent's scope. The problem is, the expression I am passing in is an optional callback, but I have no safe way of telling if the attribute is defined from the parent. I will get the function no matter what expression the attribute holds, even if it wasn't specified.
As a result, testing for the existence of scope.foo obviously doesn't help, and I can't test for scope.foo() because that will evaluate the expression if there is one. Is there a good way to make the expression binding optional? I wish we have something similar to '=?' when binding expressions, but there doesn't seem to be a '&?'.
This is where $parse comes in handy. It delegates the tasks of determining which scope and if it's an expression or a constant. Combining this with a quick check to see if the attribute is even there you could do something like so:
.directive('myDirective', function($parse){
return {
link: function($scope, elem, attrs){
if (attrs.foo){
var fooGetter = $parse(attrs.foo)
var callback = fooGetter($parse)
if (callback) callback.call()
}
}
}
})
Here are the docs on that - $parse docs
But my beef with this is that if you are doing any kind of controller-as implementation combined with a callback, you ofttimes get javascript scope issues when using this . I'd much prefer using the optional scope callback which is exactly why this exists, for calling methods on another scope:
scope: { foo: '&?'}
Not sure what version of Angular you're using but that is an option as I too had sought a similar solution - Optional two-way binding on isolate scope for Angular Directive
I'm trying to implement a double select list form component just to try out angular and try to understand it better.
The functionality of this directive is that the parent model contains the list of Available Items to choose from, and contains the destination list (selected) where chosen items are placed. When an item is chosen it will be removed from the available array and placed in the selected array.
As a form may require more than one of these double select list components I don't want to rely on hard coded property names but instead allow the lists to be passed in for two way data binding.
Here is a JSBin of the code: Double Select Lists
I haven't gone any further than just trying to bind the data to the lists via the passed in model property names.
On the directive, the following scope properties are defined. The availableItems and selectedItems should both be bound to the parent controllers properties. The highlightedAvailable and highlightedSelected are to store the user selections prior to the button clicks.
scope: {
availableItems: '=',
selectedItems: '=',
highlightedAvailable: [],
highlightedSelected: []
},
Can someone please tell me why the controller properties never bind to the directive properties?
Thanks!
First, you have an error being caused by your scope:
scope: {
availableItems: '=',
selectedItems: '=',
highlightedAvailable: [],
highlightedSelected: []
},
should be:
scope: {
availableItems: '=',
selectedItems: '='
},
Declare the arrays somewhere else, like in the link function:
link: function (scope, element, attrs) {
scope.highlightedAvailable = [];
scope.highlightedSelected = [];
The next problem was the way you specified the attributes to the directive, you had:
<div ng-Select-Lists availableItems='availableItems' selectedItems='selectedItems'>
Try this instead:
<div ng-Select-Lists available-items='availableItems' selected-items='selectedItems'>
To expand on aet's answer, the reason that the way you specified your directive attributes in your html did not work is because HTML is case-insensitive. So the 'availableItems' attribute was actually being passed to your directive scope as 'availableitems'. On the other hand, snake cased words like 'available-items' will be converted to camel case in your angular code.
That's the reason you write angular directives in the html as 'ng-repeat', 'ng-model', and so on, but in the angular source code you'll see these directive names camel cased: 'ngRepeat', 'ngModel'...
Be super careful to use snake-case in HTML, and camel case in your Javascript (Angular)! I've spent way too long on some bugs caused by that confusion.