Why does declaring an isolate scope block $emit events from being heard? - angularjs

I'm using a directive that has a controller as a child scope and i'm trying to make them talk. It seems that when I $emit an an event from the child controller, the parent directive can only hear this event when it does not have an isolate scope declared. Why does declaring an isolate scope in my directive block $emit events from being heard?
html
<flippy data-click-toggle="true" data-mouseover-toggle="true">
<flippy-front>
<div ng-controller="test">
<div class="test" ng-click="flip()">
front
</div>
</div>
</flippy-front>
<flippy-back>
<div ng-controller="test">
<div class="test" ng-click="flip()">
back
</div>
</div>
</flippy-back>
</flippy>
js
poop = angular.module('angular-flippy', []);
poop.directive('flippy', function() {
return {
restrict: 'E',
scope: {}, <------- works when isolate scope not declared
link: function($scope, $elem, $attrs) {
var flip = function() {
$elem.toggleClass('flipped');
}
$scope.$on("flipped", flip);
}
};
});
poop.controller('test', ['$scope', function($scope) {
$scope.flip = function() {
$scope.$emit("flipped");
};
}]);
codepen:
http://codepen.io/anon/pen/xzteC

It happens because your controllers scopes are not children scopes of your directive.
If you debug your application you will see
-->$rootScope (id: 002)
-->directive scope (id: 003)
-->first controller scope (id: 004)
-->second controller scope (id: 005)
As you can see, directive scope and controllers scopes are siblings - relation parent-child needed to listen the event doesn't exist.
It works if you omit scope:{} because then your directive uses rootScope as its own scope, so it becomes parent of test controllers scopes.
Check the console logs to see this structure: http://codepen.io/anon/pen/tiGEy. (Watch $parent properties of objects)

Related

Get parent controller scope into directive using controller instead of link

I am using a 'LocalCtrl' controller for all functionality needed for directive, but how do i get scope of parent controller into the directive and scope from directive back to controller.
My directive is embedded in parent controller. I know how to use link function and isolate scope for two way binding between directive and controller. I'm not sure how to inherit parent scope by using the following structure.
<div ng-controller = "mainCtrl">
<my-directive></my-directive>
</div>
app.controller('mainCtrl',function ($scope) {
$scope.mainContent = "this is main content"
});
app.controller('LocalCtrl',function () {
var vm = this;
vm.content = "This is Header"
});
app.directive('mydirective',function () {
return{
controller:'LocalCtrl as local',
templateUrl: '<div>{{local.content}}</div>',
}
});
Directives in Angularjs has 3 scopes , as mentioned below
refer In which cases angular directive scope equals controller scope?
1 . By default , scope is false , which incase on change of the scope variable in your directive also changes the parents scope variable as it doesn't create a new scope.
app.directive('mydirective',function () {
return{
controller:'LocalCtrl as local',
templateUrl: '<div>{{local.content}}</div>',
}
});
scope:true , With this it will create a new child scope in child directive , which inherits prototypically from the parent scope or parents controller scope
app.directive('mydirective',function () {
return{
scope:true,
controller:'LocalCtrl as local',
templateUrl: '<div>{{local.content}}</div>',
}
});
3: scope:{} : isolate scope , which doesn't inherit from parent's scope (could create re-usable components/directives)
view
<div ng-controller = "mainCtrl ">
<my-directive content="mainContent" some-fn="someFn"></my-directive>
</div>
app.directive('mydirective',function () {
return{
scope:{
twoWayConent:'=content',// two way data binding
oneWayConent:'#conent', // one way binding
someFn:'&someFn' //function binding ( 2 way)
},
controller:'LocalCtrl as local',
templateUrl: '<div>{{local.content}}</div>',
}
});
4. using require: : if you have one directive in another directive ,In Directive Defination object (DDO) require can be used to access the parents directive controllers variables and functionalities in your child directive as below
view
<parent-directive>
<child-directive></child-directive>
</parent-directive>
app.directive('childDirective',function () {
return{
require:'^parentDirective' // can be array of parents directive too
link:function(scope,element,attrs,parentDirectiveController){
console.log(parentDirectiveController) // can access parent directive controllers instances
}
}
});

Multiple directives [ngController, ...] asking for new/isolated scope

I have the following (https://jsfiddle.net/hhgqup4f/5/):
<div parent parent-model="vm.model1" ng-controller="Controller as vm">
Parent
</div>
And the controller is:
app.controller("Controller", Controller);
function Controller() {
var vm = this;
vm.model1 = "My Model 1";
vm.model2 = "My Model 2";
}
And then I have a directive as follows:
app.directive("parent", parent);
function parent() {
var parent = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
model: "="
}
};
return parent;
function controller($scope) {
console.log($scope.model);
}
}
With parent-model="vm.model1" I am trying to say which property from the controller should be used by the directive. But when I do console.log($scope.model) in the directive I get the error:
"Error: [$compile:multidir] Multiple directives [ngController, parent (module: app)] asking for new/isolated scope on: <div parent="" parent-model="vm.model1" ng-controller="Controller as vm">
How to solve this?
The error ...
"Error: [$compile:multidir] Multiple directives [ngController, parent (module: app)] asking for new/isolated scope on: <div parent="" parent-model="vm.model1" ng-controller="Controller as vm">
... is quite illustrative as AngularJS doesn't allow multiple directives (on the same DOM level) to create their own isolate scopes.
According to the documentation, this restriction is imposed in order to prevent collision or unsupported configuration of the $scope objects.
Normally a directive is supplied with an isolate scope with an intention towards componentization or reuse of some logic/action attached to the DOM.
Therefore, it makes sense that two reusable components cannot be merged together to produce a combined effect (at least in AngularJS).
Solution
Change your directive usage such that it is supplied with the required properties from its immediate parent (which in this case is the ngController directive).
<div ng-controller="Controller as vm">
<div parent parent-model="vm.model1"></div>
</div>
Similarly, you can access the passed properties to the isolate scope of the directive in their normalized format:
app.directive('parent', function(){
return {
scope: {
parentModel: '=' // property passed from the parent scope
},
controller: function($scope){
console.log($scope.parentModel);
}
};
});
Demo
Directive to Directive Communication
Two or more directives with isolate scopes, as mentioned earlier, can't be used on the same DOM element. However, it is possible for one of the directives to have an isolated scope. Other directives, in this case, can communicate if required by requireing its controller as such:
<div dir-isolate dir-sibling></div>
...
.directive('dirIsolate', function(){
return {
scope: {},
controller: function(){
this.askSomething = function(){
return 'Did you ask for something?';
};
}
};
})
.directive('dirSibling', function(){
return {
require: 'dirIsolate', // here is the trick
link: function(scope, iElement, attrs, dirSiblingCtrl){ // required controller passed to the link function as fourth argument
console.log(dirSiblingCtrl.askSomething());
}
};
});

Controller $scope and <form> in TranscludeScope

I have a directive that has a transclude. The transcluded content is a form which has a submit option calling a method in the (parent) controller. This method is called from the transcluded scope so any variables I access in the controller I can access via this.variable as this reflects the callers (current) scope. However, $scope.variable is undefined as $scope is that of the parent scope, which is the scope of the controller.
Should I indeed use this to access the forms values or is there a convention that I should implement to access via $scope in the (parent) controller?
Disclaimer: I know I'm not using controllerAs functionality, and right now I am not able to change that for the project.
Plunker to demonstrate the issue:
http://plnkr.co/edit/TdgZFIRNbUcHNfe3a7uO?p=preview
As you can see the value inside the directive updates when you change the value in the textbox. This is as expected as both refer to the transcluded scope. The {{name}} outside the directive does not change as this is the parent scope and not the transcluded scope. One-way traffic.
What is the proper solution? Use this.name or modify the directive so that $scope.name can be used outside the directive?
For those who like code over plunkers:
HTML:
<body ng-app="docsTransclusionExample">
<div ng-controller="Controller">
{{name}}
<my-dialog>
Check out the contents, {{name}}!
<form>
<input ng-model="name">
</form>
</my-dialog>
</div>
</body>
Directive:
(function(angular) {
'use strict';
angular.module('docsTransclusionExample', [])
.controller('Controller', function($scope) {
$scope.name = 'Tobias';
})
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html'
};
});
})(window.angular);
Directive template:
<div class="alert" ng-transclude></div>
This is related to "The Dot Rule" in angular.
This means, that when you have scope heirarchy, and a child scope is trying to modify a parent scope, it's creating it's own scope.
When you use data objects, you access members "with a dot", and so you don;t change the scope directly, and not creating a new child scope
In your case
$scope.name = 'Tobias';
shuold change to
$scope.data = {name: 'Tobias'};
see plunker:
http://plnkr.co/edit/RqORVm8r4jph532dT6nY?p=preview
for further reading:
Why don't the AngularJS docs use a dot in the model directive?
https://www.youtube.com/watch?v=DTx23w4z6Kc
PS.
This is not related to directive transclusion (whitch is about building the DOM).
If you do: scope: {} in directive, you explicitally create a child scope.

AngularJS isolate scope how to add to correct parent scope?

I have a directive 'outerDirective', it has children that are directives, and they have children that are directives. How do I add the grandchildren directives scopes to the correct child directive scope? I hope that makes sense. Here is a fiddle showing the code. It looks like I need to communicate to a scope method in the child directive rather than the controller instance. http://jsfiddle.net/hcabnettek/DMhUL/
The html is something like this
<div class="my-outer-directive">
<div class="my-inner-directive" id="foo1">
<div class="my-inner-inner-directive"></div>
<div class="my-inner-inner-directive"></div>
</div>
<div class="my-inner-directive" id="foo2"></div>
<div class="my-inner-directive" id="foo3">
<div class="my-inner-inner-directive"></div>
<div class="my-inner-inner-directive"></div>
<div class="my-inner-inner-directive"></div>
<div class="my-inner-inner-directive"></div>
</div>
I'm expecting foo1 scope, to have a subscriptions property with 2 scopes
foo2 scope, subscription property with 0 scopes, and foo3 have a subscription property of 4 scopes. I believe they are all being added to singleton controller instance. How do I link the inner-inner directives to the appropriate scope. Something like nested accordions. Any help would be wonderful.
There are at least two ways for child directives to communicate with their parent:
Use the require property on the directive configuration object to gain access to the parent directives controller inside the child, like this:
.directive('parent', function() {
return {
controller: function() {
this.parentFn() {
//do stuff.
}
}
}
})
.directive('child', function() {
return {
require: '^parent',
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.parentFn(); //this will execute the parent function here
}
}
});
Or, pass a function to the child directive in the template like this
<child parent-fn=parentFn()></child>
.directive('child', function() {
return {
scope: {
parentFn: '&' // this function is now available on child's scope
}
}
});
This way its not the parent that gains direct access to its children, but instead the children that gains access to some of the parents methods, hope that helps.

AngularJS - Explain why transcluded scopes must be siblings of an isolate scope

In the Angular docs on directives, there is this paragraph:
However isolated scope creates a new problem: if a transcluded DOM is
a child of the widget isolated scope then it will not be able to bind
to anything. For this reason the transcluded scope is a child of the
original scope, before the widget created an isolated scope for its
local variables. This makes the transcluded and widget isolated scope
siblings.
Can someone please explain why "if a transcluded DOM is a child of the widget isolated scope then it will not be able to bind to anything"?
Imagine you have some markup like this:
<html ng-app="myApp">
<body>
<div ng-controller="myController">
<div ng-repeat="item in items">
<div my-widget>
{{item.name}}
</div>
</div>
</div>
</body>
</html>
That sets up a tree of scopes ($rootScope -> controller scope -> ng-repeat scope -> widget scope). Now say your controller has some things in it:
function myController($scope) {
$scope.items = [
{name: 'Stella Artois'},
{name: 'Red Stripe'}
];
}
You can read values from a scope any number of levels up, because they inherit from each other using prototypical inheritance. {{item}} doesn't exist in the widget scope, but it does in the parent ng-repeat scope, so it's found just fine.
If you use isolate scope, you get a brand new scope that doesn't inherit from anything. So if my-widget uses scope: {} for example, the scope tree looks more like:
$rootScope
└controller scope
└ng-repeat scope
widget scope
Then in the double curlies, "item" is unknown. Using transclusion, you can set the scope up as a sibling like so:
$rootScope
└controller scope
└ng-repeat scope
└widget contents
widget scope
Another subtlety that was mentioned in this talk is that if your transcluded content's scope is a child of the directive's, then the directive could clobber whatever variables the transcluded content was trying to reference in a parent scope. For example:
<body ng-controller="MainCtrl">
<my-directive>{{ name }}</my-directive>
</body>
JS:
app.controller("MainCtrl", function($scope) {
$scope.name = 'foo';
});
app.directive("myDirective", function() {
return {
scope: {},
transclude: true,
template: '<span ng-transclude></span>',
controller: function($scope) {
$scope.name = 'bar';
}
}
});
Transclusion ensures that {{name}} in the directive references 'foo' instead of 'bar'.

Resources