Angular directive link() function receiving inconsistent scope - javascript

"use strict";
angular.module("foo")
.directive("breadcrumbs", function($rootScope, $timeout) {
return {
replace: true,
scope: false,
templateUrl: '/js/shared/directives/breadcrumbs.tpl.html',
link: function(scope, element, attrs, controller, transcludeFn){
console.debug(scope.parent);
...
Sometimes scope.parent is the scope of the current controller, sometimes it's $rootScope. I've discovered that when my custom directive is used like this:
<breadcrumbs></breadcrumbs>
it receives the controller scope, but when used with ng-if:
<breadcrumbs ng-if="searchData"></breadcrumbs>
it receives a new scope nested within the controller scope.
How do I write my directive to always receive the controller scope without resorting to any hackery (such as adding ng-if="true")?
Update: I've corrected the description of the problem. The issue is that I get scopes at different levels of nesting depending on whether ng-if is used, regardless of whether scope: false or scope: true was used.

First of all, if the directive is in context of a controller scope, you'll never receive $rootScope as the scope of directive.
When you use ng-if, you are receiving a prototypically inherited child scope (of the immediate parent scope, in this case the controller) created by ng-if becuase your directive has scope: false (which means pass in the closest scope instead of creating one)
You can always request a prototypically inherited child scope by setting scope:true or an isolated scope via scope:{}.

What ng-if=true dose is create a new scope for your element.
You could achive that by changing the parameter
From
scope: false
To
scope: true

Related

AngularJS scope variable won't persist inside a directive with isolate scope

I can't set a scope variable inside my custom directive's controller.
See this plkr:
http://plnkr.co/edit/KVGVxhgRHxkhCLytkLBv?p=preview
Link function prints the variable name but interpolator does not evaluate {{name}}. I set it in the controller of my custom directive with isolate scope. Still for some reason, scope variable does not persist. Is this behavior expected? If so what is the proper way to set scope variables?
Help is appreciated.
Import 'name' from your outside scope into your isolated scope using '=' binding:
// Code goes here
angular.module('test', [])
.directive('nametag', function() {
return {
scope: { name: '='},
controller: function($scope, $attrs, $log) {
$scope.name = 'John Doe';
},
link: function(scope, elem, attrs) {
// elem.text(scope.name);
}
};
});
HTML:
<div ng-app="app" ng-init="name='james'>
<nametag name="name"></nametag>
</div>
When you define an isolated scope for your directive (by specifying scope: {}) you create a private scope that does not prototypically inherit from the parent scope. You can think of an isolated scope as your directive's private sandbox.
Since your directive's scope is isolated, you need to import scope variables from parent scope into your isolated scope through attribute bindings. In this case we are establishing two-way binding between the parent scope variable 'name', and the directive's isolated scope variable with the same name.
Your directive controller assigns the name to the isolated scope of the directive. So you have two possibilities:
Remove the scope: {} from your directive to not create an isolated scope. While this works it is maybe not what you want as your directive modifies the outer scope.
Add a template to your diective containing the <h1>Hello {{name}}</h1>. See this plunker.

AngularJS $parse not working

I am trying to understand $parse, based on the documentation. But I am having trouble to get my test code working. Am I using $parse service the right way?
The main part of the code is:
app.directive('try', function($parse) {
return {
restrict: 'E',
scope: {
sayHello: "&hello"
},
transclude: true,
template: "<div style='background:gray;color:white'>Hello I am try: <span ng-transclude></span><div>",
link: function($scope, $elem, $attr) {
var getter = $parse($attr.sayHello);
// var setter = getter.assign;
$elem.on('click', function() {
getter($scope);
$scope.$apply();
});
}
};
});
See my code at: http://plnkr.co/edit/lwV5sHGoCf2HtQa3DaVI
I haven't used the $parse method, but this code achives what you are looking for:
http://plnkr.co/edit/AVvxLR4RcmWhLo8eqYyd?p=preview
As far as I can tell, the $parse service is intended to be used outside of an isolate scope.
When you have an isolate scope, like in your directive, you can obtain a reference to the parent scope's function using the 'sayHello': '&' as proposed in Shai's answer. The $parse service might still work as expected even with an isolate scope, if you are able to pass in the parent scope instead of the directive's scope when calling getter($scope), but I haven't tested that.
Edit: This is indeed the case - using getter($scope.$parent) works fine. When an isolate scope is used in your directive, the $scope variable no longer refers to the correct context for the getter function returned by the $parse service. Access the correct one by using $scope.$parent.
However, if you are avoiding an isolate scope, your approach works well. Try removing the scope: { ... } section out of your directive definition entirely and you'll see it works fine. This is handy if you are creating a directive for event binding that might be applied to an element in conjunction with another directive that has an isolate scope, say a dragenter directive (which isn't provided by Angular). You couldn't use Shai's method in that case, since the isolate scopes would collide and you'd get an error, but you could use the $parse service.
Here's an updated plunker with the scope removed from the directive definition: http://plnkr.co/edit/6jIjc8lAK9yjYnwDuHYZ

AngularJS Directive controllerAs syntax & scope

Here is my Code:
// HTML
<body>
<h1>{{foo.name}}</h1>
<my-directive></my-directive>
</body>
// Scripts
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
scope: true, //**********
template: '<h4>{{foo.name}}</h4>',
controllerAs: 'foo',
controller: fooCtrl,
link: function(scope) {
console.log(scope);
}
}
});
var fooCtrl = function() {
this.name = 'FOO';
}
My Question:
If I use controllerAs syntax and don't set scope: true in myDirective, the controller will become global, so the controller will insert foo.name into Tag. But once I set the scope as true, the controller will only apply on myDirective.
How could this happened? Does a controller inside directive create a new scope that inherits from the surrounding scope? Why it will apply on global?
UPDATE
This is a very silly question, as I always use isolate scope before, so forget about the usage of scope inside directive, read the docs and clearly understand. Thanks for answer
I guess your are asking regarding the scope property in Angular Directives. Also I presume you mean $rootScope by term 'global'. As explained in this guide, In a directive, scope property behaves as follows,
scope.false
The default option which does not create a new scope for a directive
but shares the scope with its parent
scope.true
Creates a new scope but prototypically inherits from the parent scope.
scope: ‘isolate’
Creates an isolated scope which does not prototypically inherit from
the parent scope but you can access parent scope using scope.$parent
Check out the directive documentation and scroll down a bit to the part about the scope option. Setting scope to true creates a new scope that the controller is attached to. Not setting scope defaults it to false, which causes the directive to use the parent scope. That's why the controller is being attached to the parent scope.

Isolated scope pitfall with ng-model dependency

Well, since 'improve this doc' button on AngularJS documentation site doesn't work and discussion is now closed, I'd like to ask a question about 'isolated scope pitfall' paragraph of ngModelController.
<div ng-app="badIsolatedDirective">
<input ng-model="someModel"/>
<div isolate ng-model="someModel"></div>
<div isolate ng-model="$parent.someModel"></div>
</div>
angular.module('badIsolatedDirective', [])
.directive('isolate', function() {
return {
require: 'ngModel',
scope: { },
template: '<input ng-model="innerModel">',
link: function(scope, element, attrs, ngModel) {
scope.$watch('innerModel', function(value) {
console.log(value);
ngModel.$setViewValue(value);
});
}
};
});
I expected to see the third input affecting first one (cause we just isolated second input's scope and have no reference to 'someModel' scope value), btw behavior of this example is just stunning: second input affects first, third doesn't affect anything. So the question is: am I loosing the concept or just don't understand it, or there are mistakes (maybe, not mistakes, but just no connection to the topic) in the example code (well, I changed it on Plunkr to make it work as I expected).
In 1.2.0 timely-delivery there was a major change (here) to how multiple isolate scope directives on the same element work. This change apparently hasn't been reflected in their documentation.
Prior to 1.2.0 all directives on an element shared an isolate scope if any of the directives requested an isolate scope. Therefore in the above example the ngModel directive shared the isolate directive's scope. Which is why we had to reference the parent scope like this- ng-model="$parent.someModel"
That is no longer true in 1.2.0.
In 1.2.0 and beyond the ngModel directive no longer shares scope with isolate. ngModel is now on the parent scope of the isolate directive. Thus we now need ng-model="someModel" instead of ng-model="$parent.someModel"
Here's their description of the change (keeping in mind as you read this that ngModel is a directive):
Make isolate scope truly isolate
Fixes issue with isolate scope leaking all over the place into other directives
on the same element.
Isolate scope is now available only to the isolate directive that requested it
and its template.
A non-isolate directive should not get the isolate scope of an isolate directive
on the same element,instead they will receive the original scope (which is the
parent scope of the newly created isolate scope).
BREAKING CHANGE: Directives without isolate scope do not get the
isolate scope from an isolate directive on the same element. If your
code depends on this behavior (non-isolate directive needs to access
state from within the isolate scope), change the isolate directive to
use scope locals to pass these explicitly.
Before
<input ng-model="$parent.value" ng-isolate>
.directive('ngIsolate', function() { return {
scope: {},
template: '{{value}}' }; });
After
<input ng-model="value" ng-isolate>
.directive('ngIsolate', function() { return {
scope: {value: '=ngModel'},
template: '{{value}} }; });
Here's a version running 1.2.0-rc3 (the last version before this change) which operates like their documentation describes: http://jsfiddle.net/5mKU3/
And immediately after, 1.2.0 stable, we no longer need, or want, the reference to '$parent': http://jsfiddle.net/5mKU3/1/

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