AngularJS asynchronous directive compilation - javascript

I'm writing an angularjs app (with a node/express backend). I have in my scope an object that gets populated (and repopulated) asynchronously via a service.
The contents of the object is a series of "question" objects, which for simplicity have a "text" and a "type" attribute.
What I'm trying to achieve is to be able to have an angular directive per type, and for these to render properly. So for example if the server returns [{type:'booleanquestion',text:'Is the sky blue?'}] I would create an element , and then the booleanquestion directive will kick in and render it appropriately.
So far I have defined a "questionset" directive which works well, watching the questions collection and compiling the required stuff so the directives display correclt.y
Html
<div questionset="questions"/>
app.js
...
scope.questions = questions; // e.g. [{text: 'some text',type: 'boolean'}]
directives.js
angular.module('myApp').directive('questionset',function($compile){
return {
transclude: 'true',
compile: function(element,attr,linker){
return function($scope,$element,$attr){
var elements = [],
parent = $element.parent();
$scope.$watchCollection('questions',function(questions){
var block, i, childScope;
if(elements.length > 0)
{
for( i = 0; i < elements.length ; i++)
{
elements[i].el.remove();
elements[i].scope.$destroy();
}
elements = [];
}
if(!questions) return;
for(i = 0; i < questions.length; i++)
{
childScope = $scope.$new();
childScope['question'] = questions[i];
var html = '<div class="'+questions[i].type+'"/>';
var e = angular.element(html);
block = {};
block.el = e;
block.scope = childScope;
$compile(e)(childScope);
element.append(e);
elements.push(block);
}
});
}
}
}
});
// For example, one type might be "boolean"
angular.module('myApp').directive('boolean',function($compile){
return {
restrict: 'AC',
template: '<div class="question">{{question.text}}</div>',
replace: true,
link: function(scope,elem,attrs,ctrl){
…….
// for some reason, $compile will not be defined here?
}
};
});
Whilst this is working OK, I have 2 questions
1). Is this the "right" angular way to do this? This is my first angular project and it seems I've jumped in rather at the deep end (or that's how it feels anyway)
2). My next goal is for the question.text to be able to contain HTML and for that to be "compiled". For example the text might be
"Is the <strong>sky</strong> blue?"
I'm not sure how to make this work - as my comment in the code suggest, for some reason $compile is not being injected into my boolean directive. Perhaps this is because I've manually created that child scope for it? Is it right I'm trying to $compile again the contents of the element? I feel like this last bit is probably very simple, but I'm not seeing it.

1) I don't know. In my opinion, the overall approach seems very nice; just need to polish it, as we'll see below.
2) Maybe $compile is not available at the nested function because it is not used in the parent level. Try referencing $compile in the parent function to see if this is really the reason:
angular.module('achiive-vision').directive('boolean',function($compile){
var justTesting = $compile;
return {
restrict: 'AC',
template: '<div class="question">{{question.text}}</div>',
replace: true,
link: function(scope,elem,attrs,ctrl){
…….
// check if $compile is defined here now
}
};
});
I would simplify things by changing the questionset elements html to:
var html = '<div class="question ' + questions[i].type + '">'
+ questions[i].text + '</div>';
This way, you don't need to compile again, and you will already have HTML support.
Also, it is a bit strange to have
var html = '<div class="'+questions[i].type+'"/>';
above, and then you will REPLACE it by a similar markup:
template: '<div class="question">{{question.text}}</div>',
...and then you want to COMPILE this again...
In my opinion, you don't need replace: true and you don't need the template at the question type directives. Implement the behaviour and anything else on them, but let the parent questionset include the question text and compile it - everything is ready for this.
Also, a detail: I wouldn't user var html and var e inside a loop. I would add them to the list above:
var block, i, childScope, html, e;
(Also, I would avoid naming variables as "i" and "e"...)
These are my comments regarding your code. As I said, I think the approach is very nice, not to say "advanced" for someone who is beginning with AngularJS. I myself am using it for less than two months, only.

Related

Replacing angular element html within a directive's link

This is my first Angular Directive.
I am trying to do a simple highlight on a html content based on the search terms used to find that content.
The problem is, that is working for the first term, but not for more. I want to all words get highlighted but I am doing something wrong when I replace the HTML content.
This is what the directive tries to do:
1.
The directive should highlight one or more words. For example.
If the search terms are "document legal" it should highlight both of them, even if they are not on this order.
So, a text like "legal something document" should get both highlighted, "legal" and "document".
2.
If the word is less than 3 characters is not going to get highlighted.
3.
If the word is not found, try removing the last character from it until its length is less than 3. You may search for "dimensions" and the search engine may return a text containing "dimension" or even "dime".
Just in case, the app is an Ionic App.
This is my code.
The angular directive:
angular.module('starter.directives', [])
.directive('highlightSearchTerms', function($compile) {
return {
restrict: 'A',
scope: true,
link: function($scope, element, attrs) {
$scope.highlightTerm = function(term) {
var html = element.html();
var highlighted = html.replace(new RegExp(term, 'gi'),
'<span class="highlightedText">$&</span>');
if (highlighted == null) {
return false;
}
// #see
// I think that the problem is here, it works the
// first time, but the second time it gets here
// the following exception is throwed
// "Cannot read property 'replaceChild' of null"
element.replaceWith(highlighted);
return html != highlighted;
};
var searchTerms = $scope.searchTerms;
if (searchTerms != undefined && searchTerms.length < 3) {
return;
}
var terms = searchTerms.split(' ');
// Try to highlight each term unless the word
// is less than 3 characters
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
// // Try to highlight unless the word is less than 3 chars
while (term.length > 2) {
// If it got highlighted skip to next term
// else remove a character from the term and try again
if ($scope.highlightTerm(term)) {
break;
}
term = term.substring(0, term.length - 1);
}
}
}
};
});
You can see some weird things. Like using $scope.highlightTerm instead of passing the highlightTerm var to the directive. I couldn't get it work.
How can I change the HTML of the element correctly?
This is the template that is using the directive:
<div ng-include src="tplName" highlight-search-terms></div>
I wish to do something like that but I couldn't get it working:
<div ng-include src="tplName" highlight-search-terms="something to highlight"></div>
Here is a Plunker:
http://plnkr.co/edit/BUDzFaTnxTdKqK5JfH0U?p=preview
I think your code is working, but the issue was that you are trying to replace the whole div that is using the directive. So what you can do is just replace element.replaceWith(highlighted); with element.html(highlighted); and it will work.
I wish to do something like that but I couldn't get it working: <div
ng-include src="tplName" highlight-search-terms="something to
highlight"></div>
You already there, just use attrs in the link function like so:
var terms = attrs.highlightSearchTerms;, and you will get what you passed in highlight-search-terms="something to highlight"
This should work for you, with using of 'compile' function:
angular.module('starter.directives', [])
.directive('highlightSearchTerms', function($compile) {
return {
restrict: 'A',
scope: true,
compile: function(elem, attrs) {
// your code
elem[0].innerHTML = '<span class="highlightedText">$&</span>';
// your code
}
};
});
Documentation also could help.
Even tough punov's solution works, I think you shouldn't trigger multiple re-compiles for a single "line". I would suggest storing the html in a variable and recompile after every term was replaced.
Here is a working example - but it needs some polishing.
http://plnkr.co/edit/3zA54A0F2gmVhCComXAb?p=preview
link: function($scope, element, attrs) {
var searchTerms = $scope.searchTerms;
var terms = searchTerms.split(' ');
$scope.highlightedHTML = element.html();
if (searchTerms !== undefined && searchTerms.length < 3) {
return;
}
$scope.highlightTerm = function(term) {
console.log("html - ", term, html);
var highlighted = $scope.highlightedHTML.replace(new RegExp(term, 'gi'),
'<span class="highlightedText">$&</span>');
//element.replaceWith(highlighted);
return highlighted;
};
function highlight(terms, compile) {
// Try to highlight each term unless the word
// is less than 3 characters
// if the term is not highlighted remove one character
// from it and try again
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
while (term.length > 2) {
var current = $scope.highlightedHTML;
$scope.highlightedHTML = $scope.highlightTerm(term);
if (current !== $scope.highlightedHTML) {
break;
}
term = term.substring(0, term.length - 1);
}
}
compile();
}
highlight(terms, function() {
element.replaceWith( $scope.highlightedHTML);
});
}

angularjs - dynamic function return

index.html
<div ng-repeat="object in objects" ng-style="getStyle(object.id)"></div>
controller.js
[...]
$scope.startWidth = 100;
$scope.getObject = [...]
$scope.getStyle = function(id) {
var widthVal = $scope.startWidth;
$scope.StartWidth += $scope.getObject(id).value;
return { width : widthVal }
}
[...]
This code runs into an infinite $digest loop. I think i know why. The return value should not to be dynamic. But how can i realize that without the loop? I want to display objects with different width. The width of an object depends on a value of the previous object.
The problem is that ng-style puts a $watchExpression on each child scope.
A $watchExpression must be idempotent, means it must return the same with multiple checks.
If a $watchExpression is still dirty another $digest will be triggered.
UPDATE
After I saw GRaAL nice answer I came up with even better solution:
Here is a demo plunker: http://plnkr.co/edit/oPrTiwTOyCR3khs2iVga?p=preview
controller:
$scope.$watchCollection('objects', function calculateWidths() {
var tempWidth = 0;
for( var i = 0; i < $scope.objects.length; i++) {
tempWidth += $scope.objects[i].value;
$scope.objects[i].width = tempWidth + 'px';
}
});
markup:
<div ng-repeat="object in objects" ng-style="{width: object.width}">
old solution
Here is a plunker: http://plnkr.co/edit/CqxOmV5lpZSLKxqT4yqo?p=preview
I agree with Ilan's problem description and solution - it works in case the objects array is not going to change, otherwise the widths will be calculated incorrectly. If new objects may be added/removed in runtime then it is better to recalculate the widths when the collection is changed (see $watchCollection method of scope) or on each digest cycle (for short list of elements it shall be fast enough), something like that:
var widths = [];
$scope.$watch(function calculateWidths() {
var tempWidth = 0;
widths = [];
angular.forEach($scope.objects, function(obj) {
tempWidth += obj.value;
widths.push(tempWidth);
});
});
$scope.getStyle = function(idx){
return {width: widths[idx] + 'px'}
}
Here is the plunker

javascript: not having to use all the IDs in a html file?

I want to use the same .js for a bunch of html pages, but not necessarily all the ID's from this .js in every single page. Right now, if I don't use one ID; no ID's are showing at all.
var yes = 'Yes';
var no = 'No';
var available = 'Available: ';
document.getElementById("001").innerHTML=available+yes;
document.getElementById("002").innerHTML=available+no;
document.getElementById("003").innerHTML=available+yes;
A html with this works fine:
<div id="001"></div>
<div id="002"></div>
<div id="003"></div>
A html with this, not so fine:
<div id="002"></div>
<div id="003"></div>
What to do to make it run even though some ID's arn't being used?
Complete noob to this, there's probably a super simple solution to it(?) - but I can't find it. Hopefully, someone here can help me - without totally bashing me, or telling me how much of a bad praxis this is and that I should rewrite it in some mega haxxor language that I havn't even heard of :D
Thanks in advance!
While I'd question why you'd need incrementing numeric IDs like that, one solution would simply be to keep an map of IDs to values, then iterate the map checking for null.
var m = {
"001":yes,
"002":no,
"003":yes
};
for (var p in m) {
var el = document.getElementById(p);
if (el) // or if (el !== null)
el.innerHTML = available + m[p];
}
The document.getElementById() function returns null if no matching element is found. null.innerHTML is an error that stops the current script executing. So you just have to test for null:
var el = document.getElementById("001");
if (el != null) el.innerHTML = available + yes;
The null test can be simplified to:
if (el) el.innerHTML = available + yes;
If element "001" is always going to be "yes", "002" is always going to be "no" and so forth then you can do this:
var results = {"001" : yes, "002" : no, "003" : yes};
var el, k;
for (k in results) {
el = document.getElementById(k);
if (el) el.innerHTML = available + results[k];
}
wrap it with if(document.getElementById("id")!==null) ?
Just wrap the whole thing in a try statement to avoid any issues and put code afterwards into a finally statement::
try{
document.getElementById("001").innerHTML=available+yes;
document.getElementById("002").innerHTML=available+no;
document.getElementById("003").innerHTML=available+yes;
//etc
}
finally{
//any other code that there is after the id stuff
}
that'll prevent errors, so if something fails, it will still continue

Using eval to assign a variable to a method. How else should I do it?

I am using a method to accept a portion of an element's ID and use that to determine which url path I need to pass to a method. In my case this is used by a click event on a button that could be attached to any one of a number of grids so I need to know which grid I am on in order to know what data to reload:
jQuery('.reset').click(function(e) {
e.preventDefault();
jQuery('#' + grid_id + '_mnf').toggle();
jQuery('#' + grid_id + '_mnfall > .instance').each(function () {
jQuery(this).removeAttr("checked");
});
var url = eval(grid_id + '_url');
jQuery('#foo').setGridParam({url:url}).trigger('reloadGrid');
});
The possible urls are defined as variables so I have something like:
var foo_url = url_path + '?input=moduleone&param=1';
var bar_url = url_path + '?input=moduleone&param=2';
This means that in the setGridParam method above the url value will be one of these vars. If I simply do:
var url = grid_id + '_url'; //note I'm not using eval here
The value passed to setGridParam will not work.
If I don't use eval like I do above the url will not work. Why? What is going on here? What else should I do since "eval is evil"?
In my mind, without using eval, I am passing the url like:
jQuery('#foo').setGridParam({url:foo_url}).trigger('reloadGrid');
but this fails so apparently eval is required here?
Because I am sure this is too rigid and clunky of a way to do this and in case anyone wants to see it (and perhaps suggest how I am doing this wrong?), here is how I am getting that grid_id:
// ex. id='foo_filter'
var source = $(this).parent().parent().attr('id').split('_');
var parsed_source = source.splice(1, source.length + 1);
grid_id_portion = '';
for (var i = 0; i < parsed_source.length; i++) {
grid_id += parsed_source[i];
if(i < parsed_source.length - 1){
grid_id += '_';
}
}
Some issues I see... I have to define the urls as variables for each grid. This is quite inflexible, yes? Those urls are pretty similar as well so defining a bunch of them seems inefficient. Also, what if someone crafts a special chunk of code to insert as the ID that I am breaking apart to pass to eval. That could be bad news, yes? Any suggestions on how to improve on this would be greatly appreciated.
Use a map to store URLs, not local variables.
var URLs = {
foo: url_path + '?input=moduleone&param=1',
bar: url_path + '?input=moduleone&param=2'
};
jQuery('.reset').click(function() {
//do stuff...
var url = URLs[grid_id];
jQuery('#foo').setGridParam({url:url}).trigger('reloadGrid');
});

basic jquery looping question around 'click'

Basic question, but I have been pounding my head for a bit so thought id bring it here.
html looks like this (edit, fixed the closing quotes)
<span class='deleteimage-119'>delete</span>
<span class='deleteimage-120'>delete</span>
<span class='deleteimage-121'>delete</span>
<span class='deleteimage-122'>delete</span>
<span class='deleteimage-123'>delete</span>
javascript/jquery looks like this
iids = ['119','120','121','122','123'];
for (i=0; i<iids.length; i++) {
place = iids[i];
$(".deleteimage-" + place).click(function () {
alert(place);
});
}
The click functionality gets attached to each individual span, but the alert after clicking just shows the last item in the array.
You have a scoping issue. By the time the callback fires, place has the last value from the loop.
You need to create a new variable for the closure; one variable per iteration, each of which will then be "caught" by the closure and used in the callback.
It would be nice if the solution were this:
var iids = ['119','120','121','122','123']; // don't forget `var` please
for (var i=0; i<iids.length; i++) {
var place = iids[i]; // local variable?
$(".deleteimage-" + place).click(function () {
alert(place);
});
}
Alas, Javascript has no block scope so there's still only one variable here called place, and it keeps getting updated as the loop runs.
So, you have to use a function instead:
var iids = ['119','120','121','122','123'];
function f(place) {
// NOW `place` is a local variable.
$(".deleteimage-" + place).click(function () {
alert(place);
});
}
for (var i=0; i<iids.length; i++) {
f(iids[i]);
}
There are neater ways to employ this function approach using closures and bound variables, and the other answers cover those neater ways quite well. My answer has focused on explaining the issue.
The issue is with scopes and closure.
In JS the scope is # function level and not block level.
Try this:
var iids = ['119','120','121','122','123'];
for (i=0; i<iids.length; i++) {
place = iids[i];
var clickFn = function(a){
return function(){
alert(a);
}
}(place);
$(".deleteimage-" + place).click(clickFn );
}
This is because the click is occurring after the loop is completed, so you're alerting place = iids[iids.length - 1]. In order to achieve the result you're looking for you need to create a function closure and pass place in as a parameter:
iids = ['119', '120', '121', '122', '123'];
for (i = 0; i < iids.length; i++) {
place = iids[i];
(function(_place) {
$(".deleteimage-" + _place).click(function() {
alert(_place);
});
} (place));
}
Inside the loop, you are binding the click event to those span, but the events are fired only after the loop is complete. So, it will always show the last value.
As others have mentioned, you have a scoping issue. Unless all of those classes have a unique meaning, I'd move the place value to a data attribute and leave deleteimage as the class name. Something like:
<span class='deleteimage' data-place='119'>delete</span>
<span class='deleteimage' data-place='120'>delete</span>
<span class='deleteimage' data-place='121'>delete</span>
<span class='deleteimage' data-place='122'>delete</span>
<span class='deleteimage' data-place='123'>delete</span>
$(".deleteimage").click(function() {
var place = $(this).data("place");
alert(place);
});
If the place values aren't unique values, then this answer doesn't apply.

Categories