I'm attempting to validate inputs inside a directive. Consider the following:
.directive('test', function ($parse, $http, $sce, $timeout) {
return {
restrict: 'EA',
scope: {
},
template: '<div class="holder">
<input id="A" name="inputA" ng-model="modelA" />
<input id="B" name="inputB" ng-model="modelB" />
</div>',
link: function($scope, elem, attrs) {
}
}
If I wanted to do my own custom validation inside the link function, how would I reference the "modelA" model? $scope.modelA appears to only reference the value inside the input, not the model itself (which is what I need for validation, as I understand it). All the other references I could find seem to deal with binding referencing models from the parent scope, which I don't need.
What am I missing?
I see you are using an isolated scope, I can't tell if that's part of your problem, but with an isolated scope and no properties, the parent scope outside the directive will have no way to access the values inside and vice-versa. I created a plunk to explain isolated scopes: (plnkr). If you want to access values on your parent scope you probably want something like this for your directive scope:
scope: {
modelA: "=modelA",
modelB: "=modelB"
},
And then calling your directive would work like this:
<test model-a="firstName", model-b="lastName"/>
Another issue might be that the link function only executes once for each time you use your directive as it is processing it and linking it. For instance if the value of modelA changes then your link function will not be called again. If you want to do something when modelA changes you need to set up a watcher for the change in your link function. Note that it will be be called once initially without changing the value:
link: function($scope, elem, attrs) {
$scope.$watch('modelA', function(newValue, oldValue) {
console.log('modelA changed from', oldValue, 'to', 'newValue');
});
}
Related
I have a directive whose 'config' attribute value I need to access inside my directive controller.
Since the controller constructor get executed first,communication from controller to link is possible but not vice versa.
What should be the best way to achieve this?
I have considered the following approaches
1)Add the variable to scope-
That would in my opinion pollute the scope,making the variable accessible every where else where the scope is being shared.
2)Use $broadcast
Again the same issue as above
3) Pass a callback function on controller's this and call it from the link function with config as its argument
4)Pass the value through a service- In my case I have multiple such directives that would need to pass date through this service
Or is there some better approach that I am missing out for doing this?
module.directive('myDirective',function(){
return{
restrict:'E',
templateUrl:'path/to/html',
link:function(scope,iElement,iAttrs,controller){
var config=iAttrs.config;
//How to access this value inside the directive controller?
},
controller:function($scope){
//the directive attribute 'config' is required here for some larger computations which are not
//manipulating the DOM and hence should be seperated from the link function
})
There you can use isolated scope concept where you create isolated scope inside your controller & that would not be prototypically inherited from its parent scope. For that you need to use scope: { ... } inside your directive option. There are three options to pass scope value inside a directive through attribute
# : One way binding
= : Two way binding
& : Expression
In your case first two cases would be fine that are depends which one you need to use. If you just want to pass the value of scope variable to the directive in that case you could use 1st approach which would be # one way binding.
If you want to update the variable in both directive as well as controller from where it come i.e. nothing but two way binding, then you need to use =
I think = suits in your case so you should go for =
Markup
<my-directive config="config"></my-directive>
Directive
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
config: '='
},
templateUrl: 'path/to/abc.html',
link: function(scope, iElement, iAttrs, controller) {
//here it will be access as scope.config
console.log(scope.config);
},
controller: function($scope) {
console.log($scope.config); //here also it would be available inisde scope
//you could put a watch to detect a changes on config
}
}
});
Demo Plunkr
Update
As config value has been provide from the attribute with expression like {{}} so we could get those changes inside controller by putting [**$observe**][2] on $attrs. For that you need to inject $attrs dependency on your controller that will give you all the attributes collection which are available on directive element. And on the same $attrs object we gonna put $observe which work same as that of $watch which does dirty checking & if value gets change it fires that watch.
Directive
app.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: 'path/to/abc.html',
link: function(scope, iElement, iAttrs, controller) {
//here it will be access as scope.config
console.log(scope.config);
},
controller: function($scope,$attrs) {
//you could put a watch to detect a changes on config
$attrs.$observe('config', function(newV){
console.log(newV);
})
}
}
});
Updated Demo
I’m building a directive, I’m calling ‘requires-authorization’ to wrap an ng-if directive. I’d like to use it as follows:
<requires-authorization role='SuperUser'>
<!— super secret user stuff goes here, within
the scope of this view's controller —>
</requires-authorization>
I’ve gotten as far as:
angular.module('myApp').directive('requiresAuthorization', function() {
return {
template: '<div ng-if=\'iAmInRole\' ng-transclude></div>',
restrict: 'E',
transclude: true,
scope: {
role: '#'
},
controller: function($scope, UserService) {
$scope.iAmInRole = (UsersService.myRoles.indexOf($scope.role) !== -1);
}
};
});
This works, but the content contained within the directive loses its scope, specifically the scope of the controller of the view it's found within. What am I overlooking?
jsfiddle for reference: http://jsfiddle.net/HbAmG/8/
Notice how the auth value isn't displayed inside the directive, but is available outside directive.
Both ng-if and ng-transclude directives perform transclusion in your directive. In this case build-in transclude mechanism does not work fine and you should implement ngIf of yourself to make it work as expected:
JavaScript
app.directive('requiresAuthorization', function () {
return {
template: '<div ng-transclude></div>',
restrict: 'E',
transclude: true,
scope: {
role: '#'
},
controller: function ($scope) {
$scope.iAmInRole = true;
},
link: function(scope, element, attr, ctrl, transcludeFn) {
transcludeFn(function(clone) { // <= override default transclude
element.empty();
if(scope.iAmInRole) { // <= implement ngIf by yourself
element.append(clone);
}
});
}
};
});
Plunker: http://plnkr.co/edit/lNIPoJg786O0gVOoro4z?p=preview
If ng-show is an option for you to use instead of ng-if it may be a very simple workaround as well. The only side effect is that hidden data will be presented in the DOM and hidden using CSS .ng-hide {display: none !important;}.
JSFiddle: http://jsfiddle.net/WfgXH/3/
This post may also be useful for you since it describes the similar issue: https://stackoverflow.com/a/22886515/1580941
Once you define the scope property in your directive, it becomes an isolated scope. With no access to the outside (well, in a way, the only way is ugly and should be avoided), except to the stuff you pass into it via the scope property.
You'll need to either pass them into the directive: updated your jsfiddle
<requires-authorization role='Admin' data-auth-value='authValue' data-unauth-value='unAuthValue'>
<div>Inside directive. For Admin eyes only</div>
<p>{{authValue}}</p>
</requires-authorization>
// your directive scope
scope: {
role: '#',
authValue: '=',
unauthValue: '='
}
Or create a service/factory to act as a middle man to communicate.
You use ng-if. It does transclusion as well, unfortunately using a child scope of it's own scope, which in turn is the isolate scope.
Below are the screenshots from Batarang. The first is your code with ng-if. 4 is the isolate scope, 6 the transcluded content.
The same without ng-if. The transcluded content is now 5 and a sibling of the isolate scope and, more importantly, child of the controller's scope.
I've a question about a directive I want to write. Here is the jsfiddle
This is the HTML
<div ng-controller="TestCtrl">
<mydir base="{{absolute}}">
<div>{{path1}}</div>
<div>{{path2}}</div>
</mydir>
</div>
The directive, called 'mydir', will need the value of the base attribute and all the path values (there can be any number of paths defined). I don't want to inject values from the controller directly into the directive scope (they need to be independent). I have 3 questions about this (checkout the jsfiddle too!)
1) although the code works as it is now, there is an error: "TypeError: Object # has no method 'setAttribute'". Any suggestions what is wrong?
2) in the directive scope.base is 'undefined' instead of '/base/'. Can this be fixed.
3) How can I make a list of all the path values in the directive ?
You need to set an isolate scope that declares the base attribute for this directive:
scope: {
base: "#"
}
This solves (2).
Problem (1) is caused from the replace: true in cooperation with the scope above, because it creates a comment node to contain the repeated <div>s.
For (3), add paths: "=" to scope and, of ocurse, pass them from the parent controller as an array.
See forked fiddle: http://jsfiddle.net/swYAZ/1/
You're creating an new scope in your directive by setting scope: true what you want to do is:
app.directive('mydir', function ($compile, $rootScope) {
return {
replace: true,
restrict: 'E',
template: '<div ng-repeat="p in paths"> {{base}}{{p}}/{{$index}}</div>',
scope: {
base : '='
},
link: function (scope, iElement, iAttrs) {
scope.paths = ['p1', 'p2'] ;
}
}
});
This will setup a bi-directional binding between your directive's local scope and the parent's scope. If the value changes in the parent controller it will change in the directive as well.
http://jsfiddle.net/4LAjf/8/
If you want to make a robust directive to be able to replace the data to its content (3)
You can pass a reference as an attribute and display the data accordingly.
See this article, look for wrapper widget.
I have a directive for a javascript grid library slickgrid.
http://plnkr.co/edit/KWZ9i767ycz49hZZGswB?p=preview
What I want to do is pass the selected row back up the controller. So I want to use isolated scope (using the '=') to get two-way binding working between the controller and directive.
Everything works if I define the directive without any sort of scope declaration:
<slickgrid id="myGrid" data="names" selected-item="selectedItem"></slickgrid>
app.directive('slickgrid', function() {
return {
restrict: 'E',
replace: true,
//scope: {
// selectedItem: '='
//},
template: '<div></div>',
link: function($scope, element, attrs) {
...
var redraw = function(newScopeData) {
grid.setData(newScopeData);
grid.render();
};
$scope.$watch(attrs.data, redraw, true);
But if I uncomment the lines above (lines 19-21 in app.js) it looks like the $scope.$watch which is watching the attrs.data object is calling redraw but the attrs.data is being passed in as undefined.
My analysis could be wrong, but I'm not sure why defining the scope would cause this. Can someone explain why that might be?
.nathan.
If you define an isolate scope, then any $watch in your directive will be looking for whatever attrs.data evaluates to on your isolate scope. attrs.data evaluates to the string names, so the $watch is looking for $scope.names on your isolate scope, which doesn't exist. (Without the isolate scope, the directive uses the same scope as MainCtrl, and $scope.names exists there.)
To use an isolate scope, you'll need to define another isolate scope property to pass in names:
scope: {
selectedItem: '=',
data: '='
},
...
$scope.$watch('data', redraw, true);
The HTML can remain the same.
I have a directive which looks something like:
var myApp = angular.module('myApp',[])
.directive("test", function() {
return {
template: '<button ng-click="setValue()">Set value</button>',
require: 'ngModel',
link: function(scope, iElement, iAttrs, ngModel) {
scope.setValue = function(){
ngModel.$setViewValue(iAttrs.setTo);
}
}
};
});
The problem is that if I use this directive multiple times in a page then setValue only gets called on the last declared directive. The obvious solution is to isolate the scope using scope: {} but then the ngModel isn't accessible outside the directive.
Here is a JSFiddle of my code: http://jsfiddle.net/kMybm/3/
For this scenario ngModel probably isn't the right solution. That's mostly for binding values to forms to doing things like marking them dirty and validation...
Here you could just use a two way binding from an isolated scope, like so:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
target: '=target',
setTo: '#setTo'
},
template: '<button ng-click="setValue()">Set value</button>',
controller: function($scope) {
$scope.setValue = function() {
$scope.target = $scope.setTo;
};
//HACK: to get rid of strange behavior mentioned in comments
$scope.$watch('target',function(){});
}
};
});
All you need to do is add scope: true to your directive hash. That makes a new inheriting child scope for each instance of your directive, instead of continually overwriting "setValue" on whatever scope is already in play.
And you're right about isolate scope. My advice to newbies is just don't use it ever.
Response to comment:
I understand the question better now. When you set a value via an expression, it sets it in the most immediate scope. So what people typically do with Angular is they read and mutate values instead of overwriting values. This entails containing things in some structure like an Object or Array.
See updated fiddle:
http://jsfiddle.net/kMybm/20/
("foo" would normally go in a controller hooked up via ngController.)
Another option, if you really want to do it "scopeless", is to not use ng-click and just handle click yourself.
http://jsfiddle.net/WnU6z/8/