AngularJS : Scope not shared across directives - javascript

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.

Related

angularjs give all html elements of a specific type their own scope

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.

Dependency issue in Angular JS

I just created a new directive, however the console is giving me an error. I believe there is a problem with dependencies as the directive is unable to see a method within the controller.
How can i fix this?
Error message:
Error: $scope.resetMessages is not a function
Controller:
angular.module('modulename').controller('controllerName', ['$scope', '$location', 'Global', 'Company', function ($scope, $location, Global, Company) {
/* A bunch of declarations and methods... */
$scope.resetMessages = function() {
$scope.errorMessage = null;
$scope.successMessage = null;
};
}]);
Directive:
angular.module('modulename').directive('directiveName', function() {
return {
restrict: 'E',
templateUrl: 'path/to/template.html',
scope: {
'Company': '&',
'Global': '&'
},
controller: function($scope) {
/*
A bunch of methods...
*/
}
};
});
As Anthony Chu alluded to, this section creates an isolated scope:
scope: {
'Company': '&',
'Global': '&'
},
That means it does NOT prototypically inherit from the parent scope, although the parent scope is available via $parent although, as Anthony mentions, this isn't a great idea because it creates coupling between the two, which you are presumably trying to sever with an isolated scope in the first place.
You shouldn't need an isolated scope at all in this case. To get access to Company and Global (both available in the Controller via dependency injection) you can just inject them into your directive too:
angular.module('modulename').directive('directiveName',
['Global', 'Company', function (Global, Company) {
// Return directive config here
}]);
Then you can omit scope: (anything) in the directive completely, and it will be the SAME scope as the controller, or if necessary, you can do scope: true to get a new scope that DOES prototypically inherit from the parent. In either case, $scope.resetMessages() would be available.
Here is a handy cheat sheet to the different directive scope options.

AngularJS : Directive transcluded scope lost

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.

Isolate Scope "=" binding and doted notation AngularJS

How do you create a 2 way binding with a nested property in an isolate scope with dotted notation. I thought 'myObject.data': "=data" would work, but it does not. I don't want to link everything in the myObject object. I know I could do some sort of watch, but 'myObject.data' seems cleaner.
.directive("myDirective", [function() {
return {
restrict: "E",
scope: {
'myObject.data': "=data"
},
link: function (scope, element, attrs) {
scope.myObject = {
data: "myValue"
};
}
};
}])
Isolated scopes are generally useful only with templates, they should not be used as a way to declare how you want your directive attributes to be interpreted. This is because most directives that don't have a template usually need the semantics of either a child scope or the direct scope of their environment.
In your case, you probably don't even need a $watch, because object references are what enable 2 way data binding, but without your full code I cannot be sure.
In case you want to know the translations for an isolated scope semantics to just a normal one:
#name -> attrs.name
=name -> $scope.$eval(attrs.name);
&name -> function() { return $scope.$eval(attrs.name); }
EDIT 2:
After your comment, I came up with this plunker. To preserve two way data binding you have to use a "." in your ng-model declaration. This is because two way data binding does not work for value types, since they are immutable. You can't change the value of 100 for example. You need to pass around a reference type object and hang the values you are changing off of it. Your desire to specify the full path to the value in the isolated scope definition is not possible based on the principles that two way data binding is made possible by.
Javascript:
angular.module('plunker', [])
.directive('twoWay', function() {
return {
restrict: 'E',
template: '<div><input ng-model="thing.name" type="text" /></div>',
scope: {
thing: "="
},
link: function(scope, element, attrs) {
}
};
})
.controller('MainCtrl', function($scope) {
$scope.data = {
name: "World"
};
});
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{data.name}}!</p>
<two-way thing="data"></two-way>
</body>
What I use in these cases is the following:
.directive("myDirective", [function() {
return {
restrict: "E",
scope: {
data: "="
},
controller: function($scope){
$scope.dot = $scope //<--- here is the trick
}
};
}])
Then you can always change data in the directive's scope from an inherited scope through dot.data = 'whatever' without setting watchers.
Not very elegant but it works jsut fine in cases where you are not using the controller as syntax and don't want a $parent nightmare.

Directive behavior changes when defining isolated scope

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.

Categories

Resources