Moving child directive from template to compile fn breaks scope connections - angularjs

I have nested directives, both creates new scopes. To preserve original content I decide to move child directive to compile function of parent directive. This move breaks connection between scopes, could someone explain me why? Thanks
jsfiddle: (remove template from parent directive to see the issue)
http://jsfiddle.net/luckylooke/uDkd3/9/
var myApp = angular.module('myApp', [])
.directive('parent', function directive() {
return {
compile: function(elm){
elm.append("</br> Appended <div child></div>");
},
template:"</br>From template <div child></div>",
scope: {},
controller: function ($scope) {
$scope.who = "parent";
$scope.message = "Text message";
//console.log($scope);
}
};
}).directive('child', function directive() {
return {
replace: true,
scope: {},
template: '<div class="child">child with {{$parent.message || "NO message!"}}</div>',
controller: function ($scope) {
$scope.who = "child";
//console.log($scope);
}
};
});

The Problem
As per this answer in Angularjs isolated scope for directives without own template:
[...] only elements in a template for the directive will be bound to
the isolated scope created by that directive. If you don't use a
template - content of the element, on which the directive is declared,
will bind as if isolated scope was not there.
Here is what your scope hierarchy looks like with a template:
... and here is what it looks like without one:
A Solution
Use transclusion to inject elements from your view into parent without using compile:
.directive('parent', function directive() {
return {
transclude: true,
template:"</br><div ng-transclude></div>From template <div child></div>",
scope: {},
controller: function ($scope) {
$scope.who = "parent";
$scope.message = "Text message";
}
};
})
Demo
Here the child directive is again an actual child of the parent directive. Notice too that the transcluded content appears in another scope which is a sibling of parent's isolate scope:

Related

AngularJS - Function declared in child directive called in the parent controller

Call a function on parent controller that is declared in is child directive.
Parent controller HTML
<div ng-click="testFunction()"></div>
Parent controller js
$scope.testFunction = function(){
$scope.functionFromChildDirective()
}
Child directive js
function TestDirective() {
return {
restrict: 'EA',
scope: {
},
templateUrl: '',
controller: function($scope) {
"ngInject";
$scope.functionFromChildDirective = function(){
console.log("TEST")
}
}
}
}
export default {
name: 'testDirective',
fn: TestDirective
};
Just delete the empty scope deceleration, by defining it you are creating a new isolate scope. If you don't declare the scope in the directive definition object it will just inherit the parents scope. However with this approach the child directive can only be used once (i.e can't be repeated) as each instance of will just overwrite the $scope.functionFromChildDirective property.
Use the ng-ref directive to bind the controller to a parent variable:
<test-directive ng-ref="testAPI">
</test-directive>
function TestDirective() {
return {
restrict: 'EA',
scope: {
},
templateUrl: '',
controller: function() {
this.testFn = function(){
console.log("TEST")
}
}
}
}
To invoke it:
<div ng-click="testAPI.testFn()"></div>
The ngRef directive tells AngularJS to assign the controller of a component (or a directive) to the given property in the current scope.
For more information, see AngularJS ng-ref Directive API Reference.

Get data back from angular directive

I've created a directive which I called my-tree, and I'm calling this directive from a view exemple-tree-view.html as following:
<my-tree ng-model="sampleTreeView.listNoeuds" ... />
this view's controller called sampleTreeView.
In my directive's link function I have a function that returns some data, which I affect to scope variable declared in the directive's controller, as following :
function linkFn(scope, element, attrs) {
//some code
scope.createNode = function ($event) {
var sel = $(element).jstree(true).create_node($($event.currentTarget)[0].closest('.jstree-node').id);
if (sel) {
$(element).jstree(true).edit(sel, '', function (node, success, cancelled) {
scope.treeActionsResult.createdNode = node;
});
}
};
//some code
}
My question is how can I get the scope.treeActionsResult.createdNode value in the sampleTreeView controller, since it's the controller for the exemple-tree-view.html where I call my directive.
You can use shared scope between the directive and controller by removing the scope property
like in this example:
MyApp.directive('studentDirective', function () {
return {
template: "{{student.name}} is {{student.age}} years old !!",
replace: true,
restrict: 'E',
controller: function ($scope) {
console.log($scope);
}
}
});
Still you have the $scope object, but in this case the scope object is shared with parent controller's scope.
You can read more about it fron the following link
Understanding Scope in AngularJs Custom Directive
If you don't create isolated scope for your directive then you can access directive scope values from your controller. like bellow
your controller and directive:
app.controller('MainCtrl', function($scope) {
$scope.value = 1;
});
app.directive('myTree', function() {
return {
restrict: 'AE',
link: function(scope, element, attrs) {
scope.values = {};
scope.values.price = 1234;
}
};
});
then use in your html like:
<body ng-controller="MainCtrl">
<p>value {{values.price}}</p>
<my-tree att="{{attValue}}"></my-tree>
</body>
here values.price shown from directive in MainCtrl

Unable to pass controller and directive scope into child directive using require

I am trying to pass the controller scope of parent controller and parent directive into a child directive but facing an error saying that the controller is not available. Here is a plunk for that
http://plnkr.co/edit/aahgOK9oFFjcP2y5VkVa?p=preview
HTML:
<div ng-controller="MainCtrl as mc">
<new-dir>
<data-customer-details customer="mc.customers[0]" logger="mc.logger()" version-age="{{mc.age}}"></data-customer-details>
</new-dir>
</div>
OK, so I tinkered with your plunker a bit. I couldn't get it working using Controller As...I had to change it over to $scope injection on the main controller. Then I created a new scope on newDir by setting scope: true.
You don't actually need to require the MainCtrl because these directives are automatically children of that scope anyway.
I changed your 'MainCtrl' to this:
angular.module('plunker').controller('MainCtrl', function ($scope) {
$scope.name = 'World';
$scope.customers = [{
"name": "angularjs 1.4",
"version": "1.4"
}, {
"name": "angularjs 1.3",
"version": "1.3"
}, {
"name": "angularjs 1.2",
"version": "1.2"
}];
$scope.age = 30;
$scope.logger = function() {
console.log('clicked');
}
$scope.ctrlScopeVariable = 'im in controller scope';
})
Minor change to newDir:
function newDir (){
return {
scope: true, //you need this
controller: function($scope){
$scope.val= 'someval';
console.log($scope.$parent.ctrlScopeVariable)
},
link: function(scope, el, attr, ctrl) {
console.log(scope.$parent.name)
}
}
}
And the last directive:
function CustomerDetails() {
var directive = {
scope: {
customer: '=',
logger: '&',
myNewAge: '#versionAge'
},
restrict: 'EA',
require: ['^newDir'],
controllerAs: 'cd',
templateUrl: 'customer-details.html',
link: linkFunction,
controller: function($scope){
console.log($scope.$parent.$parent.ctrlScopeVariable);
var cd = this;
cd.newval = 'new val';
}
};
function linkFunction(scope, elem, attributes, controllers, transclude) {
console.dir(controllers);
scope.fromMainCtrl = scope.$parent.$parent.ctrlScopeVariable
}
return directive;
}
The Plunker
I added a binding to the customer details template that passes in the $scope.ctrlScopeVariable from the main controller, so you can see the MainCtrl scope is accessible form the child directive.
In regards to require, the relevant documentation is here, I think:
If it is necessary to reference the controller or any functions bound
to the controller's scope in the template, you can use the option
controllerAs to specify the name of the controller as an alias. The
directive needs to define a scope for this configuration to be used.
This is particularly useful in the case when the directive is used as
a component.
Looking back at myPane's definition, notice the last argument in its
link function: tabsCtrl. When a directive requires a controller, it
receives that controller as the fourth argument of its link function.
Taking advantage of this, myPane can call the addPane function of
myTabs.
Essentially, you can use it to reference a parent controller on which you need to access some functions or something. Notably, it becomes available under whatever alias you give it as the fourth argument of your link function.
EDIT:
In this Plunker I added a function to the controller of newDir, required newDir in the CustomerDetail directive, and then called that function in the CustomerDetail link function:
CustomerDetails directive:
//some stuff
require: '^newDir',
//some stuff
link: function(scope, el, attr, newDirCtrl) {
console.log(newDirCtrl.doubleNum(100));
}
newDir controller:
controller: function($scope){
this.doubleNum = function(num) {
return num*2
}
// some stuff
}
First you need to declare a variable as callback function:
var MainCtrlFn = function() { .... }
Then, you can set it as parameter to angularJS:
angular.module('plunker').controller('MainCtrl', MainCtrlFn);

Why don't my nested directives work when the markup is in a single file?

I've been looking at isolateScope directives, to get a better understanding of how they interact with other nested isolateScope directives, so put together a plnkr to test a few things out.
http://plnkr.co/edit/7Tl7GbWIovDSmVeKKN26?p=preview
This worked as expected. As you can see each directive has it's own separate template.
I then decided to move the html out of each directive and into the main html file, but it's stopped working? I can see that the e1Ctrl is on the scope of the directive, but it doesn't appear to be available when the enclosed markup is processed.
http://plnkr.co/edit/33Zz1oO4q7BVFw0cMvYa?p=preview
Can someone please tell me why this is happening?
----------- UPDATE -----------
I've simplified the non-working plunker to clearly show the problem. The directive uses the controllerAs syntax and the e1Ctrl is clearly set on its $scope (see the console output).
http://plnkr.co/edit/g2U2XskJDwWKuK3gqips?p=preview
angular
.module('app', [])
.controller('AppCtrl', AppCtrl)
.directive('elementOne', elementOne)
.controller('E1Ctrl', E1Ctrl)
function AppCtrl() {
var vm = this;
vm.data = [
{
label: 'one'
},
{
label: 'two'
},
{
label: 'three'
},
{
label: 'four'
}
];
vm.callback = function() {
console.log('called app callback');
};
}
function elementOne() {
return {
restrict: 'E',
scope: {
data: '=',
handler: '&'
},
controller: 'E1Ctrl',
controllerAs: 'e1Ctrl',
bindToController: true
}
}
function E1Ctrl($scope) {
console.log('E1Ctrl', $scope);
var vm = this;
vm.click = function() {
vm.handler();
};
vm.callback = function() {
console.log('called e1 callback');
};
}
Mark up:
<body ng-app="app" ng-controller="AppCtrl as appCtrl">
<ul>
<div ng-repeat='item in appCtrl.data'>
<element-one data='item' handler='appCtrl.callback()'>
<button ng-click='e1Ctrl.click()'>e1: {{e1Ctrl.data.label}}</button>
</element-one>
</div>
</ul>
</body>
------ Transclusion solution -----
http://plnkr.co/edit/l3YvnKOYoNANteNXqRrA?p=preview
function elementOne() {
return {
restrict: 'E',
transclude: true,
scope: {
data: '=',
handler: '&'
},
controller: 'E1Ctrl',
link: function($scope, $element, $attr, ctrl, transclude) {
transclude($scope, function(clone){
$element.append(clone);
});
}
}
}
There's a difference in scope for HTML in the template of the directive and HTML that in a subtree of the directive. The former is evaluated in the context of the scope of the directive; the latter - in the scope of the View.
If the directive has an isolate scope - scope: {}, then the subtree doesn't see it. If it uses scope: true, then it creates a new child scope for the subtree which prototypically inherits from the View's scope.
Consider the following:
// isolate scope
app.directive("foo", function(){
return {
scope: {},
link: function(scope){
scope.name = "foo";
}
}
});
// child scope
app.directive("bar", function(){
return {
scope: true,
link: function(scope){
scope.name = "bar";
}
}
});
app.controller("Main", function($scope){
$scope.name = "main";
});
Here's how the View would render:
<body ng-controller="MainCtrl">
<pre>in main: {{name}} will render "main"</pre>
<foo>
<pre>in subtree of foo: {{name}} will render "main"</pre>
</foo>
<bar>
<pre>in subtree of bar: {{name}} will render "bar"</pre>
</bar>
</body>
In your case, the subtree is evaluated in the scope of the View - not the directive, and that is why it doesn't work as you expected.
plunker
EDIT:
In some cases it may makes sense to evaluate the subtree in the context of the isolate scope of the directive. I've seen this used with directives that allow templating. But be careful with this because the author of the Main View should not know (too much) about the inner workings of the directive (i.e. what is exposed in the inner scope). This would also be difficult to read because you would see variables that do not make sense in the outer scope.
To evaluate the subtree in the isolate scope of the directive, the directive needs to $compile the subtree and link it against its scope.
Here's a directive that allows the user to provide a template for each item in the list. The item variable is not defined in the main scope, and only makes sense in the context of the directive's isolate scope:
<list src="items">
<item-template>
{{item.a}} | {{item.b}}
</item-template>
</list>
The directive 'list' is below:
app.directive("list", function($compile){
return {
scope: {
src: "="
},
link: {
pre: function(scope, element){
var itemTemplate = element.find("item-template");
element.empty();
var template = angular.element('<div ng-repeat="item in src"></div>')
.append(itemTemplate.html());
element.append(template);
$compile(element.contents())(scope);
}
}
}
});
plunker 2

Directive - controller data binding in AngularJS

I'm struggling with this for hours now.
var testApp = angular.module('testApp', []);
testApp.directive('test', function() {
return {
restrict: 'E',
transclude: true,
template: '<div ng-transclude>Hello World</div>',
link: function(scope) {
}
}
});
testApp.controller('testCtrl', function ($scope) {
$scope.user = "";
});
Here's JSFiddle: http://jsfiddle.net/2bKPj/
Now, all I need is for an input embedded in directive to be able to reflect user model directly in testCtrl controller.
I'm confused on how this beast works since I taught that scopes are shared in this case, no?
ngTransclude creates a new child scope which protorypically inherits from it's parent scope.
When you use a primitive on the child scope it shadows the parent scope's variable.
It's been already said thousand times: use the dot notation!
controller:
testApp.controller('testCtrl', function ($scope) {
$scope.data = { user : "Hello World" };
});
html:
<input type="text" ng-model="data.user"/><br />
Directive model:<span>{{ data.user }}</span>
Check my other answers for description:
bound element inside ngIf does not update binding
Directives inside ng-include

Resources