Update: It must have been something stupid in another part of the code. It works now, so the bindToController syntax is fine.
We are using AngularJS 1.4, which introduced a new way to use bindToController in directives.
After quite a bit of reading (and maybe not understanding everything), we defined our directive like this:
.directive('mdAddress', function mdAddress() {
var directive = {
restrict: 'EA',
scope: {},
bindToController: {
address: '='
},
templateUrl: 'modules/address/address.html',
controller: AddressController,
controllerAs: 'dir'
};
Calling it from another view like this:
<md-address address="vm.address"></md-address>
Having previously defined in the view controller:
vm.address = {
street: null,
countryCode: null,
cityCode: null,
postalCode: null
};
Referencing the variables in the directive template like this:
<md-input-container>
<label>{{'ADDRESSNUMBER' | translate}}</label>
<input type="number" ng-model="dir.address.streetNumber">
</md-input-container>
We spent 4h trying to figure out why our directive was not working. Well, it was working, but the two-way binding between the controller and the directive was not, vm.address.street was hopelessly set to null.
After a while, we just tried the old way:
.directive('mdAddress', function mdAddress() {
var directive = {
restrict: 'EA',
scope: {
address: '='
},
bindToController: true,
templateUrl: 'modules/address/address.html',
controller: AddressController,
controllerAs: 'dir'
};
And it magically worked. Any idea WHY?
Update:
Thanks to the reference to this blog post, I need to update my answer. Since AngularJS 1.4 it really seems, that you can use
scope: {},
bindToController: {
variable: '='
}
which will do the (exact) same thing as the old syntax:
scope: {
variable: '='
},
bindToController: true
The useful lines from the AngularJS source code to explain this behavior:
if (isObject(directive.scope)) {
if (directive.bindToController === true) {
bindings.bindToController = parseIsolateBindings(directive.scope,
directiveName, true);
bindings.isolateScope = {};
} else {
bindings.isolateScope = parseIsolateBindings(directive.scope,
directiveName, false);
}
}
if (isObject(directive.bindToController)) {
bindings.bindToController =
parseIsolateBindings(directive.bindToController, directiveName, true);
}
Source: AngularJS 1.4.0
Original answer:
Hopefully, I can explain you why this behavior you experienced is correct and where you did missunderstand the concept of scope binding there.
Let me explain, what you did in your first code snippet:
.directive('mdAddress', function mdAddress() {
var directive = {
restrict: 'EA',
scope: {},
bindToController: {
address: '='
},
templateUrl: 'modules/address/address.html',
controller: AddressController,
controllerAs: 'dir'
};
With scope: {}, you created an isolated scope (without any inheritance) for your mdAddress directive. That means: No data is passed between the parent controller and your directive.
Having this in mind, regarding your second code snippet:
<md-address address="vm.address"></md-address>
vm.address from your parent controller/view will be assigned as expression to the address attribute of the directive, but as you defined an isolated scope before, the data is not passed into AddressController and therefore not available in the bindToController value.
Let's think of the scope object definition as the "which data will be passed in" and the bindToController as the "which data will be available in my view's controllerAs object".
So, now let's have a look at the last (and working code snippet):
.directive('mdAddress', function mdAddress() {
var directive = {
restrict: 'EA',
scope: {
address: '='
},
bindToController: true,
templateUrl: 'modules/address/address.html',
controller: AddressController,
controllerAs: 'dir'
};
There you created an isolated scope, too, but this time you added the address attribute to be passed in as an expression. So now the address you passed in from the view in the second snippet will be available in the controller's scope. Setting bindToController: true now, will bind all the current scope's properties to the controller (or more likely the controllerAs object). And now, it works as you would expect, because data will be passed in to the scope and data will be passed out to the controller's template scope.
Did that brief overview help you to better understand the concept of the scope and bindToController definition objects?
Related
My AngularJS typeahead FaveDirective needs to bind a single value to the parent scope, and call an update function when that value changes:
Parent html:
<div class="parent-controller-scope ng-scope">
<my-fave-picker favorite="parent.favorite" on-change="parent.update()">
</div>
Fave picker directive template:
<input
type="text"
ng-model="vm.favorite"
typeahead-on-select="vm.onChange()"
ng-init="vm.loadTypeaheadValues()"
placeholder="Pick your favorite Swift"
uib-typeahead="name for name in ::vm.TypeaheadValues | filter:$viewValue"
class="form-control">
Fave picker directive code:
(function (angular, _) {
'use strict';
angular
.module('favorite')
.directive('MyFavePicker', function() {
return {
restrict: 'E',
templateUrl: 'fave-picker-template.html',
scope: {
favorite: '=',
onChange: '&'
},
controllerAs: 'vm',
bindToController: true,
controller: 'FavePickerController'
};
})
.controller('FavePickerController', function() {
// etc.
});
}(angular, _));
This works almost correctly; when the typeahead input is committed, it calls update() on the parent scope as intended. The problem is that this happens before the latest value of favorite is propagated to the parent scope. In other words, if the typeahead has possible values ["Taylor Swift", "Jonathan Swift"] and I type "Tay" and then hit enter to select the value from the dropdown, then at the time the typeahead-on-select callback is executed, I have the following values:
vm.favorite = "Taylor Swift"
parent.favorite = "Tay"
The parent.update() function therefore operates with the wrong value of parent.favorite ("Tay" instead of "Taylor Swift").
I can think of some bad ways, but what's the right way to do this so that the change to vm.favorite gets propagated back to the parent scope before calling parent.favorite()?
Note that the following things are not possible in my circumstances:
inheriting parent scope instead of using isolate scope
passing favorite as an argument to update (on-change="parent.update(favorite)")
setting a timeout in dir.onChange() before calling parent.update()
Avoid using two-way, '=', binding in components to propagate values. Instead use one-way, '<', binding for inputs and expression, '&', binding for outputs:
<my-fave-picker favorite="parent.favorite"
on-change="parent.favorite=$value; parent.update($value)">
</my-fave-picker>
app.directive('MyFavePicker', function() {
return {
restrict: 'E',
templateUrl: 'fave-picker-template.html',
scope: {
̶f̶a̶v̶o̶r̶i̶t̶e̶:̶ ̶'̶=̶'̶,̶
favorite: '<',
onChange: '&'
},
controllerAs: 'vm',
bindToController: true,
controller: 'FavePickerController'
};
})
In the component template:
<input
type="text"
ng-model="vm.favorite"
ng-change="vm.onChange({$value: vm.favorite})"
typeahead-on-select="vm.onChange({$value: vm.favorite})"
ng-init="vm.loadTypeaheadValues()"
placeholder="Pick your favorite Swift"
uib-typeahead="name for name in ::vm.TypeaheadValues | filter:$viewValue"
class="form-control"
/>
By applying the value in the expression, '&', binding, the value is propagated immediately. With two-way, '=', binding, the value is propagated after a digest cycle.
For more information, see
AngularJS Comprehensive Directive API Reference - scope
AngularJS Developer Guide - Component Based Application Architecture
I have two directives, Isolated and Shared, the Isolated directive pass the two-way binding directly to the Shared directive but the Shared directive is not using the Isolated scope, is creating its own.
The objective is that the Isolated directive should respond to changes in the two-way bindings when the Shared directive changes them.
<body ng-app="app">
<div ng-controller="main as $ctrl">
<h3>Main data: {{$ctrl.data.bind}}</h3>
<isolated bind="$ctrl.data.bind"></isolated>
</div>
</body>
angular.module("app", [])
.controller("main", function() {
this.data = {
bind: 123
}
})
.directive("isolated", function() {
return {
scope: {
bind: '='
},
bindToController: true,
template: '<div><h3>Parent directive data: {{$ctrl.bind}}</h3> </div>'
+ '<input type="text" shared ng-model="$ctrl.bind" />',
controller: function() {
this.changed = function() {
console.log('Data changed: ' + this.bind);
}
},
controllerAs: '$ctrl',
link: {
pre: function($scope) {
console.log("Parent data: " + $scope.$ctrl.bind);
}
}
}
})
.directive("shared", function() {
return {
restrict: 'A',
require: {
ngModel: '^'
},
bindToController: true,
link: function($scope) {
console.log('Current data in shared: ' + $scope.$ctrl.bind)
},
controller: function() {
this.$postLink = function() {
this.ngModel.$modelValue = 321;
}
},
controllerAs: '$ctrl'
}
});
Here I have a Plunker
Gourav Garg is correct. Due to the shared scope, the second directive declaration is overriding the $scope.$ctrl field. The controllerAs property in the second declaration is unneeded anyways, as you never access the controllers properties within the template. If you do end up needing the second directives controller information within your template, you need to declare it's name as something other than $ctrl, or, better yet, use require syntax to require the second directive on the first directive. That will bind the second directive's controller to a property on the first directive's controller.
For more information on require, see the "Creating Directives That Communicate" section of the angular directive guide here.
Best of luck!
I have piece of html I want to show as a component, as I'm not manipulating the DOM.
As a directive it works fine, but as a component it doesn't. I have made components before with no problem, just can't see what the issue is here.
If I comment in the component code, and the directive out, it doesn't work.
Any idea what I've done wrong?
(function() {
"use strict";
angular
.module('x.y.z')
// .component('triangularStatus', {
// bindings: {
// value: '=',
// dimension: '=?'
// },
// templateUrl: '/path/to/triangular-status.html',
// controller: TriangularStatusController,
// controllerAs: 'vm'
// });
.directive('triangularStatus', triangularStatus);
function triangularStatus() {
var directive = {
scope: {
value: '=',
dimension: '=?'
},
replace: true,
templateUrl: '/path/to/triangular-status.html',
controller: TriangularStatusController,
controllerAs: 'vm',
};
return directive;
}
TriangularStatusController.$inject = [];
function TriangularStatusController() {
var vm = this;
}
})();
Here is the working code, most probably you are not using vm.values to access data.
Just be sure you are using right version of angular js ~1.5
(function(angular) {
angular.module('x.y.z', [])
.component('triangularStatus', {
bindings: {
value: '=',
dimensions:'=?'
},
template: '{{vm.value}} <br/> {{vm.dimensions}}' ,
controller: TriangularStatusController,
controllerAs: 'vm'
});
TriangularStatusController.$inject = [];
function TriangularStatusController() {
}
})(window.angular);
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.js"></script>
<div ng-app = "x.y.z">
<triangular-status value="24" dimensions="348"></triangular-status>
</div>
The definition of your component, using bindings, is not directly equivalent to the definition of your directive, using scope, even though both are defined to use controllerAs. This is because your component will be binding directly to the controller, and your directive will be binding to $scope (by default).
I've used your code in the snippet below, slightly modified to allow the component and directive(s) to be used together. I've also added an additional directive that makes use of bindToController:true to demonstrate a directive that behaves a little more like a component in binding its attribute values directly to the controller, rather than to $scope.
I've also used a very basic shared template that attempts to show the bound attribute values by looking for them on $scope, followed by looking for them on vm (the ControllerAs).
(function() {
"use strict";
var templateBody = '<h2>$scope</h2>' +
'<p>value: {{value}}</p><p>dimension: {{dimension}}</p>' +
'<h2>vm</h2>' +
'<p>vm.value: {{vm.value}}</p><p>vm.dimension: {{vm.dimension}}</p>';
angular
.module('x.y.z', [])
.component('triangularStatusComponent', {
bindings: {
value: '=',
dimension: '=?'
},
template: '<div><h1>Triangular Status Component</h1>' + templateBody + '</div>',
controller: TriangularStatusController,
controllerAs: 'vm'
})
.directive('triangularStatusDirective', triangularStatusDirective)
.directive('triangularStatusDirectiveBound', triangularStatusDirectiveBound);
function triangularStatusDirective() {
var directive = {
scope: {
value: '=',
dimension: '=?'
},
replace: true,
template: '<div><h1>Triangular Status Directive</h1>' + templateBody + '</div>',
controller: TriangularStatusController,
controllerAs: 'vm',
};
return directive;
}
function triangularStatusDirectiveBound() {
//https://docs.angularjs.org/api/ng/service/$compile#-bindtocontroller-
var directive = {
scope: {
value: '=',
dimension: '=?'
},
bindToController: true,
replace: true,
template: '<div><h1>Triangular Status Directive Bound</h1>' + templateBody + '</div>',
controller: TriangularStatusController,
controllerAs: 'vm',
};
return directive;
}
TriangularStatusController.$inject = [];
function TriangularStatusController() {
var vm = this;
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="x.y.z">
<triangular-status-component value="'componentValue'" dimension="'componentDimension'">
</triangular-status-component>
<hr>
<triangular-status-directive value="'directiveValue'" dimension="'directiveDimension'">
</triangular-status-directive>
<hr>
<triangular-status-directive-bound value="'directiveValueBound'" dimension="'directiveDimensionBound'">
</triangular-status-directive-bound>
</div>
If you're finding that your code works as a directive, where your values are bound to $scope, but not as a component, where your values are bound to the controller, I would assume either your template html (most likely?) or your controller function are relying on trying to access your values as though they were on $scope. To confirm this, you may notice there are errors being logged to your javascript console that will help you zero in.
I think the only problem is, that your missing the brackets:
angular.module('x.y.z')
change to
angular.module('x.y.z', [])
https://docs.angularjs.org/api/ng/function/angular.module
As was mentioned in the comment, I need to clarify, the problem can be how are your JS files ordered or bundled, some other JS file executed later can overwrite this module and therefor you will not see any tag rendered.
I m actually creating a little directive and I m facing a problem with the scope object and controllAs.
In fact, I have this result :
angular.module('app')
.directive('historyConnection', function () {
return {
templateUrl: 'views/directives/historyconnection.html',
restrict: 'E',
scope: {
idUser: '#iduser'
},
controller:function($scope){
console.log(this.idUser); // gives undefined
console.log($scope.idUser); // gives the good value
},
controllerAs:'history'
};
});
From the html code :
<history-connection iduser="55"></history-connection>
I dont know how to make controllerAs work when passing parameters to directive. Can you help me ?
Important informations are commented in the javascript code above
If you want the scope properties to be bound to the controller you have to add bindToController: true to the directive definition.
I have a directive i'm using to do the same search filtering across multiple pages. So the directive will be using a service and get pretty hefty with code. Because of that I want to link to a controller instead of have the controller inside the directive like this:
.directive('searchDirective', function($rootScope) {
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search'
};
});
I also want access to parent scope data inside the template, so I don't want to use a isolated scope.
Anyway here's what i'm not sure how to do. My directive looks like this:
<search-directive filter="foo"/>
How do I pass in the value in the filter attribute so that I can access it in my controller using $scope.filter or this.filter?
If I were using an isolated scope it'd be simple. If i had the controller in the same page I could use $attrs. But since i'm using a controller from another spot and don't want an isolated scope i'm not sure how to get the attrs values into the controller.
Any suggestions?
What about using the link function and passing the value to the scope?
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search',
link: function (scope, element, attr) {
scope.filter = attr.filter;
}
};
searchDirective.js
angular
.module('searchDirective', []).controller('SearchCtrl', SearchCtrl)
.directive('SearchDirective', directive);
function directive () {
var directive = {
templateUrl:'searchtemplate.html',
restrict: "E",
replace: true,
bindToController: true,
controller: 'searchCtrl as search',
link: link,
scope: { filter:'=' } // <-- like so here
};
return directive;
function link(scope, element, attrs) {}
}
SearchCtrl.$inject = [
'$scope',
'$filter'];
function SearchCtrl(
$scope,
$filter) {
/** Init SearchCtrl scope */
/** ----------------------------------------------------------------- */
var vs = $scope;
// ....
Also I highly recommend checking out this AngularJS style guide, how you are writing your directive above is how I use to do it too. John Papa shows some way better ways: https://github.com/johnpapa/angular-styleguide
Directives:
https://github.com/johnpapa/angular-styleguide#directives
Controllers:
https://github.com/johnpapa/angular-styleguide#controllers
Flip the values of bindToController and scope around.
{
....
scope: true,
bindToController: { filter:'=' }
...
}
I have just hit the same issue over the weekend, and made a simple complete example here: bindToController Not Working? Here’s the right way to use it! (Angular 1.4+)