Decompile angular elements - angularjs

We have a angular grid written by some guys here at work, the entire company uses it.
A td-cell could look like this
<td typeahead-cell="location as location.Name for location in getApiLocations($viewValue, mapping)" ng-model="mapping.selectedLocation">
{{mapping.getLocationNames()}}
</td>
The typeahead-cell directive will execute some custom code on the td, what it does is hookup some code so that if you double click or write in the cell it will go from display only to (in this case) typeahead. It does this by taking the html in the td cell (The td cell is already compiled by angular), wrap it with some custom code that does above functioanlly and then call $compile on the entire thing. This works with expressions above like {{mapping.getLocationNames()}} because they do not change when compiling so it can be compiled any number of times.
The problem I face now is that I try to use a more complex expression with ng-repeat. Problem is the first compile (Done directly by angular-core) will change html from example
from
<span ng-repeat="location in mapping.locations">...</span>
to
<!-- ngRepeat: location in mapping.locations -->
Then when our custom grid code executs it will try to compile the code above which will result in an empty since it compiles against a html comment.
This is the code that breaks
$element.html($compile(displayElement.html($element.html()))($scope));
$element is the td-cell that contains my orignal code that, when doing $element.html() it will take compiled code and try to use that. Wont work. Displayelement is a wrapper that will show when we are in displaymode.
I either need to decompile $elementbefor edoing $element.html or somehow move the content of the $element (td cell) compiled and hooked up.
Any ideas?
edit: I have somewhat solved it, doing this
$element.children().appendTo(displayElement);
displayElement.appendTo($element);
This will take the children from the td-cell and add them to the displayElement without actually breaking the original $compile. jQuery.children cant move <!-- comment --> elements so if you have an expression with ng directives like my repater above you need to wrap it in a dummy element like
<span><span ng-repeat="location in mapping.locations">...</span></span>
Any workaround for this?

Instated of that line if you can check with this
//Store it first on a variable if blank
var html;
if(!html) html = displayElement.html($element.html());
$element.html($compile(html)($scope));
Hopefully it will work. May be you need to manage the scope of the variable.

Final solution is this
$element.contents().appendTo(displayElement);
displayElement.appendTo($element);
It's very important to use contents and not children because childrenwill ignore text nodes which will not include the comments generated by ng-repeat directive.

Related

Using contenteditable with ng-model inside ng-repeat?

Here is my issue:
I am using ng-repeat to make a list of spans.
Each span has the contenteditable attribute and ng-model directive.
Everything works as expected (including two-way data binding), until I try to add a new item to the list.
<div ng-repeat="item in list">
<span ng-model="item.text" contenteditable></span>
</div>
<button ng-click="addItemToList"></button>
The methods look like this:
$scope.addItemToList = function () {
$scope.list.push({text: 'dummy text'});
}
or
$scope.addItemToList = function () {
$scope.list.splice(1, 0, {text: 'dummy text'});
}
When adding the new item to the list (push or splice), the DOM updates, but the last item is initialised empty, with no text whatsoever. The last item in the model list also empties out, even if I specifically push an element with text in it.
After a few tests, I've noticed that this only happens if the list's length is bigger after modifying it:
if I try to replace/modify/remove (not add) an element in the list, it works well.
I believe this has to do with the way contenteditable elements initialise in the DOM (I think they initialise empty, and somehow, the model empties out as well).
Has anyone encountered this problem before? If yes, how did you solve it / what workaround have you found?
Based on the angular docs related to ngModelController, it seems that there is not built-in support for two-way data binding with contenteditable elements, seeing as in the plunkr example they wrote their own contenteditable directive. You might be able to use a modified version of that as a workaround.
It looks to be a similar problem as this question and the contenteditable directive there looks similar to the contenteditable directive in the angular docs example.
I also found this directive on github that might accomplish what you are trying to do.
Edit: I did a new version of the plunk I posted in the comment above:
https://plnkr.co/edit/v3elswolP9AgWHDIPwCk
In this version I added a contenteditable directive that appears to be working correctly. It is basically a spin off of how the input[type=text] directive is written in angular, but I took out the places where it handles different types of input (since in this case it will just be text) and the places where it handles events that contenteditable elements don't even fire. I also changed it so that the $viewValue gets updated based on element.html() instead of element.val(). You might be able to use something like this as a workaround
The issue is old but that was the same problem for me today. (angular 1.5). My workaround was to add on blur update option: <td contenteditable data-ng-model="position.value" ng-model-options="{updateOn: 'blur'}"></td> somehow then model stopped getting be empty on initialize. I like using update on blur in many places (solves some performaces issues) so this is good for me, however it's some kind of trick.

Angular render markup that is nested

I get JSON like this
{
"lots of":"keys"
"description" : {
"key":"Some sample key",
"value":"This is the markup™"
}
}
from server and I ultimately iterate the description objects and populate table rows with two columns: one for the key and one for the value.
I have tried putting on my <td> tag ng-bind-html as well as injecting $sce into my controller and using trustAsHtml but so far the string always displays as it is in the JSON. Not every value will be HTML but I can easily detect based on the key if HTML is a possibility. It seemed when I put in the directive on the td it did not display anything if no HTML was present. I am interested in using something that can allow HTML in the value but not require it so I can display either
HTML fragment
<tr ng-repeat="(key, val) in record.description">
<td>{{key}}:</td>
<td>{{val}}</td>
</tr>
I created a quick fiddle here:
https://jsfiddle.net/frishi/mvw97s3q/6/
I used angular-sanitize, which I am not sure you mentioned injecting in your module dependency list. Either way, the example works simply by using ng-bind-html
Relevant docs page: https://docs.angularjs.org/api/ng/directive/ngBindHtml
It works by using the directive ng-bind-html on the element you want to display the HTML string in. You use it like so:
<p ng-bind-html="data.firstName"></p>
assuming that data.firstName = "<strong>FeeFee</strong>" or something like that.
I would also like to add that Angular does not allow this natively because of legitimate security concerns. That and the fact that allowing arbitrary HTML to be rendered might not always produce desirable results. Your page layout could quite possibly break because of some HTML you allowed to be passed through.
Angular was designed with security in mind, and will prevent you from displaying HTML from raw strings whenever possible - to prevent various injection attacks.
Here is workarround for your problem: AngularJS: Insert HTML from a string. Generally you should use ng-bind-html insted of ng-bind (this is used by curly braces).

ng-html-bind and syntax highlighting

I have an element with ng-html-bind that loads HTML content:
<p ng-bind-html="content.body"></p>
Inside this content I have one or more <code> blocks.
I would like to apply syntax highlighting only to the code tags of the loaded content, for example using angular-highlightjs directive.
Any idea on how to achieve this?
There are many ways to bind html on the page with Angular. One way is with ng-bind-html, but it's not really the best for this use case, since you also need angular-highlightjs directive to compile. You can achieve your goal with $compile like this:
/** Here, you will need to do some transformations to your html string
* 1. Add `hljs` attribute or `class="hljs"` to the `<code>` tag in any `<pre><code>`
* 2. Hopefully you already have your line breaks in place. This will result in
* a single line code block otherwise. See my plunk for how I added '\n'
*/
var myHTML = $scope.content.body;
element.append( $compile( myHTML )($scope) );
See my plunk

AngularJS: how to prevent nested HTML from being compiled against a scope?

Say I wanna have the popup directive with ability to declare it's content within a same HTML view:
<a popup>
<span>Click me</span>
<popup-content>
<div with-some-other-directives></div>
</popup-content>
</a>
I've tried:
At popup directive, modify HTML during compile phase: get the content of the <popup-content>...</popup-content> element and then remove it completely. This way there are 2 problems:
with-some-other-directives compiles and links.
If I have an multiple <a> elements within <popup-content> I see some strange behavior. Suppose this has something to do with nesting an <a> inside an <a>.
Making a separate popup-content directive, that has own compile phase handler. There, I could either access controller of parent popup directive and set the HTML there then self remove it.
Either way I get compilation and execution of with-some-other-directives directive.
Now I have 2 questions:
How to prevent HTML code within <popup-content> from being executed?
Are there better alternatives?
EDIT
The final goal is to use HTML code taken from popup-content, compile it against the scope of popup directive and append to BODY.

Stop AngularJS inserting <span class="ng-scope"></span> using ng-include

I'm using the Foundation layout framework, which automatically floats the last sibling of .column to the right and I really appreciate this is a behaviour. However, AngularJS takes it upon itself to insert span.ng-scope after every div.column, which somehow causes browsers to consider the last span the last sibling of .column (even though it is not).
Specifically the css in Foundation responsible for this is:
[class*="column"] + [class*="column"]:last-child { float: right; }
As I understand it, [attribute*="substring"] should select only siblings that match, so, for the above, only elements whose class attribute contains column (including columns). I would think a span tag whose class attribute that does not contain column should not match (and thus be ignored by :last-child). However, this does not seem to be the case.
Regardless, the span is causing the problem:
Angular buggering it up (jsfiddle)
Works fine without Angular (same jsfiddle, no ng-include)
Is there a way to configure angular to stop inserting those span tags? I would, begrudgingly, modify the css selector to somehow ignore all span tags; however I might eventually need/want to use a span tag.
Since you indicated the div can be moved inside, this works:
<ng-include src="'main.tmpl'"></ng-include>
Then in your template:
<div class="row">
<article id="sidepanels" class="four columns">
...
</div>
I'm not aware of any way to prevent angular from inserting the span tags (I think it keeps track of scopes that way -- for garbage collection).
Also you can try my version of include directive that does not creates a scope: Gist source.
As no scopes are created, AngularJS should not create additional element to mainain scope (it actually use data attributes to store link to scope).

Resources