Why does using ng-repeat to iterate html fail when the html contains divs? - angularjs

When I use ng-repeat to iterate the following code, everything is fine:
<p ng-repeat="user in users">
<input size="50" ng-model="user.name"></input>
<span>Foo</span>
</p>
However, using the following fails:
<p ng-repeat="user in users">
<input size="50" ng-model="user.name"></input>
<div>Foo</div>
</p>
In the latter case, it looks like the div is excluded from the loop and appended only once after the input tags that have been repeated as expected.
I'm trying to understand the difference in behaviour towards div tags.
Thanks for any insights.
EDIT: The question was already answered, but here's a js-fiddle that allowed me to demonstrate the issue: http://jsfiddle.net/f26Cg/5/

You cannot nest <div> elements in <p> element: as shown here, it's permitted to contain so-called phrasing content only.
As <div> opening tag is considered to be an end of <p> element, technically its corresponding element is outside of <p> in the second case - that's why it's not repeated.

I'll quote Josh's answer to this question : Directive inside ng-repeat only appears once
It is actually related to how your browser will handle blocks inside a non-allowing blocks tag.
I imagine this is the browser's doing. Technically, paragraph tags are
only allowed to contain inline elements, which div is not. Some
browsers (most?) will automatically close the <p> when hits an
unauthorized tag. If you inspect the DOM, you will see that even the
div that makes it into the DOM from the ngRepeat is not inside the
generated paragraph.
Josh

Related

ng-repeat fails on a p>div structure, but works for a div>div structure?

I'm maintaining a legacy product, and found a quirk that I haven't seen before in AngularJS.
As demonstrated in this Plunker, the following HTML fails to render:
<p ng-repeat="item in items">
<div>{{item.type}}</div>
</p>
while this renders just fine:
<div ng-repeat="item in items">
<div>{{item.type}}</div>
</div>
Is there any explanation as to why this might be the case?
I was rather caught off-guard with this, as I don't recall seeing anything about this in the development resources.
It most likely is due to the fact that the HTML spec specifies that for a <p> immediately followed by a <div>, the close tag is optional.
I would assume this means that somehow the browser silently ignores the presence of any explicit source-specified </p> tag. I'd guess that when ng-repeat is parsing the source, it then cannot find the end of the repeated section, and therefore cannot render as expected.

When ng-init in ng-repeat is replays?

I have a simple ng-repeat to build a HTML list from a javascript array.
Each item can be moved using an input to get the new rank. This input is binded to a variable rank. This variable is initialized using the ng-init directive.
Code looks like this :
<li ng-repeat="item in ctrl.getItems()">
<div ng-init="rank = $index">
[$index: {{$index}}]
{{item}}<br/>
<label>
Move to
<input type="number" ng-model="rank"/>
</label>
<button type="button" ng-click="ctrl.moveItem($index, rank)">
Ok
</button>
</div>
</li>
At runtime, when I change the input value and click to the Ok button, function ctrl.moveItem is called and item is really moved in the ctrl.getItems() array.
So the ng-repeat is replayded and items appears in the new order.
BUT variable rank is not reinitialized and 2 items appears with the same rank.
The sample is here : https://jsfiddle.net/nlips/4ng34b7b/
My question is not so much about moving items in a list, but I need to understand how ng-init works in the context of ng-repeat.
I did not find anything on this subject in the AngularJS official documentation.
From AngularJS docs:
The ngInit directive allows you to evaluate an expression in the current scope.
Now. You are working with different scope.
You are using ngInit into the transcluded scope, overriding $scope.rank each time it repeats that portion of template.
If you want to persist your rank you should init it into the ngRepeat scope.
Try with:
<li ng-repeat="item in ctrl.getItems()" ng-init="rank = $index">
[$index: {{$index}}]
{{item}}<br/>
<label>
Move to
<input type="number" ng-model="rank"/>
</label>
<button type="button" ng-click="ctrl.moveItem($index, rank)">
Ok
</button>
</li>
EDITED ANSWER
Ok, i got it.
The ngInit expression is evaluated only when that part of template is going to be rendered into the DOM.
So, when the page is loaded for the first time your expression is fired and each rank is evaluated correctly.
But, when you make changes on an item that is already rendered, your template is not going to be rendered again, so your ng-init will not be fired.
If you want that ng-init to be executed again you have to remove the item from the DOM and then append it back, into the new position.
There are several alternatives to this approach, but i hope this clarifies what was going on.

Filter a directive by using a div wrapper or within the directive tag

I'm trying to go with the best approach and avoid unnecessary rendering/processing time in my AngularJS app when choosing between 2 directives to be displayed in the page inside an ngRepeat loop, want to know which is the best way:
If by setting the ng-if directly in the directive html element, like:
<div ng-repeat="element in list">
<my-directive-a ng-if="someFunction(element)"></my-directive-a>
<my-directive-b ng-if="!someFunction(element)"></my-directive-b>
</div>
Or by moving out the first <div> from the directive's template and use it as a wrapper for each directive. For instance:
<div ng-repeat="element in list">
<div ng-if="someFunction(element)">
<my-directive-a></my-directive-a>
</div>
<div ng-if="!someFunction(element)">
<my-directive-b></my-directive-b>
</div>
</div>
NOTE: The starting <div> element on each directive could be modified behave the same so I will basically take that out of the directive's html and moving it outside the directive declaration in order to place the ng-if there
What would be the best approach for this case? Are there any performance implications from doing it one way or another? Or is it just the same thing? Consider that the number of elements in the list could get really big.
They are quite the same, but you can improve performance with one-time binding, but only when element does not change at runtime (for example, let's say that it has property name, and your someFunction is like return element.name === 'John'). Angular just stop observing this function when it returns value, and watches will be deleted. There are 2 prerequisites to use this solution:
Elements properties in list does not change (if you rely on them in someFunction), for example if you rely on name property name must not change, because watcher on someFunction is note available.
When list changes or its elements properties change, you reload all list (for example, you fetch it from server again if you know that change occurred)
What you get with this? There is no watches after my-directives are drawn on ng-ifs, and when something changes, new reference is bound to list (for example, it comes from server) and everything will be redrawn, ng-ifs will run again and when will become stable (function returns value) then will be unbound. How it looks like? Like this:
<div ng-repeat="element in list">
<div ng-if="::(someFunction(element))">
<my-directive-a></my-directive-a>
</div>
<div ng-if="::(!someFunction(element))">
<my-directive-b></my-directive-b>
</div>
</div>
Two colons before expression. But be aware, that with one-time binding it's easy to mess up - you need to be sure that you test your code enough to be sure it works.

Angular with Microdata

Does Microdata work with dynamic Angular ng-repeat items?
Can I use it as:
<div itemscope itemtype="http://schema.org/Product" ng-repeat="item in items">
…
</div>
I have found schema validator which, for my site actually shows angular expressions:
...
datePublished {{lvl_project['year']}}
name "{{lvl_project['title']}}"
keywords {{lvl_project['tools'].join(',')}}
...
Furthermore, it does NOT show all of the ng-repeat-generated elements.
This seems to me like a strong indication that the google-bot did not see the angular-generated elements and their values, but there could be more to the issue that I don't know.
Yes, you can use...it will work on all (but use if all comes in same category).

How do I display an Angular directive conditionally?

I'm attempting to use Angular directives to create custom input field types. For example, I have a type called "duration" which draws three separate hours, minutes, and seconds fields. I'm trying to use the directive as an attribute of an input element, and the directive replaces the input element.
For example, in the following code:
<input duration>
Would be rendered as a <div> with several inputs inside of it, and the original input would be out of the picture.
I'm running an ngRepeat loop through several form fields of different types, including duration. I'd like to find a way that only requires me to put one input in the HTML, with the duration attribute applied only if the field is supposed to be of duration type. I tried the following:
<div ng-repeat="field in fields">
<input type='{{field}}' ng-attr-duration="field==='duration'">
</div>
The problem with that code is that every element is rendered as duration because ng-attr-duration gets evaluated to duration='false' when the field is not duration, which triggers my directive.
Is there a way for me to apply the directive conditionally without having to define multiple <input>s to reduce redundancy in my code?
Take a look at what the directive docs say about ng-attr:
When using ngAttr, the allOrNothing flag of $interpolate is used, so if any expression in the interpolated string results in undefined, the attribute is removed and not added to the element.
Note that you need to be using curly braces so Angular will interpolate the expression. And, if your expression has an undefined term in it, the attribute won't be added.
How about something like this:
<div ng-repeat="field in fields">
<input type='{{field}}' ng-attr-duration="{{field==='duration' ? true : undefined}}">
</div>
The true in the ternary expression can be whatever you want, just not undefined.
What i can think of you can use condition if-else of ng-switch
<div ng-repeat="field in fields">
<div ng-switch on="field">
<input ng-switch-when="true" type='{{field}}' ng-attr-duration="field==='duration'">
<input ng-switch-default="true" type='{{field}}'>
</div>
</div>
It will also maintain readability of code.

Resources