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.
Related
I need to do this to make use of the <dialog> tag in HTML5, I want every <dialog> on my site to have its own unique scope accessible using the controller and controllerAs syntax.
Here is what I thought would work.
Javascript
\\Dialog Controller
function dialog () {
return {
scope: {},
controller: function () {
this.test = 'Dialog Test';
},
controllerAs: 'Dialog',
bindToController: true
}
}
angular.module('app',[]).directive('dialog', dialog);
HTML
<!-- Dialog HTML Example Case -->
<body ng-app='app'>
<dialog id='test'>{{Dialog.test}}</dialog>
</body>
I would expect that when the dialog was activated Dialog.test would evaluate to Dialog Test. What happens instead is that it evaluates to an empty string. What's more is that if I add a controller to the body, the dialog has access to its scope. It is as though the isolate scope definition in my directive is completely ignored.
Plunk
Note that I have modified the plunk to use <span> instead of <dialog> due to the lack of support in most browsers.
http://plnkr.co/edit/eXtUq7BxCajOZAp8BpVe?p=preview
You are creating isolated scope, thats good thing. But after AngularJS1.2 version, they have done some breaking changes, where isolated scope will be completely isolated.
So Span directive's scope will be visible to template of that directive(Span) only.
And inner html of that directive will get only parent/current scope only instead of directive isolated scope(As Isolated Scope will be visible to template only). To print value of Span.test, you have to create template and refer that template in your directive as below:
var app = angular.module('test', []);
function mainCtrl() {
this.test = 'test';
};
function spanCtrl() {
this.test = 'Span Test';
}
function span () {
return {
scope: {},
controller: spanCtrl,
controllerAs: 'Span',
template: '{{Span.test}}'
}
}
app.controller('mainCtrl', mainCtrl);
app.directive('span', span);
You can checkout two awesome blog for more detail Component In AngularJS and Transclude In AngularJS
Contents of an element do not "see" the isolate scope of a directive. The fact that the scope is "isolate" means that it is separate from the scope of the View where both the directive and contents of its hosting element reside.
To link the contents against the internal scope, you'd need to transclude them - the transclude function allows you to link it against any scope:
function dialog () {
return {
scope: {},
controller: function () {
this.test = 'Dialog Test';
},
controllerAs: 'Dialog',
bindToController: true
transclude: true.
link: function(scope, element, attrs, ctrls, transclude){
// scope here is the isolate scope of the directive
transclude(scope, function cloneAttachFn(contentsClone){
element.append(contentsClone);
});
}
}
}
The above would work, but it's still important to understand why the "default" behavior makes sense. Consider that to a user of your directive, the directive's internal (i.e. isolate) functionality (and thus scope variables) ought to be invisible. If it wasn't, the user of your directive would need to know about "special" scope variables, like Dialog, in your case. So, someone who reads your HTML would have no idea where Dialog came from without knowing how the directive operates.
There is a mistake in original assertion: the contents of dialog directive node
<dialog id='test'>{{Dialog.test}}</dialog>
belong to parent's scope, not to directive's scope. Therefore, it interpolates Dialog.test from parent controller (or root scope if there is none).
It is possible to achieve the behaviour that was expected with something like this:
app.directive('dialog', function ($interpolate) {
return {
scope: {},
controller: function ($scope) {
this.test = 'test';
},
controllerAs: 'Dialog',
compile: function (element) {
var template = element.text();
return function (scope, element) {
element.text($interpolate(template)(scope));
}
}
};
});
But it can hardly be called a promoted way to use Angular. Let the directive handle its template.
I have 2 directives on the same tag, <settings> and <modal>. settings provides a template, while modal creates an isolated scope:
jsfiddle
var app = angular.module('app', []);
app.directive('settings', function () {
return {
restrict: 'E',
template: '<div>SETTINGS</div>',
link: function (scope) {
console.log('settings', scope.$id);
}
};
});
app.directive('modal', function () {
return {
restrict: 'A',
scope: {},
link: function (scope) {
console.log('modal', scope.$id);
}
};
});
However, they do not end up sharing the same scope, as shown by the logs:
<settings modal="settings"></settings>
settings 002
modal 003
Why is that ?
Till version 1.2.0rc3, sibling directives were sharing the same isolated scope if created.
As of version 1.2.0, you cannot (and shouldn't) access the isolated scope from outside the contents compiled against this isolated scope. You shouldn't because you are creating a hidden relation between both directives. Better use directive's controller requirement via the require property, or share informations through injected services if singleton pattern can be applied to your use case.
It is finally deeply related to this other question about Directives with Isolated scope versions conflict
scope: {} option always creates an isolated scope for the directive, it doesn't care about other directives
app.directive('modal', function () {
return {
restrict: 'A',
scope: false,
link: function (scope) {
console.log('modal', scope.$id);
}
};
});
scope: false will not create a new isolated scope. I you want to make two directives work between them use require a controller on the directive that way you can share data between them.
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/