I have a directive nested within a directive. To satisfy our designers needs I require the contents to be direct children of the DOM node.
<div>
<my-directive style="color: blue;">
<p>Name: {{ ctl_a.fname }} {{ ctl_a.sname }}</p>
<p>External Test: {{ xternal }}</p>
<div>
<nested-directive incoming="ctl_a.a_counter"></nested-directive>
</div>
</my-directive>
</div>
What are my best options for getting this to load correctly? I.E "my-directive" can access ctl_a.fname, and "nested-directive" can access $scope.incoming, as the argument passed in by "ctl_a.a_counter".
Here is a plunk that demonstrates the problems I am running in to using $compile. If I use $compile, the nested directives execute twice. Once for the outer directive compile method, and once again for the manual one. Worse still the manual one is the only one that appears to render the contents correctly.
If I use ng-transclude then none of attributes passed to the inner directive work without prefixing with $$prevSibling or $parent because ng-transclude appears to create a new scope. This seems to be fundamentally wrong having to reference it like that.
Edit: Here is another plunk which forks the first one. This time i'm demonstrating ng-transclude and how I need to use $parent to access the controller for its directive.
Thanks.
I'm not sure if that's all you want to know, but you need to add terminal: true to your directive definition object. This prevents Angular from processing the HTML within the my-directive tags until you compile it yourself. Modified code from your plunker:
function myDirective($compile) {
var directive = {
compile: compile,
controller: controller,
controllerAs: 'ctl_a',
replace: true,
terminal:true, //<=======
restrict: 'E',
scope: {
data: '='
}
};
return directive;
It seems you are not using data property of my-directive. You can remove below code from my-directive.
scope:{
data: '='
}
Now you do not need to manually compile as below. So remove it also.
$compile(element.contents())(scope);
Working plunkr
Related
I know that a angular directive can be define in four ways:
'A' - only matches attribute name
'E' - only matches element name
'C' - only matches class name
'M' - only matches comment
For example a directive with the restiction "M":
angular.module('exampleApp', [])
.directive('myDirective', function() {
return {
restrict: 'M',
...
};
});
and declaring the directive in HTML
<!-- directive: my-directive -->
But why would anybody use the M restriction for a directive? I find this really strange. Because if i comment out code, i don't want it to run. So why is this a thing?
From the docs:
Best Practice: Prefer using directives via tag name and attributes
over comment and class names. Doing so generally makes it easier to
determine what directives a given element matches.
Best Practice: Comment directives were commonly used in places where the DOM API limits the ability to create directives that spanned multiple elements
(e.g. inside elements). AngularJS 1.2 introduces
ng-repeat-start and ng-repeat-end as a better solution to this
problem. Developers are encouraged to use this over custom comment
directives when possible.
It is not good practice to use it now.
According to this post it is used usually only for backwards compatibility and for passing markup validations.
An example of how to use it is below
(function() {
angular
.module('exampleApp', [])
.directive("comment", function() {
return {
restrict: 'M',
replace : true,
template : "<h1>Made by a comment directive!</h1>",
link: function(scope, element, attrs) {
console.log(attrs.comment);
}
};
})
})();
<!DOCTYPE html>
<html ng-app='exampleApp'>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
</head>
<body>
</body>
<body>
<!-- directive: comment comment argument string -->
</body>
</html>
Because if i comment out code, i don't want it to run.
This is not HTML you're talking about, it's an Angular convention/feature. The comment is still visible/accessible inside the DOM as a comment node. The HTML parser won't do anything with it, true. But Angular can still find and parse and act on it. So, yes, it's another way to declare a directive which doesn't have any side effect on the DOM. You have to very explicitly use the directive: ... syntax inside a comment; you probably won't be triggering this by accident with code you simply want to comment out.
I'm using controller as syntax. Inside this scope, I have a directive which transcludes content that access this controller. That controller appears to be inaccessible from within the ng-transclude.
DEMO: http://plnkr.co/edit/ZYPCym2WQV43wh4R4nwI?p=preview
Is there a restraint from using controller as from within transcluded content?
I think stuff is getting mixed up b/c of the way you're using ng-controller with a directive. Unfortunately, I can't explain why, but the way you were setting up the controller seemed funny to me :)
My thinking is, if you're putting ng-controller on an element that is a directive, you might as well make it a "directive controller" like this:
function transcludeDirective() {
return {
restrict: 'E',
transclude:true,
scope: false,
controller: DemoCtrl,
controllerAs: 'DemoCtrlVM',
template: '<div>'+
'<p>First name: <b>{{ DemoCtrlVM.first_name }}</b></p>'+
'<ng-transclude></ng-transclude>'+
'</div>'
}
}
Be sure to remove the ng-controller="DemoCtrl as DemoCtrlVM" from the HTML. If you do this, it works as expected.
This might not be what you want. Doing it your way, you could use a different controller with the directive, and doing it my way the controller is coupled to the directive...
I can't seem to get this done: I have HTML that is compiled from a ng-repeat, and I'd like to compile the result of that as well. How would I go about that?
I have a dataset containing chunks of text, that have been given a display type. This type is set as the span class. Most types are just triggering CSS rules (for example, comment-style boxes, see screenshot), but others should invoke a directive.
For example, the chunk containing 'named Nicodemus, ' is of type-hidden. I have a directive that collapses the chunk and inserts a little button to expand it.
Code:
<span class="chunk type-{{chunk.type}}" ng-repeat="chunk in verse.chunks">{{chunk.text}}</span>
Results in something like
<span class="chunk type-hidden">named Nicodemus, </span>
If the second would be my source html, it would compile the typeHidden directive just fine. I guess I need to find a way to make angular compile a second time. I can't seem to get it done using $compile (though I guess I don't really understand how that works).
Hope you can help!
Thanks in advance!
Here's a plunker to show how you can get a directive to compile an element and then again.
The code for the lazy:
angular
.module('App')
.directive('compileTwice', compileTwiceFactory);
function compileTwiceFactory($compile) {
return {
restrict: 'AE', // Whatever you want
terminal: true, // Angular should not keep compiling the element
// by itself after encountering this directive!
compile: compile, // Instead, we tell Angular how to compile the rest of the element
priority: 1001, // This directive should get compiled before the others, obviously
};
function compile(element, attrs) {
element.removeAttr('compile-twice');
element.removeAttr('data-compile-twice');
return function postLink(scope, _element, _attrs) {
var compiledTwice = $compile($compile(_element)(scope)[0])(scope)[0];
// do something with compiledTwice
};
}
}
edit: And obviously you can generalize that to compile an arbitrary number of times that you could specify like this:
<div compile-n-times="420"></div>
edit: The plunker doesn't seem to work under Firefox?
I've actually been able to fix this with a workaround. Not as elegant, but it works if I nest my directive within the ngrepeat and hardcode the name, making it visible using ng-if.
<!-- special type hidden -->
<span ng-if="chunk.type=='hidden'">
<span class="type-hidden">
{{chunk.text}}
</span>
</span>
I'm learning how to create directives because they seem very useful and I thought it would be a good use for a top navigation bar. I'm not sure if I'm misunderstanding how they should be used, missed something small along the way or something entirely different.
The templateUrl isn't loading and looking through other posts and the docs, I can't see what went wrong.
Here is the directive
.directive('stNavDir', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: 'partials/TopNav.html',
scope: {
siteName: "=",
},
link: function(scope, element, attrbiutes) {
element.addClass('topBar');
}
}
Using it in index.html
<body>
<st-NavDir site-name={{siteName}}>{{title}}</st-NavDir>
TopNav.html
<div>
<button>Menu</button>
</br>
<div >
This will hold the navigation with a menu button, title of current location in app, and maybe other things
</div>
</div>
So it only shows the value of {{title}} and, looking in the console, there are no errors and it doesn't seem to even load TopNav.html.
If I'm using it completely wrong or there's a better way, I'm all ears as well. But this seemed like a good place to try out using a directive. I can load it fine using ng-include but I wanted to try this way and see if it would be more effective.
I'm also having trouble getting the style to take but that may be caused by this initial problem.
Change this line
<st-NavDir site-name={{siteName}}>{{title}}</st-NavDir>
to
<st-nav-dir site-name={{siteName}}>{{title}}</st-nav-dir>
Camel-case should be converted to snake-case.
st-nav-dir
in html may help.
stNavDir is the corresponding directive definition name.
Here is an interesting article:
Naming a directive
I am relatively new to AngularJS.
I have a series of DIVs in a partial view. Each of the DIVs has a unique ID. I want to show / hide these DIVs based on a scope value (that matches one of the unique ID).
I can successfully write out the scope value in the view using something like {{showdivwithid}}
What would be the cleanest way to hide all the sibling divs that dont have an ID of {{showdivwithid}}
I think you are approaching the problem with a jQuery mindset.
Easiest solution is to not use the id of each div and use ngIf.
<div ng-if="showdivwithid==='firstDiv'">content here</div>
<div ng-if="showdivwithid==='secondDiv'">content here</div>
<div ng-if="showdivwithid==='thirdDiv'">content here</div>
If you don't mind the other elements to appear in the DOM, you can replace ng-if with ng-show.
Alternatively use a little directive like this:
app.directive("keepIfId", function(){
return {
restrict: 'A',
transclude: true,
scope: {},
template: '<div ng-transclude></div>',
link: function (scope, element, atts) {
if(atts.id != atts.keepIfId){
element.remove();
}
}
};
});
HTML
<div id="el1" keep-if-id="{{showdivwithid}}">content here</div>
<div id="el2" keep-if-id="{{showdivwithid}}">content here</div>
<div id="el3" keep-if-id="{{showdivwithid}}">content here</div>
First, I want to echo #david004's answer, this is almost certainly not the correct way to solve an AngularJS problem. You can think of it this way: you are trying to make decisions on what to show based on something in the view (the id of an element), rather than the model, as Angular encourages as an MVC framework.
However, if you disagree and believe you have a legitimate use case for this functionality, then there is a way to do this that will work even if you change the id that you wish to view. The limitation with #david004's approach is that unless showdivwithid is set by the time the directive's link function runs, it won't work. And if the property on the scope changes later, the DOM will not update at all correctly.
So here is a similar but different directive approach that will give you conditional hiding of an element based on its id, and will update if the keep-if-id attribute value changes:
app.directive("keepIfId", function(){
return {
restrict: 'A',
transclude: true,
scope: {
keepIfId: '#'
},
template: '<div ng-transclude ng-if="idMatches"></div>',
link: function (scope, element, atts) {
scope.idMatches = false;
scope.$watch('keepIfId', function (id) {
scope.idMatches = atts.id === id;
});
}
};
});
Here is the Plunkr to see it in action.
Update: Why your directives aren't working
As mentioned in the comments on #david004's answer, you are definitely doing things in the wrong way (for AngularJS) by trying to create your article markup in blog.js using jQuery. You should instead be querying for the XML data in BlogController and populating a property on the scope with the results (in JSON/JS format) as an array. Then you use ng-repeat in your markup to repeat the markup for each item in the array.
However, if you must just "get it working", and with full knowledge that you are doing a hacky thing, and that the people who have to maintain your code may hate you for it, then know the following: AngularJS directives do not work until the markup is compiled (using the $compile service).
Compilation happens automatically for you if you use AngularJS the expected, correct way. For example, when using ng-view, after it loads the HTML for the view, it compiles it.
But since you are going "behind Angular's back" and adding DOM without telling it, it has no idea it needs to compile your new markup.
However, you can tell it to do so in your jQuery code (again, if you must).
First, get a reference to the $compile service from the AngularJS dependency injector, $injector:
var $compile = angular.element(document.body).injector().get('$compile');
Next, get the correct scope for the place in the DOM where you are adding these nodes:
var scope = angular.element('.blog-main').scope();
Finally, call $compile for each item, passing in the item markup and the scope:
var compiledNode = $compile(itm)(scope);
This gives you back a compiled node that you should be able to insert into the DOM correctly:
$('.blog-main').append(compiledNode);
Note: I am not 100% sure you can compile before inserting into the DOM like this.
So your final $.each() in blog.js should be something like:
var $compile = angular.element(document.body).injector().get('$compile'),
scope = angular.element('.blog-main').scope();
$.each(items, function(idx, itm) {
var compiledNode = $compile(itm)(scope);
$('.blog-main').append(compiledNode);
compiledNode.readmore();
});