AngularJS ng-repeat with no html element
Asked Answered
B

7

93

I am currently using this piece of code to render a list:

<ul ng-cloak>
    <div ng-repeat="n in list">
        <li><a href="{{ n[1] }}">{{ n[0] }}</a></li>
        <li class="divider"></i>
    </div>
    <li>Additional item</li> 
</ul>

However, the <div> element is causing some very minor rendering defects on some browsers. I would like to know is there a way to do the ng-repeat without the div container, or some alternative method to achieve the same effect.

Bernabernadene answered 12/10, 2012 at 11:5 Comment(6)
What rendering issues are you talking about?Thalassa
the last li divider seems a bit thicker like its stacked (chrome win7). This could be a css issue, however, I would like to know if this could be fixed in angular. For comparison, knockoutJS allows containerless bindings using <!-- -->.Bernabernadene
I see, I use phptal on the server side and they have a tal:block tag specifically for this purpose :) guess with DOM not always accepting a new kind of tag that's harder with angularThalassa
Could you also update the html to reflect how you are using the htmlAppend directive?Pecuniary
Done... did this a while ago, but I do remember it workingBernabernadene
Put the ng-repeat on the ul? Actually strike that, it doesn't account for the <li>Additional item</li>Spirometer
J
105

As Andy Joslin said they were working on comment based ng-repeats but apparently there were too many browser issues. Fortunately AngularJS 1.2 adds built-in support for repeating without adding child elements with the new directives ng-repeat-start and ng-repeat-end.

Here's a little example for adding Bootstrap pagination:

<ul class="pagination">
  <li>
    <a href="#">&laquo;</a>
  </li>
  <li ng-repeat-start="page in [1,2,3,4,5,6]"><a href="#">{{page}}</a></li>
  <li ng-repeat-end class="divider"></li>
  <li>
    <a href="#">&raquo;</a>
  </li>
</ul>

A full working example can be found here.

John Lindquist also has a video tutorial of this over at his excellent egghead.io page.

Jalisajalisco answered 16/9, 2013 at 9:8 Comment(7)
Doesn't this require an element print?Obovate
I'm seriously confused what this accomplishes over simply <li ng-repeat="page in [1,2,3,4,5,6]"><a href="#">{{page}}</a></li> -edit: nevermind i think i get itBuchheim
I get that you're just trying to demonstrate ng-repeat-start/end but this is a confusing example and the wrong use case for it. A standard ng-repeat should be used here as your code renders an extra empty li for each item. You should probably mention that in your post.Sharpie
@Sharpie you are correct. But he seems to want that extra element for each iteration, something that is not possible without ng-repeat-start. I'll add the divider html class to my answer to be more clear.Jalisajalisco
ng-repeat-start and ng-repeat-end are a little confusing. The Angular documentation helps: https://docs.angularjs.org/api/ng/directive/ngRepeat#special-repeat-start-and-end-pointsPamilapammi
I had to use ng-repeat-start with unnecessary html tr elements that i removed them by ng-if .Salvadorsalvadore
If I have a non-repeating list of page items, comming from a JSON: <ul class="pagination"><li class="active"><span>1</span></li><li><a href="http://apiblog.com/api/?page=2" data-ci-pagination-page="2">2</a></li><li><a href="http://apiblog.com/api/?page=3" data-ci-pagination-page="3">3</a></li><li><a href="http://apiblog.com/api/?page=2" data-ci-pagination-page="2" rel="next">&rsaquo;</a></li><li><a href="http://apiblog.com/api/?page=4" data-ci-pagination-page="4">&raquo;</a></li></ul> how do i render it as HTML?Bergmans
M
30

KnockoutJS containerless binding syntax

Please bear with me a second: KnockoutJS offers an ultra-convenient option of using a containerless binding syntax for its foreach binding as discussed in Note 4 of the foreach binding documentation. http://knockoutjs.com/documentation/foreach-binding.html

As the Knockout documentation example illustrates, you can write your binding in KnockoutJS like this:

<ul>
    <li class="header">Header item</li>
    <!-- ko foreach: myItems -->
        <li>Item <span data-bind="text: $data"></span></li>
    <!-- /ko -->
</ul>

I think it is rather unfortunate AngularJS does not offer this type of syntax.


Angular's ng-repeat-start and ng-repeat-end

In the AngularJS way to solve ng-repeat problems, the samples I come across are of the type jmagnusson posted in his (helpful) answer.

<li ng-repeat-start="page in [1,2,3,4,5]"><a href="#">{{page}}</a></li>
<li ng-repeat-end></li>

My original thought upon seeing this syntax is: really? Why is Angular forcing all this extra markup that I want nothing to do with and that is so much easier in Knockout? But then hitautodestruct's comment in jmagnusson's answer started making me wonder: what is being generated with ng-repeat-start and ng-repeat-end on separate tags?


A cleaner way to use ng-repeat-start and ng-repeat-end

Upon investigation of hitautodestruct's assertion, adding ng-repeat-end on to a separate tag is exactly what I would not want to do in most cases, because it generates utterly usesless elements: in this case, <li> items with nothing in them. Bootstrap 3's paginated list styles the list items so that it looks like you did not generate any superfluous items, but when you inspect the generated html, they are there.

Fortunately, you do not need to do much to have a cleaner solution and a shorter amount of html: just put the ng-repeat-end declaration on the same html tag that has the ng-repeat-start.

<ul class="pagination">
  <li>
    <a href="#">&laquo;</a>
  </li>    
  <li ng-repeat-start="page in [1,2,3,4,5]" ng-repeat-end><a href="#"></a></li>
  <li>
    <a href="#">&raquo;</a>
  </li>
</ul>

This gives 3 advantages:

  1. less html tags to write
  2. useless, empty tags are not generated by Angular
  3. when the array to repeat is empty, the tag with ng-repeat won't get generated, giving you the same advantage Knockout's containerless binding gives you in this regard

But there is still a cleaner way

After further reviewing the comments in github on this issue for Angular, https://github.com/angular/angular.js/issues/1891,
you do not need to use ng-repeat-start and ng-repeat-end to achieve the same advantages. Instead, forking again jmagnusson's example, we can just go:

<ul class="pagination">
  <li>
    <a href="#">&laquo;</a>
  </li>
  <li ng-repeat="page in [1,2,3,4,5,6]"><a href="#">{{page}}</a></li>
  <li>
    <a href="#">&raquo;</a>
  </li>
</ul>

So when to use ng-repeat-start and ng-repeat-end? As per the angular documentation, to

...repeat a series of elements instead of just one parent element...

Enough talk, show some examples!

Fair enough; this jsbin walks through five examples of what happens when you do and when you don't use ng-repeat-end on the same tag.

http://jsbin.com/eXaPibI/1/

Malignity answered 30/1, 2014 at 1:16 Comment(10)
You seem to have misunderstood the point of the question. The goal is to repeat two or more list items simultaneously, and neither your example nor the page you linked to provide a way to accomplish this. As you've noted yourself, attaching ng-repeat-start and ng-repeat-end to the same element is utterly useless when you can use angular's default ng-repeat and be done with it.Unprecedented
@Asad - "the point of the question" is not clearly stated by the OP. Also, have you inspected the rendered DOM output of the accepted answer? I cannot imagine anyone wanting that kind of output, at least certainly not for a bootstrap pagination list.Malignity
@Malignity Yes, the output of the accepted answer is unacceptable, which is why I scrolled all the way down here. The point of the question is very clear: find a way to apply ng-repeat or find an alternative to OP's usage of ng-repeat that eliminates the div artifact in the HTML. Your answer doesn't do this, since there is no way to use it for the two li elements in the question. If I'm wrong here, please provide sample markup that adapts OP's code to eliminate the div element.Unprecedented
@Asad - The point of the question would have been clearer if the OP had provided a finished example of the output he intended. The fact that he accepted the "accepted" answer, where the accepted answer uses bootstrap pagination with essentially improper markup, only muddied the issue further. Example 2 in the jsbin, if that is what is really intended, solves the OP's problem in the way the accepted answer does, but no one should use that type of markup for bootstrap pagination. jsbin.com/eXaPibI/1Malignity
But example 2 is exactly what is in the accepted answer, which is what you're claiming to provide an alternative to. The point of the question "would have been clearer", but is by no means unclear. To quote the question verbatim: "I would like to know is there a way to do the ng-repeat without the div container, or some alternative method to achieve the same effect". The accepted answer does provide a (admittedly imperfect) way to repeat the two elements from the OP's question, whereas the approach you're advocating does not.Unprecedented
In other words, your "cleaner way to use ng-repeat-start and ng-repeat-end" is an antipattern. In all circumstances where you would want to use ng-repeat-start and ng-repeat-end (i.e. you have multiple elements to repeat), using them on the same element would not work. And when considering the one situation where it would work, i.e. you have a single element to repeat, you should be using the vanilla ng-repeat.Unprecedented
@Asad - yes, the final "way".Malignity
Actually, I just looked at the first answer again and the markup generated is exactly right for the OP's use case, even though it doesn't satisfy my requirements. The OP wants a divider li item being generated, which is the extra li you see in your jsbin.Unprecedented
It's a nice answer, and a good answer, but doesn't solve the problem. It's just an ng-repeat. The poster wanted 2 sibling elements repeated each time.Spirometer
Hey.. Asad and @Malignity - So who is correct? can we have an ng-repeat of your conversation? *(runs away)...Ambassador
M
6

ngRepeat may not be enough, however you can combine that with a custom directive. You could delegate the the task of adding divider items to code if you don't mind a little bit of jQuery.

 <li ng-repeat="item in coll" so-add-divide="your exp here"></li>

Such a simple directive doesn't really need an attribute value but might give you lots of possiblities like conditionally adding a divider according to index, length, etc or something completely different.

Missie answered 12/10, 2012 at 20:58 Comment(1)
cool I added a custom directive and it works. This video guide helped: youtube.com/watch?v=A6wq16Ow5EcBernabernadene
B
3

I recently had the same problem in that I had to repeat an arbitrary collection of spans and images - having an element around them was not an option - there's a simple solution however, create a "null" directive:

app.directive("diNull", function() {
        return {
            restrict: "E",
            replace: true,
            template: ""
        };
    });

You can then use a repeat on that Element, where element.url points to the template for that element:

<di-null ng-repeat="element in elements" ng-include="element.url" ></di-null>

This will repeat any number of different templates with no container around them

Note: hmm I could've sworn blind this removed the di-null element when rendering, but checking it again it doesn't...still solved my layout issues though...curioser and curioser...

Biotype answered 17/9, 2013 at 11:54 Comment(2)
replace is deprecated since 2.0.Tenebrous
I tried the above, but template: "" causes that nothing is changed. I used the linker from jsfiddle.net/markcoleman/fMP9s/1 and modified it: link: function (scope, element, attrs) { element[0].outerHTML = ''; $compile(element.contents())(scope); }Igniter
S
2

for a solution that really works

html

<remove  ng-repeat-start="itemGroup in Groups" ></remove>
   html stuff in here including inner repeating loops if you want
<remove  ng-repeat-end></remove>

add an angular.js directive

//remove directive
(function(){
    var remove = function(){

        return {    
            restrict: "E",
            replace: true,
            link: function(scope, element, attrs, controller){
                element.replaceWith('<!--removed element-->');
            }
        };

    };
    var module = angular.module("app" );
    module.directive('remove', [remove]);
}());

for a brief explanation,

ng-repeat binds itself to the <remove> element and loops as it should, and because we have used ng-repeat-start / ng-repeat-end it loops a block of html not just an element.

then the custom remove directive places the <remove> start and finish elements with <!--removed element-->

Shwa answered 20/7, 2019 at 22:40 Comment(1)
This is interesting. However, in my testing replaceWith(...) results in a text node not a comment, though. I've found just using element.remove() works.Ostmark
T
1

There is a comment directive restriction, but ngRepeat doesn't support it (since it needs an element to repeat).

I think I saw the angular team say they would work on comment ng-repeats, but I'm not sure. You should open an issue for it on the repo. http://github.com/angular/angular.js

Thekla answered 12/10, 2012 at 12:4 Comment(1)
Comment from 2012. Is this still the case? Tried to search in their documentation, and couldn't find any reference.Sort
R
1

There is no Angular magic way to do this, for your case you can do this, to get valid HTML, if you are using Bootstrap. Then you will get same effect as adding the li.divider

Create a class:

span.divider {
 display: block;
}

Now change your code to this:

<ul ng-cloak>
 <li div ng-repeat="n in list">
    <a href="{{ n[1] }}">{{ n[0] }}</a>
    <span class="divider"></span>
 </li>
 <li>Additional item</li>
</ul>
Rancid answered 13/5, 2015 at 11:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.