Compile Angular directive from dynamic html - javascript

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>

Related

AngularJS custom directive loosing root properties

I have created a sidebar custom directive. It's working properly as it loads on where it should. What isn't working properly are the tags. Their supposed behavior is that of a drowpdown, where when clicked they show their inner elements. It works properly when the code is pasted directly but not when the directive is called with the code inside the other html file. I took 2 screenshots to show the difference between using a class="page-sidebar" inside the file that contains the html code of the directive and using it on the "root" file:
It's pretty clear that several properties on the highlighted lines are not being applied on the first one.
Please help as I need this as a "partial" view to be used across several pages.
EDIT: Directive code:
app.directive('sidebar', function () {
return {
restrict: 'E',
templateUrl: "/app/views/sidebar.html"
};
});
EDIT2:
Adding this in the post because it might be confusing from how I explained it:
I see where the confusion might be but they're different things. < sidebar > is a directive created by me. class="page-sidebar" is from the template I'm using and is what formats everything to its place. I tried to insert the class="page-sidebar" into the directive to see if it would work, but they're different things.
EDIT3:
To clear up the confusion, I hope: both pics show the sidebar is working. I know it's an element and as such I'm using < sidebar >, it's working, this is not the problem. The problem is when I use it, the contents such as Dropdowns (as shown in the second pic) don't work when I click them, while when the element contents are simply pasted into the index.html and not in the sidebar.html, it works.
EDIT4:
Found the issue but still no solution. I changed some stuff up and instead of the sidebar it's now on the widgets. Sidebar is now always loaded and it's the page contents which are loaded depending on the URL. This helped me track down the issue:
$(".owl-carousel").owlCarousel({mouseDrag: false, touchDrag: true, slideSpeed: 300, paginationSpeed: 400, singleItem: true, navigation: false,autoPlay: true});
The previous code is in a plugins.js file which is included in the html. For some reason, this line is NOT being run when the page is loaded. When I ran this line in the chrome console, the proper widget appeared.
For some reason, the js contents are not being run when the page loads.
Your main problem is the restrict: 'E',, which is restricting it to elements. This explains why it works for <sidebar>, but not for <div class="sidebar">. If you want to use classes, you need to change it to restrict: 'C'.
Another problem is that when you are trying to use the directive as a class, you are using class="page-sidebar" rather than class="sidebar".
See the docs for directives.
From angular documentation:
The restrict option is typically set to:
'A' - only matches attribute name
'E' - only matches element name
'C' - only matches class name
'M' - only matches comment
These restrictions can all be combined as needed:
'AEC' - matches either attribute or element or class name
The directive definition object for your sidebar quite clearly states that it will treat a DOM node with the tag name sidebar to render the directive template due to the restrict : 'E' property.
So use the directive as an HTML node, and NOT in a class (as it would require the property restrict to be set to C letter).
<sidebar></sidebar>

templateUrl not loading when creating custom directive

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

Using Angular, how can I show a DOM element only if its ID matches a scope variable?

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();
});

Angular JS - Evaluation Timing

I have the following problem in angularjs. I want to use an UI libary that injects some html code itself (Metro UI CSS) and I have troubles to getting the execution order right.
A simple example: http://metroui.org.ua/hint.html
If I declare in html:
<span data-hint="My hint"></span>
The UIjs will create the html elements needed for the hint display. Nofurther script code has to be added. Well actually when you load the js the following code gets executed: $('[data-hint]').hint();
Since the angular created html doesn't exist when I load the javascript, it doesn't work at first at all.
I believe I need an angular directive to solve the problem (and in parts it does) - I created the fowlling directive:
app.directive('hints', function() {
return {
link: function() {$('[data-hint]').hint()}
};
});
The following does work, even if this is in html created by angular:
<span hints data-hint="the text for my hint">test</span>
The following doesn't work (at least it doesn't behave the way I'd like to):
<span hints data-hint="{{something}}">Test</span>
The hint text will display literally {{something}} and not whatever is behind the angular expression. I tried already to create template like, but the result is still the same:
app.directive('hints', function() {
return {
template: '<span data-hint="{{something}}">Test</span>',
link: function() {$('[data-hint]').hint()}
};
});
Any hints on how to solve that problem would be greatly appreciated.
The main problem seems to be that if you attach the hint() in the link function, jquery takes the old value before angular has evaluated it. One option would be to wrap $timeout(function(){..}) around element.hint(), but I use that hack too much already, and it doesn't solve another problem: the hint needs to update when the $scope changes (if it depends on the $scope). To solve that problem we can add a $watch function and update the hint value when needed.
So, in conclusion:
/* This directive triggers automatically on data-hint as well */
app.directive('hint', function($timeout) {
return {
link: function(scope, element, arguments) {
/* set the value of the hint, then attach the metro-hint widget */
element.data('hint' , arguments.hint).hint();
/* watch for changes in the value so the hint gets updated */
scope.$watch(function(){
return arguments.hint;
}, function() {
element.data('hint' , arguments.hint);
});
}
};
});
(Tested with jquery 1.10.2, jquery-ui 1.10.3 and angular 1.2.6)

Angularjs animate ngClick + ngShow with custom directive (mySlide)

I'm trying to get the same ultimate functionality as ng-click + ng-show, except that I want the show to slide in instead of suddenly appear by toggling display: block/none;. I've got the jQuery animate I need, and I've set up the ng-click. I've got 2 problems, but the second might be a result of the first:
Problem 1
ng-click does not change the value of aside_users. I saw SO#12202067 which seems to be a similar situation, but I don't understand how/why their custom directive works and the native ng-click doesn't.
I see the scope: { … } after restrict: 'A',, but that appears to make $scope values available within the newly-created DOM element (my elements already exist and show up just fine, but no triggers/events are happening).
infobox.html
<aside
class="users"
ng-include src="'views/users.html'"
my-slide={"direction":"left","condition":"aside_users"}
></aside>
<i
class="icon icon-user"
ng-click="aside_users=!aside_users"
ng-init="aside_users=false"
></i>
The above code is a $compile'd template and elsewhere within the template I print out the value of the $scope parameter aside_users (prints false).
Problem 2
my-slide doesn't seem to be initiated/triggered (the logging of 'elm: ', elm doesn't appear in Chrome's console). I verified that directives.js is linked in my index.html page.
EDIT I remembered to link directives.js in index.html, but I forgot to add it to the resources array in app.js.
Plunkr
P.S. I'm not sure if <aside attr={object}> is strictly valid, but legitimate browsers seem to accept it in test cases (didn't bother to check IE). My alternate plan is to use 2 attributes: <foo my-slide="direction" my-condition="boolean"></foo>

Categories

Resources