Transcluded content not accessible by parent directive AngularJS - angularjs

My template uses two custom directives
<div my-parent-directive>
<div my-child-directive>
<input name="foo" />
</div>
</div>
The Parent Directive adds a class to the parent element of an input element with name=foo. That parent element is added by the child directive.
angular.module('myApp', [])
.directive('myParentDirective', function() {
return {
restrict: 'A',
transclude: true,
link: function(scope, element, attrs) {
angular.element("input[name=foo]").closest(".control-group").addClass("warning");
},
template: '<div ng-transclude></div>'
}
})
.directive('myChildDirective', function() {
return {
restrict: 'A',
transclude: true,
template: '<div class="control-group"><div ng-transclude></div><span class="help-inline"></span></div>'
}
});
However, I think the child directive hasn't parsed or done its job yet when the parent directive looks for the element. I'm providing the fiddle below as well:
http://jsfiddle.net/bsy8o1p4/
How can I make sure the child directive parses first before the parent?

You would need to wait for one digest cycle to do that. You should let one digest cycle to finish so that the child directive would have rendered. Also note that you cannot select elements by tagname with jqlite selector (# angular.element()). Use a $timeout to let the child directive render and then perform your action and make your selection relative by doing
element.find("input[name=foo]").closest(".control-group")
or you could even do
element.find(".control-group")`
against a more generic angular.element("input[name=foo]") if at all it works:
$timeout(function () {
element.find(".control-group").addClass("warning");
}, false);
Fiddle

Related

Assign random ngModel to directive

I'm trying to assign a random string to ngModel. But I can't even seem to assign a regular string to it. In the code below, I'm trying to change the ngModel to "new", but in chrome, it's still showing me that the ngModel is "placeholder". What am I doing wrong?
app.directive("page", function(){
return {
restrict: 'E',
replace: true,
template: '<div ng-model="placeholder"></div>',
controller: function ($scope, $element) {
$scope.placeholder = "new";
}
}});
The plain div tag was just an example. What I actually have is a content editable div that I've bound to a textarea with a contenteditable directive. I made a button to allow me to add as many of these directives as I want, but when I add this directive, I'd like a new ng-model for each one, because that's what I'm using to save the content of each content editable div to a file.
This is the full example in case it might help someone else. I put the addPage directive to a button, which, when clicked, appends a new content editable div (which I'm calling a page). There is one more directive that I didn't include (contenteditable) because I got it from the bottom of the docs over here
app.directive("addPage", function($compile){
return function(scope, element, attrs){
element.bind("click", function(){ angular.element(document.getElementById('container')).append($compile('<page></page>')(scope));
});
};
});
app.directive("page", function(){
return {
restrict: 'E',
replace: true,
// template: '<div class="page" contenteditable strip-br="true" ng-model="chapter"></div>',
template: function (elem, attr) {
var test="chapter.two"; //in reality, I will generate a random string to keep these divs unique
return '<div class="page" contenteditable strip-br="true" ng-model=' + test +'></div>'
}
}});
In my controller, I have this object that is used to store all the ng-model content
$scope.chapter = {};
I think you are approaching this a bit backwards. Starting with the model, you'd need an array to keep the values from all of the inputs / contenteditables.
.controller("MainCtrl", function($scope){
$scope.data = [{v: ""}]; // first element
$scope.addContentEditable = function(){
$scope.data.push({v: ""});
};
})
Then, you can bind to each element of that array easily, and add elements at will:
<button ng-click="addContentEditable()">Add</button>
<div ng-repeat="item in data" ng-model="item.v" contenteditable></div>
I'm not sure exactly how the page directive needs to be used here, but one way or another, the approach is the same as above.
If you just want to pass a string in you could use attributes and template(fn)
HTML
<div page="new">
Directive
app.directive("page", function () {
return {
restrict: 'E',
replace: true,
template: function (elem, attr) {
return '<div ng-model="' + attr.page + '"></div>';
}
}
});

why does the mouse click in parent fires twice when a directive is injected during compile

I have a directive which needs to plug in an additional directive depending
on some model value. I am doing that in the [edit] pre-link phase of the directive.
The parent directive sets up a host of
methods and data which the child uses. Therefore I have child directive with
scope:true.
On the outer (or, parent) directive there is a click handler method. When I
click this, it gets fired twice. I want to know why and how. Presently the
only way I know how to stop that is by calling event::stopImmediatePropagation()
but I have a suspicion I am doing something wrong here.
template usage
<dashboard-box data-box-type="column with no heading">
<div class="card">
<div
ng-click="show($event)"
class="box-title item item-divider ng-binding is-shown"
ng-class="{'is-shown':showMe}">
<span class="box-title-string ng-binding">A/R V1</span>
</div>
<div class="box-content item item-text-wrap" ng-show="showMe">
<!-- plug appropriate dashbox here - in dashboard-box compile -->
<dashbox-column-with-no-heading>
<div>
SOME DATA...
</div>
</dashbox-column-with-no-heading>
</div>
</div>
</dashboard-box>
In the directive for dashboard-box:
scope: {
boxType: "#"
},
pre: function preLink(scope, iElement, iAttrs, controller) {
// some crazy hardcoding - because find by tagname...replaceWith throws in ionic
var parent_div = angular.element(angular.element(iElement.children()[0]).children()[1]);
if (!parent_div) {
return; // programmer error in template??
}
var html_tag_name = d_table_type_to_directive_name.xl[iAttrs.boxType];
parent_div.append(angular.element( "<" + html_tag_name + ">" ));
$compile(iElement.contents())(scope); // angular idiom
}
In the controller for dashboard-box:
$scope.show = function($e){
$log.log("dashboard box show(), was=", $scope.showMe, $e);
if ($e) $e.stopImmediatePropagation(); // <<<<<<<<<<< without this, double-hits!!
$scope.showMe = ! $scope.showMe;
// etc
};
In the directive for dashbox-column-with-row-heading:
restrict: "E",
scope: true,
templateUrl: "dashbox-column-with-row-heading.tpl.html"
controller: function(){
// specialized UI for this directive
}
I am using ionicframework rc-1.0.0 and angularjs 1.3.13.
What is happening here is that you are double-compiling/linking the ng-click directive: 1) first time Angular does that in its compilation phase - it goes over the DOM and compiles directives, first dashboardBox, then its children, together with ngClick), and 2) - when you compile with $compile(element.contents())(scope).
Here's a reduced example - demo - that reproduces your problem:
<foo>
<button ng-click="doFoo()">do foo</button>
</foo>
and the foo directive is:
.directive("foo", function($compile) {
return {
scope: true,
link: {
pre: function(scope, element, attrs, ctrls, transclude) {
$compile(element.contents())(scope); // second compilation
scope.doFoo = function() {
console.log("foo called"); // will be called twice
};
}
}
};
});
What you need to do instead is to transclude the content. With transclusion, Angular compiles the content at the time that it compiles the directive, and makes the content available via the transclude function.
So, instead of using $compile again, just use the already-compiled - but not yet linked (until you tell it what it should be linked to) - contents of the directive.
With the foo example below, this would look like so:
.directive("foo", function($compile) {
return {
scope: true,
transclude: true,
link: {
pre: function(scope, element, attrs, ctrls, transclude) {
transclude(scope, function(clone, transcludedScope){
var newEl = createNewElementDynamically();
$compile(newEl)(transcludedScope); // compile just the newly added content
// clone is the compiled-and-now-linked content of your directive's element
element.append(newEl);
element.append(clone);
});
scope.doFoo = function() {
console.log("foo called");
};
}
}
};
});

Why does my nested directive disconnect from its ngModel as soon as $setViewValue() is called?

Plunker here.
I have a directive ("child") nested inside another directive ("parent"). It requires ngModel, and ngModelCtrl.$modelValue is shown and kept up-to-date just fine in its template. That is, until I call ngModelCtrl.$setViewValue().
So here is the HTML initialising the directives:
<div parent>
<div child ng-model="content">Some</div>
</div>
And here are the directives:
angular.module('form-example2', [])
.controller('MainCtrl', function($scope){
$scope.content = 'Hi';
})
.directive('parent', function() {
return {
transclude: true,
template: '<div ng-transclude></div>',
controller: function(){
},
scope: {}
};
})
.directive('child', function() {
return {
require: ['ngModel', '^parent'],
transclude: true,
template: '<div>Model: {{model.$modelValue}} (<a style="text-decoration: underline; cursor: pointer;" ng-click="alter()">Alter</a>)<br />Contents: <div style="background: grey" ng-transclude></div></div>',
scope: {},
link: function(scope, elm, attrs, ctrl) {
var ngModelCtrl = ctrl[0];
var parentCtrl = ctrl[1];
scope.model = ngModelCtrl;
// view -> model
scope.alter = function(){
ngModelCtrl.$setViewValue('Hi2');
}
// model -> view
// load init value from DOM
}
};
});
When the model (i.e. content) changes, this change can be seen inside the child directive. When you click the "Alter" link (which triggers a call of $setViewValue()), the model's value should become "Hi2". This is correctly displayed inside the child directive, but not in the model outside the directive. Furthermore, when I now update the model outside the directive, it is no longer updated inside the directive.
How come?
The directives ended up being just fine; the only problem was that the passed model should be an object property. Hence, the directives work if the following modifications are made to the calling code (Plunker):
In the controller, instead of $scope.content = 'Hi';:
$scope.content = {
value: 'Hi'
};
In the template, replace all references to content with content.value:
<input ng-model="content.value" type="text" />
<div parent>
<div child ng-model="content.value">Some</div>
</div>
<pre>model = {{content.value}}</pre>
The reason this works, roughly, is that when Angular passes the reference to the model to the transcluded scope of the parent directive (i.e. the one the child is in), this is only a reference when it refers to an object property - otherwise it is a copy, which Angular cannot watch for changes.
#Josep's answer helped greatly so, even though it did not provide the actual solution, if you're reading this and it's useful, give it a vote :)

how to communicate from one directive to another directive

I have tow directives one is for ng-grid another is for pagination when I click page numbers in one directive ng-grid directive should be changed according to that, can I have any idea on that.
There are many ways to achieve it:
For example:
First solution
You can share data between directives:
<directive-one attribute="value" other-attribute="value2" shared-variable="yourData">
<directive-two shared-variable="yourData">
And set $watch inside first directive on that value
scope.$watch('yourData',function(newVal,oldVal){ //your logic called after change });
Second solution
You can use events:
app.directive('first',function(){
return{
restrict: 'E',
template: 'Im first directive!',
scope: true,
link:function(scope,elem,attrs){
scope.$on('event',function(event,args){
alert(args);
});
}
}
});
app.directive('second',function($rootScope){
return{
restrict: 'E',
template: 'Im second directive! <button ng-click="click()">click me!</button>',
scope: true,
link:function(scope,elem,attrs){
scope.click = function(){
$rootScope.$broadcast('event','hello!');
};
}
}
});
Event is sent by $rootScope.$broadcast('event','hello!');. It means event is sent by your root scope downwards to child scopes. http://jsfiddle.net/aartek/fJs69/

Illegal use of ngTransclude directive in the template

I have two directive
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: 'element',
compile: function (element, attr, linker) {
return function (scope, element, attr) {
var parent = element.parent();
linker(scope, function (clone) {
parent.prepend($compile( clone.children()[0])(scope));//cause error.
// parent.prepend(clone);// This line remove the error but i want to access the children in my real app.
});
};
}
}
});
app.directive('panel', function ($compile) {
return {
restrict: "E",
replace: true,
transclude: true,
template: "<div ng-transclude ></div>",
link: function (scope, elem, attrs) {
}
}
});
And this is my view :
<panel1>
<panel>
<input type="text" ng-model="firstName" />
</panel>
</panel1>
Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <div class="ng-scope" ng-transclude="">
I know that panel1 is not a practical directive. But in my real application I encounter this issue too.
I see some explanation on http://docs.angularjs.org/error/ngTransclude:orphan. But I don't know why I have this error here and how to resolve it.
EDIT
I have created a jsfiddle page. Thank you in advance.
EDIT
In my real app panel1 does something like this:
<panel1>
<input type="text>
<input type="text>
<!--other elements or directive-->
</panel1>
result =>
<div>
<div class="x"><input type="text></div>
<div class="x"><input type="text></div>
<!--other elements or directive wrapped in div -->
</div>
The reason is when the DOM is finished loading, angular will traverse though the DOM and transform all directives into its template before calling the compile and link function.
It means that when you call $compile(clone.children()[0])(scope), the clone.children()[0] which is your <panel> in this case is already transformed by angular.
clone.children() already becomes:
<div ng-transclude="">fsafsafasdf</div>
(the panel element has been removed and replaced).
It's the same with you're compiling a normal div with ng-transclude. When you compile a normal div with ng-transclude, angular throws exception as it says in the docs:
This error often occurs when you have forgotten to set transclude:
true in some directive definition, and then used ngTransclude in the
directive's template.
DEMO (check console to see output)
Even when you set replace:false to retain your <panel>, sometimes you will see the transformed element like this:
<panel class="ng-scope"><div ng-transclude=""><div ng-transclude="" class="ng-scope"><div ng-transclude="" class="ng-scope">fsafsafasdf</div></div></div></panel>
which is also problematic because the ng-transclude is duplicated
DEMO
To avoid conflicting with angular compilation process, I recommend setting the inner html of <panel1> as template or templateUrl property
Your HTML:
<div data-ng-app="app">
<panel1>
</panel1>
</div>
Your JS:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>",
}
});
As you can see, this code is cleaner as we don't need to deal with transcluding the element manually.
DEMO
Updated with a solution to add elements dynamically without using template or templateUrl:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<div></div>",
link : function(scope,element){
var html = "<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>";
element.append(html);
$compile(element.contents())(scope);
}
}
});
DEMO
If you want to put it on html page, ensure do not compile it again:
DEMO
If you need to add a div per each children. Just use the out-of the box ng-transclude.
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div><div ng-transclude></div></div>" //you could adjust your template to add more nesting divs or remove
}
});
DEMO (you may need to adjust the template to your needs, remove div or add more divs)
Solution based on OP's updated question:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div ng-transclude></div>",
link: function (scope, elem, attrs) {
elem.children().wrap("<div>"); //Don't need to use compile here.
//Just wrap the children in a div, you could adjust this logic to add class to div depending on your children
}
}
});
DEMO
You are doing a few things wrong in your code. I'll try to list them:
Firstly, since you are using angular 1.2.6 you should no longer use the transclude (your linker function) as a parameter to the compile function. This has been deprecated and should now be passed in as the 5th parameter to your link function:
compile: function (element, attr) {
return function (scope, element, attr, ctrl, linker) {
....};
This is not causing the particular problem you are seeing, but it's a good practice to stop using the deprecated syntax.
The real problem is in how you apply your transclude function in the panel1 directive:
parent.prepend($compile(clone.children()[0])(scope));
Before I go into what's wrong let's quickly review how transclude works.
Whenever a directive uses transclusion, the transcluded content is removed from the dom. But it's compiled contents are acessible through a function passed in as the 5th parameter of your link function (commonly referred to as the transclude function).
The key is that the content is compiled. This means you should not call $compile on the dom passed in to your transclude.
Furthermore, when you are trying to insert your transcluded DOM you are going to the parent and trying to add it there. Typically directives should limit their dom manipulation to their own element and below, and not try to modify parent dom. This can greatly confuse angular which traverses the DOM in order and hierarchically.
Judging from what your are trying to do, the easier way to accomplish it is to use transclude: true instead of transclude: 'element'. Let's explain the difference:
transclude: 'element' will remove the element itself from the DOM and give you back the whole element back when you call the transclude function.
transclude: true will just remove the children of the element from the dom, and give you the children back when you call your transclude.
Since it seems you care only about the children, you should use transclude true (instead of getting the children() from your clone). Then you can simply replace the element with it's children (therefore not going up and messing with the parent dom).
Finally, it is not good practice to override the transcluded function's scope unless you have good reason to do so (generally transcluded content should keep it's original scope). So I would avoid passing in the scope when you call your linker().
Your final simplified directive should look something like:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.replaceWith(clone);
});
}
}
});
Ignore what was said in the previous answer about replace: true and transclude: true. That is not how things work, and your panel directive is fine and should work as expected as long as you fix your panel1 directive.
Here is a js-fiddle of the corrections I made hopefully it works as you expect.
http://jsfiddle.net/77Spt/3/
EDIT:
It was asked if you can wrap the transcluded content in a div. The easiest way is to simply use a template like you do in your other directive (the id in the template is just so you can see it in the html, it serves no other purpose):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv' ng-transclude></div>"
}
});
Or if you want to use the transclude function (my personal preference):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv'></div>",
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.append(clone);
});
}
}
});
The reason I prefer this syntax is that ng-transclude is a simple and dumb directive that is easily confused. Although it's simple in this situation, manually adding the dom exactly where you want is the fail-safe way to do it.
Here's the fiddle for it:
http://jsfiddle.net/77Spt/6/
I got this because I had directiveChild nested in directiveParent as a result of transclude.
The trick was that directiveChild was accidentally using the same templateUrl as directiveParent.

Resources