How to access parent scope from within a custom directive *with own scope* in AngularJS?
Solution 1
See What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
To summarize: the way a directive accesses its parent ($parent
) scope depends on the type of scope the directive creates:
-
default (
scope: false
) - the directive does not create a new scope, so there is no inheritance here. The directive's scope is the same scope as the parent/container. In the link function, use the first parameter (typicallyscope
). -
scope: true
- the directive creates a new child scope that prototypically inherits from the parent scope. Properties that are defined on the parent scope are available to the directivescope
(because of prototypal inheritance). Just beware of writing to a primitive scope property -- that will create a new property on the directive scope (that hides/shadows the parent scope property of the same name). -
scope: { ... }
- the directive creates a new isolate/isolated scope. It does not prototypically inherit the parent scope. You can still access the parent scope using$parent
, but this is not normally recommended. Instead, you should specify which parent scope properties (and/or function) the directive needs via additional attributes on the same element where the directive is used, using the=
,@
, and&
notation. -
transclude: true
- the directive creates a new "transcluded" child scope, which prototypically inherits from the parent scope. If the directive also creates an isolate scope, the transcluded and the isolate scopes are siblings. The$parent
property of each scope references the same parent scope.
Angular v1.3 update: If the directive also creates an isolate scope, the transcluded scope is now a child of the isolate scope. The transcluded and isolate scopes are no longer siblings. The$parent
property of the transcluded scope now references the isolate scope.
The above link has examples and pictures of all 4 types.
You cannot access the scope in the directive's compile function (as mentioned here: https://github.com/angular/angular.js/wiki/Dev-Guide:-Understanding-Directives). You can access the directive's scope in the link function.
Watching:
For 1. and 2. above: normally you specify which parent property the directive needs via an attribute, then $watch it:
<div my-dir attr1="prop1"></div>
scope.$watch(attrs.attr1, function() { ... });
If you are watching an object property, you'll need to use $parse:
<div my-dir attr2="obj.prop2"></div>
var model = $parse(attrs.attr2);
scope.$watch(model, function() { ... });
For 3. above (isolate scope), watch the name you give the directive property using the @
or =
notation:
<div my-dir attr3="{{prop3}}" attr4="obj.prop4"></div>
scope: {
localName3: '@attr3',
attr4: '=' // here, using the same name as the attribute
},
link: function(scope, element, attrs) {
scope.$watch('localName3', function() { ... });
scope.$watch('attr4', function() { ... });
Solution 2
Accessing controller method means accessing a method on parent scope from directive controller/link/scope.
If the directive is sharing/inheriting the parent scope then it is quite straight forward to just invoke a parent scope method.
Little more work is required when you want to access parent scope method from Isolated directive scope.
There are few options (may be more than listed below) to invoke a parent scope method from isolated directives scope or watch parent scope variables (option#6 specially).
Note that I used link function
in these examples but you can use a directive controller
as well based on requirement.
Option#1. Through Object literal and from directive html template
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<p> Directive Content</p>
<sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged(selectedItems)" items="items"> </sd-items-filter>
<P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnedFromDirective}} </p>
</body>
</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" ng-change="selectedItemsChanged({selectedItems:selectedItems})" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
<option>--</option>
</select>
app.js
var app = angular.module('plunker', []);
app.directive('sdItemsFilter', function() {
return {
restrict: 'E',
scope: {
items: '=',
selectedItems: '=',
selectedItemsChanged: '&'
},
templateUrl: "itemfilterTemplate.html"
}
})
app.controller('MainCtrl', function($scope) {
$scope.name = 'TARS';
$scope.selectedItems = ["allItems"];
$scope.selectedItemsChanged = function(selectedItems1) {
$scope.selectedItemsReturnedFromDirective = selectedItems1;
}
$scope.items = [{
"id": "allItems",
"name": "All Items",
"order": 0
}, {
"id": "CaseItem",
"name": "Case Item",
"model": "PredefinedModel"
}, {
"id": "Application",
"name": "Application",
"model": "Bank"
}]
});
working plnkr: http://plnkr.co/edit/rgKUsYGDo9O3tewL6xgr?p=preview
Option#2. Through Object literal and from directive link/scope
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<p> Directive Content</p>
<sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged(selectedItems)" items="items"> </sd-items-filter>
<P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnedFromDirective}} </p>
</body>
</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;"
ng-change="selectedItemsChangedDir()" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
<option>--</option>
</select>
app.js
var app = angular.module('plunker', []);
app.directive('sdItemsFilter', function() {
return {
restrict: 'E',
scope: {
items: '=',
selectedItems: '=',
selectedItemsChanged: '&'
},
templateUrl: "itemfilterTemplate.html",
link: function (scope, element, attrs){
scope.selectedItemsChangedDir = function(){
scope.selectedItemsChanged({selectedItems:scope.selectedItems});
}
}
}
})
app.controller('MainCtrl', function($scope) {
$scope.name = 'TARS';
$scope.selectedItems = ["allItems"];
$scope.selectedItemsChanged = function(selectedItems1) {
$scope.selectedItemsReturnedFromDirective = selectedItems1;
}
$scope.items = [{
"id": "allItems",
"name": "All Items",
"order": 0
}, {
"id": "CaseItem",
"name": "Case Item",
"model": "PredefinedModel"
}, {
"id": "Application",
"name": "Application",
"model": "Bank"
}]
});
working plnkr: http://plnkr.co/edit/BRvYm2SpSpBK9uxNIcTa?p=preview
Option#3. Through Function reference and from directive html template
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<p> Directive Content</p>
<sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged" items="items"> </sd-items-filter>
<P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnFromDirective}} </p>
</body>
</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;"
ng-change="selectedItemsChanged()(selectedItems)" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
<option>--</option>
</select>
app.js
var app = angular.module('plunker', []);
app.directive('sdItemsFilter', function() {
return {
restrict: 'E',
scope: {
items: '=',
selectedItems:'=',
selectedItemsChanged: '&'
},
templateUrl: "itemfilterTemplate.html"
}
})
app.controller('MainCtrl', function($scope) {
$scope.name = 'TARS';
$scope.selectedItems = ["allItems"];
$scope.selectedItemsChanged = function(selectedItems1) {
$scope.selectedItemsReturnFromDirective = selectedItems1;
}
$scope.items = [{
"id": "allItems",
"name": "All Items",
"order": 0
}, {
"id": "CaseItem",
"name": "Case Item",
"model": "PredefinedModel"
}, {
"id": "Application",
"name": "Application",
"model": "Bank"
}]
});
working plnkr: http://plnkr.co/edit/Jo6FcYfVXCCg3vH42BIz?p=preview
Option#4. Through Function reference and from directive link/scope
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<p> Directive Content</p>
<sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged" items="items"> </sd-items-filter>
<P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnedFromDirective}} </p>
</body>
</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" ng-change="selectedItemsChangedDir()" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
<option>--</option>
</select>
app.js
var app = angular.module('plunker', []);
app.directive('sdItemsFilter', function() {
return {
restrict: 'E',
scope: {
items: '=',
selectedItems: '=',
selectedItemsChanged: '&'
},
templateUrl: "itemfilterTemplate.html",
link: function (scope, element, attrs){
scope.selectedItemsChangedDir = function(){
scope.selectedItemsChanged()(scope.selectedItems);
}
}
}
})
app.controller('MainCtrl', function($scope) {
$scope.name = 'TARS';
$scope.selectedItems = ["allItems"];
$scope.selectedItemsChanged = function(selectedItems1) {
$scope.selectedItemsReturnedFromDirective = selectedItems1;
}
$scope.items = [{
"id": "allItems",
"name": "All Items",
"order": 0
}, {
"id": "CaseItem",
"name": "Case Item",
"model": "PredefinedModel"
}, {
"id": "Application",
"name": "Application",
"model": "Bank"
}]
});
working plnkr: http://plnkr.co/edit/BSqx2J1yCY86IJwAnQF1?p=preview
Option#5: Through ng-model and two way binding, you can update parent scope variables.. So, you may not require to invoke parent scope functions in some cases.
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<p> Directive Content</p>
<sd-items-filter ng-model="selectedItems" selected-items-changed="selectedItemsChanged" items="items"> </sd-items-filter>
<P style="color:red">Selected Items (in parent controller) set to: {{selectedItems}} </p>
</body>
</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;"
ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
<option>--</option>
</select>
app.js
var app = angular.module('plunker', []);
app.directive('sdItemsFilter', function() {
return {
restrict: 'E',
scope: {
items: '=',
selectedItems: '=ngModel'
},
templateUrl: "itemfilterTemplate.html"
}
})
app.controller('MainCtrl', function($scope) {
$scope.name = 'TARS';
$scope.selectedItems = ["allItems"];
$scope.items = [{
"id": "allItems",
"name": "All Items",
"order": 0
}, {
"id": "CaseItem",
"name": "Case Item",
"model": "PredefinedModel"
}, {
"id": "Application",
"name": "Application",
"model": "Bank"
}]
});
working plnkr: http://plnkr.co/edit/hNui3xgzdTnfcdzljihY?p=preview
Option#6: Through $watch
and $watchCollection
It is two way binding for items
in all above examples, if items are modified in parent scope, items in directive would also reflect the changes.
If you want to watch other attributes or objects from parent scope, you can do that using $watch
and $watchCollection
as given below
html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{user}}!</p>
<p>directive is watching name and current item</p>
<table>
<tr>
<td>Id:</td>
<td>
<input type="text" ng-model="id" />
</td>
</tr>
<tr>
<td>Name:</td>
<td>
<input type="text" ng-model="name" />
</td>
</tr>
<tr>
<td>Model:</td>
<td>
<input type="text" ng-model="model" />
</td>
</tr>
</table>
<button style="margin-left:50px" type="buttun" ng-click="addItem()">Add Item</button>
<p>Directive Contents</p>
<sd-items-filter ng-model="selectedItems" current-item="currentItem" name="{{name}}" selected-items-changed="selectedItemsChanged" items="items"></sd-items-filter>
<P style="color:red">Selected Items (in parent controller) set to: {{selectedItems}}</p>
</body>
</html>
script app.js
var app = angular.module('plunker', []);
app.directive('sdItemsFilter', function() {
return {
restrict: 'E',
scope: {
name: '@',
currentItem: '=',
items: '=',
selectedItems: '=ngModel'
},
template: '<select ng-model="selectedItems" multiple="multiple" style="height: 140px; width: 250px;"' +
'ng-options="item.id as item.name group by item.model for item in items | orderBy:\'name\'">' +
'<option>--</option> </select>',
link: function(scope, element, attrs) {
scope.$watchCollection('currentItem', function() {
console.log(JSON.stringify(scope.currentItem));
});
scope.$watch('name', function() {
console.log(JSON.stringify(scope.name));
});
}
}
})
app.controller('MainCtrl', function($scope) {
$scope.user = 'World';
$scope.addItem = function() {
$scope.items.push({
id: $scope.id,
name: $scope.name,
model: $scope.model
});
$scope.currentItem = {};
$scope.currentItem.id = $scope.id;
$scope.currentItem.name = $scope.name;
$scope.currentItem.model = $scope.model;
}
$scope.selectedItems = ["allItems"];
$scope.items = [{
"id": "allItems",
"name": "All Items",
"order": 0
}, {
"id": "CaseItem",
"name": "Case Item",
"model": "PredefinedModel"
}, {
"id": "Application",
"name": "Application",
"model": "Bank"
}]
});
You can always refer AngularJs documentation for detailed explanations about directives.
Solution 3
scope: false
transclude: false
and you will have the same scope(with parent element)
$scope.$watch(...
There are a lot of ways how to access parent scope depending on this two options scope& transclude.
Solution 4
Here's a trick I used once: create a "dummy" directive to hold the parent scope and place it somewhere outside the desired directive. Something like:
module.directive('myDirectiveContainer', function () {
return {
controller: function ($scope) {
this.scope = $scope;
}
};
});
module.directive('myDirective', function () {
return {
require: '^myDirectiveContainer',
link: function (scope, element, attrs, containerController) {
// use containerController.scope here...
}
};
});
and then
<div my-directive-container="">
<div my-directive="">
</div>
</div>
Maybe not the most graceful solution, but it got the job done.
Solution 5
If you are using ES6 Classes and ControllerAs
syntax, you need to do something slightly different.
See the snippet below and note that vm
is the ControllerAs
value of the parent Controller as used in the parent HTML
myApp.directive('name', function() {
return {
// no scope definition
link : function(scope, element, attrs, ngModel) {
scope.vm.func(...)
colllin
Updated on November 19, 2020Comments
-
colllin over 3 years
I'm looking for any manner of accessing the "parent" scope within a directive. Any combination of scope, transclude, require, passing in variables (or the scope itself) from above, etc. I'm totally willing to bend over backwards, but I want to avoid something totally hacky or unmaintainable. For example, I know I could do it right now by taking the
$scope
from the preLink parameters and iterating over it's$sibling
scopes to find the conceptual "parent".What I really want is to be able to
$watch
an expression in the parent scope. If I can do that, then I can accomplish what I'm trying to do over here: AngularJS - How to render a partial with variables?An important note is that the directive must be re-usable within the same parent scope. Therefore the default behavior (scope: false) doesn't work for me. I need an individual scope per instance of the directive, and then I need to
$watch
a variable that lives in the parent scope.A code sample is worth 1000 words, so:
app.directive('watchingMyParentScope', function() { return { require: /* ? */, scope: /* ? */, transclude: /* ? */, controller: /* ? */, compile: function(el,attr,trans) { // Can I get the $parent from the transclusion function somehow? return { pre: function($s, $e, $a, parentControl) { // Can I get the $parent from the parent controller? // By setting this.$scope = $scope from within that controller? // Can I get the $parent from the current $scope? // Can I pass the $parent scope in as an attribute and define // it as part of this directive's scope definition? // What don't I understand about how directives work and // how their scope is related to their parent? }, post: function($s, $e, $a, parentControl) { // Has my situation improved by the time the postLink is called? } } } }; });
-
colllin almost 11 yearsYes, short & sweet, and correct. They seem to share the exact same scope as parent element though... which makes them impossible to re-use in the same scope. jsfiddle.net/collindo/xqytH
-
colllin almost 11 yearsTHANK YOU, Mark. It turns out the solution I posted on How to render a partial with variables really does work quite beautifully. What you really needed to link me to was something titled "The nuances of writing HTML and recognizing that your element isn't nested inside the ng-controller that you think it is." Wow... rookie mistake. But this is a useful addition to your other (much longer) answer explaining scopes.
-
Mark Rajcok almost 11 years@collin, great, I'm glad you solved your issue, since I wasn't quite sure how to respond to your other (now deleted) comment.
-
Junaid Qadir Shekhanzai over 9 yearsWhat stuff can/should I perform within
scope.$watch('localName3', function() { ...[?? WHAT TO DO HERE for example?] });
-
Andy over 9 yearsFor your example 3, should I be using
$parse
when watchingattr4
like you mentioned in example 2? Great explanation by the way! -
Mark Rajcok over 9 years
-
Andy over 9 yearsThank you for clearing this up, I really appreciate it!
-
John Trichereau about 9 yearsThis is a great answer, very thorough. It also illustrates why I simply hate working with AngularJS.
-
br3w5 almost 9 yearsThank you - points 1 to 4 the simplest explanation i've found yet
-
JustAMartin over 8 yearsI found one case where I seemingly need to use
$scope.$parent
- this when I want to redirect an event handler (e.g. ng-focus) from my custom directive to an element inside directive. If I just bind it using=
, then it gets evaluated immediately by Angular, but I need it to be evaluated later, when the event occurs in my directive. So I can to like this:directiveSscope.$parent.$eval($scope.ngFocusExpression);
inside directive's ng-focus handler. -
khichar.anil over 8 yearskudos...nice explanation. @MarkRajcok: To capture object property change, we can simply use $watch instead of $parse. Plunker: plnkr.co/edit/GWT3Dvps4eJk3oxvZ3fF Here you can see 'options[0].form_list' is being watched and value is updating correctly. Could you please post plunker for $parse for better explanation.
-
slim over 8 yearsHe works hard for his rep... so hard for his rep... he works hard for his rep so you better upvote him right.
-
Dave over 8 yearsplease see answer by Simon H below if you're using the ControllerAs syntax in your html.
-
redress over 8 yearsdownvoted--any valuable information within the answer is inaccessible due to its length
-
Yogesh Manware over 8 yearsI answered the question with all available alternatives with clear separation. In my opinion, short answers are not always helpful until you have a big picture in front of you.
-
Yvon Huynh over 8 yearsmany times we need isolated scope when we write reusable component, so the solution is not that simple
-
damd over 8 years@YogeshManware: It could be shortened a lot by leaving out the irrelevant stuff like stylesheets, not using lengthy markup, simplifying the examples to not use things like "group by", etc. It would also be very useful with some sort of explanation for each example.
-
Winnemucca almost 7 yearsThis isn't a reason to down vote. People abuse this privlege
-
Taylor Ackley over 6 yearsSO answer goals
-
balron about 6 years#6 is what i am looking for. Thanks