Controller $scope and <form> in TranscludeScope - angularjs

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.

Related

Along with Transclude element, can i pass its scope too to a directive?

Moment i felt i have understood enough about Transclude i came across this statement :
Transclude allows us to pass in an entire template, including its scope, to a directive.
Doing so gives us the opportunity to pass in arbitrary content and arbitrary scope to a directive.
Does this mean, if there is a scope attached to Transclude element and it can be passed on to the directive ? If that's true then am not able to access that scope property inside directive template.
Let me take couple of steps back and explain with code about what am trying to do :
JSFiddle Link
My directive is directive-box and transclude: true is defined in Directive Definition Object(DDO).
Now there is a Child Div, which is the element to be Transcluded
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
and it has controller TransCtrl attached to it.
Now am trying to access $scope.name property which is part of TransCtrl from directive level after defining this in DDO :
scope: {
title: '#directiveTitle',
name: '='
}
Is this possible ?
This is more like a Parent scope trying to access Child scope property, is this permitted in JavaScript Protoypical inheritance ? Or is there something else i need to know ??
If this is not possible what does first statement mean ?
Transclude allows us to pass in an entire template, including its scope, to a directive.
UPDATE 1 :
My primary concern is Controller should remain with Transclude element, still we should be able to pass its (Transclude element) scope to Directive and then Directive should be able to consume that scope i.e., name from TransCtrl controller .
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
Above line of code should remain as is.
I may be completely wrong with my question but please let me if this can be accomplished.
The problem seems to be with the way the controller is defined within the ng-transcluded html.
I have made it clearer by using
the bindToController construct
using a controller at the directive level
Refer this fiddle for a working example.
controllerAs: "TransCtrl",
bindToController: true
And your statement, 'Parent scope trying to access Child scope property' is incorrect right? Since we are trying to use the parent scope property, i.e. name from within the child (ng-transcluded content), which is possible with protypical inheritance, and not the other way around.
Does this answer your question: https://jsfiddle.net/marssfa4/4/?
In it I have created a new controller on the outside (effectively replacing the functionality of your rootScope for inside the directive) and I made the directive's controller be set inside your controller template.
The long and short of it is though that you can see that it is possible to transclude html along with its scope even into a directive with its own scope.
The html:
<div ng-app='myApp' ng-controller="OutsideScope">
<h1>{{externalWorld}}</h1>
<div directive-box directive-title='{{directiveWorld}}' name='name'>
<div>Inside Transclude Scope : {{name}}</div>
</div>
</div>
JS (includes Update 1):
angular.module('myApp', [])
.directive('directiveBox', function() {
return {
restrict: 'EA',
scope: {
title: '#directiveTitle',
name: '='
},
transclude: true,
template: '<div ng-controller="TransCtrl">\
<h2 class="header">{{ title }}</h2>\
<div class="dirContent">Directive Element</div>\
<div>Outside Transclude Scope : {{name}}</div>\
<div class="content" ng-transclude></div>\
</div>'
}
})
.controller('TransCtrl', function($scope) {
$scope.name = 'Transclude World'
})
.controller('OutsideScope', function($scope) {
$scope.name = 'External World'
})
.run(function($rootScope) {
$rootScope.externalWorld = 'External World',
$rootScope.directiveWorld = 'Here comes directive'
});
UPDATE 1: JSFIDDLE
I restored the original scope declarations as the scope: false was a mistake.
If I understand your comment correctly you want to leave the controller on the element to be transcluded but still have the {{name}} within that element ignore its immediate controller and use as controller its parent (i.e. the directive's) scope.
The reason I placed the controller within the template directive is because that is the only way to limit the directive's scope on the directive and not its transcluded elements. If you are explicitly placing a controller on an element, then regardless of whether it is contained within a directive with another scope, its closest scope will override whatever scope has been declared on the directive. In other words, regardless of what the directive's scope is, the {{name}} in
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
will always be whatever $scope.name is in TransCtrl.

How to pass string and bool variables automatically to directive parent scope

I bind variable in a directive like this :
<path-filter-modal is-opened="filterModalIsOpened">
And in the directive I use '=' binding like this:
scope: {
isOpened: '='
}
When I change variable in directive, a parent scope contains own value.
How can I make that the parent scope contain the same value?
For objects it works well but not with strings and booleans.
Notice that I use controller that is defined in my directive in my directive to change values.
Because JavaScript is designed to be so.
Defining an isolate scope in the directive creates a new $scope object, which is a separate $scope object. Its only relationship with the parent scope is that: $isolateScope.$parent === $parentScope. It doesn't inherits from $parentScope prototypical.
When you assign some primitive type (string/boolean) to $scope.isOpened, actually JavaScript engine will create a new variable isOpened on $scope. It is totally not related to $parentScope.isOpened.
But now, Angular syncs the two variables for you implicitly. So binding primitive variables still makes two-way binding work well. Please check JSFiddle.
If you binds to some object type, the child scope and parent scope are referencing to the exactly the same copy of an object in the memory. Changing on the parent scope will change the child scope automatically. So two-way binding is always recommended to bind objects, not primitive types.
Check this JSFiddle. I bind a primitive and an object to the directive myDirective. Then modify them inside the link function:
scope.primitiveParam = 'primitive from directive';
// $parent.primitive and primitiveParam refer to different memory;
// Angular is responsible to sync them.
console.log(scope.$parent.primitive);
console.log(scope.primitiveParam);
scope.objectParam.name = 'object from directive';
// $parent.obj and objectParam refer to an identical object
console.log(scope.$parent.obj.name);
console.log(scope.objectParam.name);
console.log(scope.objectParam === scope.$parent.obj);
And the result is like:
primitive from parent
primitive from directive
object from directive
object from directive
For more details: Understanding Scopes (here are many intuitive images illustrating the concepts clearly)
RE: For objects it works well but not with strings and booleans
I think it's the usual case of prototypal inheritance problem. When the model come from object it works well, but if it come from non-objects there's a possibility that the ng-model is created on child scope.
To solve that problem, use modern approach, use Controller as approach. Or put the filterModelIsOpened in an object. The first approach is better.
<div ng-controller="SomeController as s">
<path-filter-modal is-opened="s.filterModalIsOpened">
</div>
function SomeController() { // no need to use $scope
this.filterModalIsOpened = false;
}
Or if you are using older version of Angular, you cannot use Controller as approach. Just create your own alias in the controller:
<div ng-controller="SomeController">
<path-filter-modal is-opened="s.filterModalIsOpened">
</div>
function SomeController($scope) {
$scope["s"] = this;
this.filterModalIsOpened = false;
}
Here's a good article explaining the prototypal inheritance: http://codetunnel.io/angularjs-controller-as-or-scope/
Here are the demo why you should always prefix your model, be they are object or primitive.
Not recommended. Live code demo: http://jsfiddle.net/hdks813z/1/
<div ng-app="App" ng-controller="Ctrl">
<div ng-if="true">
<input type="checkbox" ng-model="filterModalCanBeOpened"/>
<the-directive primitive-param="filterModalCanBeOpened"></the-directive>
</div>
<hr/>
<p>
The value below doesn't react to changes in primitive(non-object) property
that is created a copy on a directive(e.g., ng-repeat, ng-if) that creates
child scope
</p>
$scope.primitive: {{filterModalCanBeOpened}}
</div>
angular.module('App', [])
.directive('theDirective', function () {
return {
restrict: 'AE',
scope: {
primitiveParam: '='
},
template: '<div>primitiveParam from directive: {{ primitiveParam }}; </div>',
link: function (scope) {
}
};
})
.controller('Ctrl', ['$scope', function ($scope) {
$scope.filterModalCanBeOpened = true;
}]);
Recommended: Live code demo: http://jsfiddle.net/2rpv27kt/
<div ng-app="App" ng-controller="Ctrl as c">
<div ng-if="true">
<input type="checkbox" ng-model="c.filterModalCanBeOpened"/>
<the-directive primitive-param="c.filterModalCanBeOpened"></the-directive>
</div>
<hr/>
<p>
The value below react to changes in primitive(non-object) property that is
addressed directly by its alias c, creating child scope on it would be
impossible. So the primitive below react to changes on
the c's filterModalCanBeOpened.
</p>
c.primitive: {{c.filterModalCanBeOpened}}
</div>
angular.module('App', [])
.directive('theDirective', function () {
return {
restrict: 'AE',
scope: {
primitiveParam: '='
},
template: '<div>primitiveParam from directive: {{ primitiveParam }}; </div>',
link: function (scope) {
}
};
})
.controller('Ctrl', [function () {
this.filterModalCanBeOpened = true;
}]);

Angular: What is a good way to hide undefined attributes that exist in isolated scopes?

I wanted to rewrite this fiddle as it no longer worked in angular 1.2.1. From this exercise, I learned that a template is apparently always needed now in the isolated scopes.
somewhere in the directive:
template: '<p>myAttr1 = {{myAttr1}} // Passed by my-attr1<br>
myAttr2 = {{myAttr2}} // Passed by my-alias-attr2 <br>
myAttr3 = {{myAttr3}} // From controller
</p>',
I was not able,however, to successfully add this to the template:
<p ng-show="myAttr4">myAttr4= {{myAttr4}} // Hidden and missing from attrs</p>
What is a good way to hide undefined attributes that are defined on the isolated scope but not given a value from the dom?
my humble fiddle
EDIT: I use a directive called my-d1 to encapsulate the bootstrap tags. I use my-d2 to demo how to use the # in isolated scopes.
Working version merged with Sly's suggestions
I ran into the same template issue in Angular 1.2.0, see the first entry in the 1.2.0 breaking changes:
Child elements that are defined either in the application template or in some other directives template do not get the isolate scope. In theory, nobody should rely on this behavior, as it is very rare - in most cases the isolate directive has a template.
I'm not exactly sure what the issue is that you are encountering - it might be some incorrect markup or you are misnaming the scope variables listed in your isolate scope.
Using ng-show will correctly hide the element if the attribute has not been passed in.
i.e. your example here is correct: <p ng-show="myAttr4">myAttr4= {{myAttr4}}</p>
Updated version of your Fiddle: http://jsfiddle.net/Sly_cardinal/6paHM/1/
HTML:
<div ng-app='app'>
<div class="dir" my-directive my-attr1="value one" my-attr3='value three'>
</div>
<div class="dir" my-directive my-attr1="value one" my-attr3='value three' my-attr4='value four'>
</div>
</div>
JavaScript:
var app = angular.module('app', []);
app.directive('myDirective', function () {
return {
// can copy from $attrs into scope
scope: {
one: '#myAttr1',
two: '#myAttr2',
three: '#myAttr3'
},
controller: function ($scope, $element, $attrs) {
// can copy from $attrs to controller
$scope.four = $attrs.myAttr4 || 'Fourth value is missing';
},
template: '<p>myAttr1 = {{one}} // Passed by my-attr1</p> '+
'<p ng-show="two">myAttr2 = {{two}} // Passed by my-alias-attr2 </p>'+
'<p>myAttr3 = {{three}} // From controller</p>'+
'<p ng-show="four">myAttr4= {{four}} // Has a value and is shown</p>'
}
});

Angular Directive's template binding doesn't update

I have a directive set up here http://jsfiddle.net/screenm0nkey/8Cw4z/3 which has two bindings to the same scope property, but for some reason the binding in the directive's template property doesn't update when the model changes (after typing in the input).
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count">
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
}
};
});
But if I move the input position in the markup it works and both bindings update.
<input type="text" ng-model="count">
<test>
<h3>Inner {{count}}</h3>
</test>
http://jsfiddle.net/screenm0nkey/dCvZk/3
Can anyone explain why the position of the input containing the binding, would have an affect the bindings. I assumed that during the digest loop the watchers for both binding would be updated regardless of the position of the markup.
Many thanks
To me, this seems purely to be a scope issue. Lets take a look at the markup that is generated by both:
Not working:
<body ng-app="App" class="ng-scope">
<h1 class="ng-binding">Outer1 <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner 1</h3>
<input type="text" ng-model="count" class="ng-scope ng-pristine ng-valid">
</div>
</h1>
</body>
Working:
<body ng-app="App" class="ng-scope">
<input type="text" ng-model="count" class="ng-valid ng-dirty">
<h1 class="ng-binding">Outer <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner </h3>
</div>
</h1>
</body>
The ng-scope class is a useful marker for where Angular is declaring a new scope.
You can see by the markup that the in the working example both the count properties are enclosed in the scope that is attached to body. So, in this case, the directive scope is a child of the body scope (and therefore has access to it).
However, In the example that is not working, the Outer1 property is sitting outside of the scope that the input is in.
The Angular Scope documentation covers this well. The scopes are arranged in a hierarchy with child scopes having access to parent scopes (but not the other way around):
The application can have multiple scopes, because some directives
create new child scopes (refer to directive documentation to see which
directives create new scopes). When new scopes are created, they are
added as children of their parent scope. This creates a tree structure
which parallels the DOM where they're attached
Long story short - as others have said, this is a scope issue. Using the "ng-transclude" directive creates a new scope. When a new scope is created values from the old scope will be accessible in the new scope (hence the first replace) but after that only objects that are shared between the old/new scope will be updated. That is why using an object would work, but using a value will not.
In your case placing the input field inside of the ng-transclude causes this to only edit the value in that scope, not the value in the parent scope (which is where the count for the "test" directive is pulled from).
Incidentally, this can be an issue with repeaters (ng-repeat) as well as other directives. Its best to use a tool such as "Batarang" in order to find issues such as this. It allows you to look at what is in each scope and determine why the screen isn't showing the "correct" data. Hope that helps explain further!
Add ng-change to input , it should work. The problem is that controller into directive doesn't know about count change.
JS
var App = angular.module('App', []);
App.directive('test', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer {{this.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
$scope.onChange = function(count){
$scope.count = count;
}
}
};
});
HTML
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count" ng-change="onChange(count)">
</test>
Demo Fiddle
The order matters because of the difference between creating a property on the scope versus actually using an object bound to the scope (especially when a transclude creates a new child scopr). Best practice is to use an object on the scope and bind properties to that object when scope issues can come into play with directives and transcludes.
If you change your code to this, it will work as you were expecting and order does not matter. Notice that I am creating a scope object and placing the count as a property on that object.
<test>
<h3>Inner {{data.count}}</h3>
<input type="text" ng-model="data.count"/>
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{data.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.data = {};
$scope.data.count = 1;
}
};
});
This is a great tutorial on this subject. Props to EggHead. https://egghead.io/lessons/angularjs-the-dot
It's a scoping issue.
$scope.count = 1; adds the property count to the scope that <test> is in. Let's call it parent scope.
ng-transclude creates a new scope, let's call it child scope. When <h3>Inner {{count}}</h3> is evaluated, the child scope has no property count so it's read from the parent scope.
<input type="text" ng-model="count"> binds the value of the input to the property count in the child scope. As soon as you enter something the property will be created if it's not there yet. From this point on <h3>Inner {{count}}</h3> gets its value from the child scope.
Scopes in angular are simple JavaScript objects and are connected to their parents via prototypes. So before you enter something the child scope looks something like
{
prototype: { // = parent scope
count: 1
}
}
When you change the value to, say, 5, the scope looks something like
{
count: 5,
prototype: { // = parent scope
count: 1
}
}
Because data binding does something like scope.count = 5.
Here's a work around
Change $scope.count to
$scope.helper = {
count: 1
}
and refactor the rest.
Check this video out for an explanation.
It seems that we cannot override this since ngTransclude will use $transclude function directly.
See: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
and: http://docs.angularjs.org/api/ng.$compile
transcludeFn - A transclude linking function pre-bound to the correct transclusion scope. The scope can be overridden by an optional first argument. This is the same as the $transclude parameter of directive controllers. function([scope], cloneLinkingFn).

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