Adding An ngRepeat List Delimiter In AngularJS
Recently, I played around with an AngularJS directive that compiled and transcluded its target element. That experiment left me a little confused as to how the compile step actually worked; so, I wanted to take a step back and look at compiling an element with fewer facets. This time, rather than transcluding, I'm simply going to compile the DOM, adding an additional HTML element. Specifically, I'll be adding a list delimiter to the end of an ngRepeat template.
In my post on using the ngController directive in conjunction with the ngRepeat directive, I added a Span tag to [one of] my ngRepeat lists:
<span ng-repeat="friend in selectedFriends">
{{ friend.name }}
<span ng-show=" ! $last ">-</span>
</span>
This Span tag took care of conditionally showing a list delimiter based on the index of the ngRepeat iteration. You'll notice that the delimiter is a dash, rather than the more commonly-used, "comma." This is because a comma would look strange unless it was butted directly up against the content in the ngRepeat:
<span ng-repeat="friend in selectedFriends">
{{ friend.name }}<span ng-show=" ! $last ">,</span>
</span>
To me, this code is less visually pleasing; and code that is less visually pleasing makes me die a little bit inside. As such, I thought this would make a good AngularJS directive - I could encapsulate the awkward placement of the Span inside an easy-to-read attribute.
In the following demo, I'll be adding the same Span tag; however, I'll be adding it in a compile step within the bnRepeatDelimiter directive. Notice that the the trailing whitespace of the ngRepeat template is maintained, even as the delimiter is injected.
<!doctype html>
<html ng-app="Demo" ng-controller="DemoController">
<head>
<meta charset="utf-8" />
<title>
Adding An ngRepeat List Delimiter In AngularJS
</title>
</head>
<body>
<h1>
Adding An ngRepeat List Delimiter In AngularJS
</h1>
<form ng-submit="addFriend()">
<input type="text" ng-model="newFriendName" size="20" />
<input type="submit" value="Add Friend" />
</form>
<p ng-show="friends.length">
<strong>Friends</strong>:
<!--
Notice that we define the comma as the list delimiter
for our ngRepeat directive.
-->
<span
ng-repeat="friend in friends"
bn-repeat-delimiter=",">
{{ friend }}
</span>
</p>
<!-- Load jQuery and AngularJS from the CDN. -->
<script
type="text/javascript"
src="//code.jquery.com/jquery-1.9.1.min.js">
</script>
<script
type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
</script>
<!-- Load the app module and its classes. -->
<script type="text/javascript">
// Define our AngularJS application module.
var demo = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I am the main controller for the application.
demo.controller(
"DemoController",
function( $scope ) {
// -- Define Scope Methods. ----------------- //
// I add a new friend to the list and reset the model.
$scope.addFriend = function() {
$scope.friends.push( $scope.newFriendName );
$scope.newFriendName = "";
};
// -- Define Scope Variables. --------------- //
// I am the list of friends to show.
$scope.friends = [ "Sarah" ];
// I am the name of the new friend being added.
$scope.newFriendName = "Joanna";
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I add a delimiter to the end of the list items. This is
// designed to be used in conjunction with ngRepeat. It will
// add the delimiter to all list items; but it will only show
// on all but the $last ngRepeat item.
demo.directive(
"bnRepeatDelimiter",
function() {
// I compile the list, injecting in the conditionally
// visible delimiter onto the end of the template.
function compile( element, attributes ) {
// Get the delimiter that goes between each item.
var delimiter = ( attributes.bnRepeatDelimiter || "," );
// The delimiter will show on all BUT the last
// item in the list.
var delimiterHtml = (
"<span ng-show=' ! $last '>" +
delimiter +
"</span>"
);
// Add the delimiter to the end of the list item,
// making sure to add the existing whitespace back
// in after the delimiter.
var html = element.html().replace(
/(\s*$)/i,
function( whitespace ) {
return( delimiterHtml + whitespace );
}
);
// Update the compiled HTML.
element.html( html );
}
// Return the directive configuration. Notice that
// our priority is 1 higher than ngRepeat - this will
// be compiled before the ngRepeat compiles.
return({
compile: compile,
priority: 1001,
restirct: "A"
});
}
);
</script>
</body>
</html>
As you can see, I am using the jQuery html() method to read and then replace the content of the compiled element. Furthermore, I am capturing the trailing whitespace of the ngRepeat element so that I can maintain it even after I inject my Span-based delimiter. This allows me to butt the delimiter right up against the rendered content of the repeater without corrupting the overall source.
In this experiment, I have configured AngularJS to compile my directive before the ngRepeat directive is compiled. Since ngRepeat compiles with a priority of 1000, the bnRepeatDelimiter - with a priority of 1001 - will compile first. Based on the highly dynamic nature of ngRepeat, this feels like the right order of operations. But, as it turns out, having a priority lower than 1000 leads to the same outcome. I suppose that it because the ngRepeat element is only compiled once, no matter what - its repetition is based on clone-transclusion during the linking phase, not the compile phase.
Clearly, this kind of list delimiter would only work with certain kinds of lists (namely inline, textual lists). But, the real point here was simply to explore the compile phase of the compile-link lifecycle. AngularJS directives hold a tremendous amount of power; but, really mastering them is going to take a good deal of time and practice.
Want to use code from this post? Check out the license.
Reader Comments
Great post, Keep your 'thumb up' post about Angular :)
Great post ,,,^..^,,
And you have a typo here:
@Kodai,
Thanks my man! Will do!
@Yaroslav,
Oh snap, good eyes! Looks like my spell-checker didn't catch that one. It seems to be weird about what it will / won't check.
Hi Ben, I really love your posts on angularjs.
It seems that every time something new catches my eyes you either have just started looking into it or will start shortly after - Sometimes I even just wait with diving into the subject until after you have dug the hole a little deeper :)
On a side note to this post. Do you need jquery for the html manipulations when angularjs comes with an jquerylite addition?
http://docs.angularjs.org/api/angular.element
Hi Ben,
I changed the priority of the
to 999, but i'm getting the same html strucutre, as when it was in 1001. Could you please explain how that differs.
@Morten,
I guess great minds think alike ;)
As far as the jQuery "lite" that comes with AngularJS, there are things you can do out of the box and things you can't do. I've been hit by the difference a number of times, so I just include the core jQuery by default (even if I don't need it). Then, if I have to add something like show() or hide() or slideDown() or a find() with CSS selectors, I don't have to then go back and add jQuery.
But, as long as you're not doing too much CSS oriented, I think the built-in "lite" version is pretty good.
@Rajkamal,
When I first wrote this, I used 1001 because it *felt* right to have to have it compile *before* the ngRepeat compiles. But, as you have seen, 1001 vs. 999 makes no difference.
I'm still trying to fully-understand the compile/link life-cycle, but I think in this case it doesn't make a difference since the ngRepeat directive only compiles a single element as well. Even though ngRepeat can result in multiple elements, those only appear in the LINK phase as part of the cloning process, after the compile has taken place.
I know that probably didn't help clarify it too much; and that's because my understanding isn't too strong yet.
Hey Ben,
Your demo's on angular have been very useful. When you start needing angular to do things that normally were easy in backbone/jquery it gets pretty confusing, but you seem to be figuring it out pretty well, and its helpful referring to these posts for tips and clarification.
My question isn't really angular related but something i have been curious about for some time while reading your posts. I noticed that you usually wrap any variables or objects, any return statements in parentheses. I'm just wondering is that a preference of yours, or is it better javascript coding practice? Mostly wondering if its something i should be doing that i dont know about.
Example:
Thank you
@John,
First, I'm glad that you are finding these posts helpful. AngularJS is pretty darn awesome; but, when you want to do some more UI-based shenanigans, it definitely makes you jump through a few more hoops than something like jQuery (vanilla) or other JS frameworks might. That said, I love that it forces you to keep a stricter separation of concerns.
The one thing that I wish I could do is "namespace" my directives. I haven't really come up with a good strategy for that yet.
Ok, so as for the formatting, ALL of that stuff is just personal preference. If you continue to read my blog, you'll come to find that I love a few things:
* White space.
* Comments (more in my demos than in my production code).
* Parenthesis to group anything that seems groupable.
* Parenthesis around my return values.
* Semicolons.
* Line lengths of less than or equal to 70 chars (for the blog content display).
But, that's all personal preference. Some of it is influenced by the fact that I'm "thinking outloiud" in my code, hence the comments. I happen to think that my choices make my code more readible... for ME, but others downright dislike the way I do things :)
Excellent post, directives are definitely the trickiest part of Angular to wrap your head around.
The comma-delimited ngRepeat is an actual use case I've had recently as well and found CSS to be a suitable enough for my particular situation.
@Scott,
Awesome use of CSS :) I've only recently started to use :before / :after pseudo-elements and they are hugely useful. I haven't gotten too much into the last/first child stuff.
Honestly, I did not realize that you could *override* the *content* value of the :after element. That's pretty bad ass! Thanks for the tip!
i need some like this:
<ul>
{% for foo in bar %}
{% if foo == 'some' %}
<li>------</li>
{% endif %}
<li>{{ foo }}</li>
{% endfor %}
</ul>
<span ng-repeat="item in data.prog.fields_of_study" ng-bind="item.name + ($last ? '' : ', ')"></span>
Yet again you saved me a shit load of time! Thanks Ben!