This has caught me with a surprise. AngularJS directive is not setting a local scope if it's being used with isolated scope.
I've a amount field which is being validated. The validation message is within a directive which watches for the amount, if greater than 5 it should display an error message. However the error message is not displayed, but the template responses to ng-if which has property declared on local scope.
Below is the code and PLUNKR
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.vm = {};
});
app.directive('errorMessage', function() {
return {
restrict: "E",
replace: true,
templateUrl: 'my-template.tpl.html',
scope: {
amount: '='
},
link: function(scope, element, attrs) {
scope.isAmountError = true;
scope.$watch('amount', function() {
if (scope.amount > 5) {
scope.isAmountError = true;
scope.errorText = 'Amount lesser than 5';
} else {
scope.isAmountError = false;
}
});
}
};
});
Your problem is ng-if. From the API page at https://docs.angularjs.org/api/ng/directive/ngIf
Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored.
So each time ng-if is evaluating to true, a new scope is created which does not reference your errorText. On the other hand, if you use ng-show, it hides/shows but keeps the scope, and all works fine.
<div class="row" ng-show="isAmountError">
Here is the updated plunkr http://plnkr.co/edit/BsSueLNpWA8XveyxblmE?p=preview
Related
I have the following (https://jsfiddle.net/f30bj43t/5/) HTML:
<div ng-controller="DataController as vm">
<div ng-repeat="name in vm.users track by $index">{{name}}</div>
<form name="form" validation="vm.errors">
<input validator ng-model="vm.name" name="vm.name" placeholder="name" type="text" />
Add
</form>
</div>
This form adds names to a list and the controller is:
app.controller("DataController", DataController);
function DataController() {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = [];
vm.add = function(name) {
if (name == "Mary") {
var error = { property: "name", message: "name cannot be Mary"};
if (vm.errors.length == 0)
vm.errors.push(error);
} else {
vm.users.push(name);
vm.errors = [];
}
}
}
On the form I added validation="vm.errors" that defines which variable holds the errors to be used in each validator directive ...
Then in each validator directive I will use that variable to pick the correct error and display it ...
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.getErrors = function () {
return $scope.validation;
}
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
var errors = controller.getErrors();
console.log(errors);
// do something with errors
}
}
PROBLEM
In link function of validator directive I need to track changes of the variable passed by validation="vm.errors" so I can in each validator check if a error occurs and take action.
But console.log(errors) seems to have no effect ...
How can I solve this?
You can do it with either $watch or $broadcast and $on.
I preffer to use the $on, because watching a variable consumes more than listening to an event and I'd say that to your case it's better to use $broadcast and $on, because you're not watching a scope variable, but a variable from a controller.
If you want to learn more about $on and $watch, here's a suggestion: Angular JS $watch vs $on
And here's your JSFiddle with the modifications.
Here you have a working jsfiddle: https://jsfiddle.net/f30bj43t/8/
I am logging the vm.errors variable into the console each time it changes:
function link(scope, element, attributes, controller) {
scope.$watch('vm.errors.length', function () {
if(scope.vm.errors){
console.log(scope.vm.errors);
}
});
}
This is possible due to the scope inheritance. On your case, vm is a variable added to the controller's scope, and all directives inside this scope will inherit it by default (there are ways to avoid that).
Anyhow it seems you are creating too much directives. On your case, for me would be enough having everything in the controller.
Cheers
Javier
MyController has clickedvalue as false in its scope.
the click changes it to true
the dir directive watches for change on the clicked attribute.
How is the $watch seeing change - or how is the clickvalue changing without being interpolation markup {{clickvalue}}
right now click="clickvalue" triggers the $watch which would mean it is 'bound' to the MyContoller's clickvalue
<div ng-controller='MyController'>
<button ng-click='click()'>Call function inside directive</button>
<dir clicked="clickedvalue"></dir>
</div>
app.js
app.controller('MyController', ['$scope', function($scope) {
$scope.clickedvalue = false;
$scope.click = function() {
$scope.clickedvalue = !$scope.clickedvalue;
};
}
]);
appDirectives = angular.module('app.directives', []);
appDirectives.directive('dir', [function() {
return {
restrict: 'E',
scope: {
clicked: '='
},
link:function(scope, element, attrs ) {
scope.$watch('clicked', function() {
console.log('innerFunc called');
})
}
};
}]);
If I understood you well, the '=' does not require you to use {{interpolation}}, '#' would.
Moreover, take note that you set up the $watch on attribute, not on the controller's clickedvalue.
In angular js {{}} means one way binding & when you are using directive and declaring isolated scope with using = that means two way binding of the scope variables. If one of variable changes then the relative scope updates.
scope: {
clicked: '='
},
In your case clicked has been mapped with clickedvalue.As value of clicked gets update, it will also update the value of clickedvalue which is isolated scope directive
I have a custom directive which is sometimes hidden using ng-hide:
<my-custom-directive ng-show="vm.showBox"
value="vm.objects"
></my-custom-directive>
A snippet from my custom directive code:
function myCustomDirective() {
var directive = {
controller: controller,
controllerAs: 'vm',
///...
scope: {
value: '='
}
};
return directive;
function controller($scope) {
var vm = this;
///...
$scope.value.dates = $scope.value.dates || [];
}
}
The problem: even when the directive isn't supposed to be loaded/displayed (because vm.showBox is false), the custom directive's controller code runs and in such case it fails because $scope.value isn't passed (it's undefined there).
Why does the directive's controller code runs anyway, if the directive is hidden? I want to assume that if the directive is used, it's given valid parameters without having to check whether $scope.value is defined.
ng-show controls visibility of element (change of css property display) but element still exists in DOM. to remove/create element you can use ng-if instead of ng-show
ng-show only controls display property of element. It does not prevent element to render in DOM. To prevent it you can use ng-if.
But the problem with ng-if is that according to condition every it destroy scope and creates new one.
To prevent this problem you should use ng-show.
In your case you can use watch expressin in your directive.
<my-custom-directive ng-show="vm.showBox"
value="vm.objects"
></my-custom-directive>
function myCustomDirective() {
var directive = {
controller: controller,
controllerAs: 'vm',
///...
scope: {
value: '='
}
};
return directive;
function controller($scope) {
var vm = this;
///...
var watchExpression = $scope.$watch('value', function(newValue, oldValue){
if(newValue)
{
$scope.value.dates = $scope.value.dates || [];
watchExpression(); //de-register watch expression
}
});
}
}
I have a directive which I render only if my authentication service tells me so:
<div my-loggedin-directive ng-if="security.isAuthenticated()"></div>
The directive itself is quite empty :
.directive('myLoggedinDirective', [
function() {
return {
templateUrl: 'modules/myLoggedinDirective.tpl.html',
restrict: 'A',
replace: true,
link: function($scope) {
$scope.$on('$destroy', function() {
console.log('$destroy');
});
}
};
}
]);
Since my directive should be always rendered when I'm logged in, the ngIf logic should be inside the directive declaration, and not in a ng-if (the directive without the ngIf would be totally broken).
How can I change the directive code (injecting the test on the security service inside the directive declaration) knowing that I want the same behavior as the ngIf ? :
directive present in the DOM only when nfIf resolves to true
automatic calling of $destroy when nfIf resolve to false) ?
I tried to use compile function and a watcher to watch security.isAuthenticated() without success
Declare extra directive attribute that will pass security object into your directive:
<div my-loggedin-directive my-security='security'></div>
And directive should include scope, make sure it uses two-way binding:
.directive('myLoggedinDirective', [
function() {
return {
scope: {
security: "=mySecurity"
}
templateUrl: 'modules/myLoggedinDirective.tpl.html',
restrict: 'A',
replace: true,
link: function($scope) {
$scope.$on('$destroy', function() {
console.log('$destroy');
});
$scope.$watch('security', function(newVal, oldVal) {
if (newVal.isAuthenticated()) {
// do something
} else {
// do something
}
});
}
};
}
]);
And now you can access $scope.security variable in your temple myLoggedinDirective.tpl.html
<div ng-if="security.isAuthenticated()">...</div>
I'm struggling with this for hours now.
var testApp = angular.module('testApp', []);
testApp.directive('test', function() {
return {
restrict: 'E',
transclude: true,
template: '<div ng-transclude>Hello World</div>',
link: function(scope) {
}
}
});
testApp.controller('testCtrl', function ($scope) {
$scope.user = "";
});
Here's JSFiddle: http://jsfiddle.net/2bKPj/
Now, all I need is for an input embedded in directive to be able to reflect user model directly in testCtrl controller.
I'm confused on how this beast works since I taught that scopes are shared in this case, no?
ngTransclude creates a new child scope which protorypically inherits from it's parent scope.
When you use a primitive on the child scope it shadows the parent scope's variable.
It's been already said thousand times: use the dot notation!
controller:
testApp.controller('testCtrl', function ($scope) {
$scope.data = { user : "Hello World" };
});
html:
<input type="text" ng-model="data.user"/><br />
Directive model:<span>{{ data.user }}</span>
Check my other answers for description:
bound element inside ngIf does not update binding
Directives inside ng-include