Using ngController With ngRepeat In AngularJS

Posted February 15, 2013 at 4:20 PM by Ben Nadel

Tags: Javascript / DHTML

Most of the time, when you use the ngController directive in AngularJS, you're associating a Controller with a relatively static part of your user interface (UI). When it comes to ngRepeat, however, you can still use ngController - you just have to realize that you're creating a controller instance for every clone that gets created in the ngRepeat loop. These controllers provide you with a clean way of exposing per-item behavior to your end user.


 
 
 

 
  
 
 
 

When the ngRepeat directive executes, AngularJS creates a new $scope instance for each template clone. It then puts your iteration cursor reference in that $scope. So, if we had an ngRepeat loop that looked like this:

  • <li ng-repeat="friend in friends"> .. </li>

... AngularJS would create a $scope instance for every LI instance that it generated; then, it would put the "friend" cursor reference inside that per-item $scope.

If we then added an ngController directive to the LI template, the given controller would be instantiated once per-item and the item-specific $scope would be injected into the item-specific controller. This allows our per-item controller to maintain per-item context as it exposes behavior.

This isn't always necessary; but, when it is, it's rather powerful.

To demonstrate, I've put together an interface that uses a list with per-item interface requirements. Specifically, each list item has a hover state and a selected state. Furthermore, as each item is selected, its selection is echoed in a secondary list of selected items.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="DemoController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Using Controllers With ngRepeat In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • ul {
  • list-style-type: none ;
  • margin: 16px 0px 22px 0px ;
  • padding: 0px 0px 0px 0px ;
  • }
  •  
  • ul:after {
  • clear: both ;
  • content: "" ;
  • display: block ;
  • }
  •  
  • li {
  • background-color: #F0F0F0 ;
  • border: 1px solid #CCCCCC ;
  • border-radius: 4px 4px 4px 4px ;
  • cursor: pointer ;
  • float: left ;
  • height: 70px ;
  • margin: 0px 16px 0px 0px ;
  • text-align: center ;
  • width: 160px ;
  • }
  •  
  • li.selected {
  • border-color: #CC0000 ;
  • }
  •  
  • span.name {
  • display: block ;
  • font-size: 18px ;
  • padding: 14px 0px 10px 0px ;
  • }
  •  
  • span.nickname {
  • color: #666666 ;
  • display: block ;
  • font-size: 14px ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • Using Controllers With ngRepeat In AngularJS
  • </h1>
  •  
  • <!--
  • List of friend - each item in the ngRepeat directive gets
  • its own instance of the ItemController.
  • -->
  • <ul>
  •  
  • <li
  • ng-repeat="friend in friends"
  •  
  • ng-controller="ItemController"
  • ng-click="toggleSelection()"
  • ng-mouseenter="activate()"
  • ng-mouseleave="deactivate()"
  • ng-class="{ selected: isSelected }">
  •  
  • <span class="name">
  • {{ friend.name }}
  • </span>
  •  
  • <span ng-show="isShowingNickname" class="nickname">
  • aka {{ friend.nickname }}
  • </span>
  •  
  • </li>
  •  
  • </ul>
  •  
  • <!-- List of selected friends. -->
  • <p ng-show="selectedFriends.length">
  •  
  • <strong>Selected Friends</strong>:
  •  
  • <span
  • ng-repeat="friend in selectedFriends">
  •  
  • {{ friend.name }}
  •  
  • <span ng-show=" ! $last ">-</span>
  •  
  • </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 remove the given friend from the list of
  • // selected friends.
  • $scope.deselectFriend = function( friend ) {
  •  
  • // NOTE: indexOf() works in IE 9+.
  • var index = $scope.selectedFriends.indexOf( friend );
  •  
  • if ( index >= 0 ) {
  •  
  • $scope.selectedFriends.splice( index, 1 );
  •  
  • }
  •  
  • };
  •  
  •  
  • // I add the given friend to the list of selected
  • // friends.
  • $scope.selectFriend = function( friend ) {
  •  
  • $scope.selectedFriends.push( friend );
  •  
  • };
  •  
  •  
  • // -- Define Scope Variables. --------------- //
  •  
  •  
  • // I am the list of friends to show.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Tricia",
  • nickname: "Sugar Pie"
  • },
  • {
  • id: 2,
  • name: "Joanna",
  • nickname: "Honey Dumpling"
  • },
  • {
  • id: 3,
  • name: "Kit",
  • nickname: "Sparky"
  • }
  • ];
  •  
  •  
  • // I am the list of friend that have been selected
  • // by the current user.
  • $scope.selectedFriends = [];
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the controller for the list item in the ngRepeat.
  • // Each instance of the LI in the list will bet its own
  • // instance of the ItemController.
  • demo.controller(
  • "ItemController",
  • function( $scope ) {
  •  
  •  
  • // -- Define Scope Methods. ----------------- //
  •  
  •  
  • // I deactivate the list item, if possible.
  • $scope.deactivate = function() {
  •  
  • // If the list item is currently selected, then
  • // ignore any request to deactivate.
  • if ( $scope.isSelected ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.isShowingNickname = false;
  •  
  • };
  •  
  •  
  • // I activate the list item.
  • $scope.activate = function() {
  •  
  • $scope.isShowingNickname = true;
  •  
  • };
  •  
  •  
  • // I toggle the selected-states of the current item.
  • // Remember, since ngRepeat creates a new $scope for
  • // each list item, we have a reference to our
  • // contextual "friend" instance.
  • $scope.toggleSelection = function() {
  •  
  • $scope.isSelected = ! $scope.isSelected;
  •  
  • // If the item has been selected, then we have to
  • // tell the parent controller to selected the
  • // relevant friend.
  • if ( $scope.isSelected ) {
  •  
  • $scope.selectFriend( $scope.friend );
  •  
  • // If the item has been unselected, then we have
  • // to tell the parent controller to DEselected the
  • // relevant friend.
  • } else {
  •  
  • $scope.deselectFriend( $scope.friend );
  •  
  • }
  •  
  • };
  •  
  •  
  • // -- Define Scope Variables. --------------- //
  •  
  •  
  • // I determine if the nichkame is showing.
  • $scope.isShowingNickname = false;
  •  
  • // I determine if the list item has been selected.
  • $scope.isSelected = false;
  •  
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

In this demo, the per-item controller is responsible for updating the display as local user interactions take place. When an item is selected, however, it does have to communicate this selection back up to the parent controller. This way, the parent controller will know when to add and remove items from its "selected" collection.

By dividing the responsibilities up between the parent controller and the per-item controller, it allows each of our controllers to stay small and relatively cohesive.




Reader Comments

Feb 15, 2013 at 4:36 PM // reply »
28 Comments

I like the idea but performance concerns me. It is creating 1000 controllers for a 1000 item list. Yes, 1000 item list is extreme but you get my point.

Have you tested the performance of something like this on a larger list of items?


Feb 15, 2013 at 4:46 PM // reply »
11,233 Comments

@John,

I think performance *can* definitely become a problem. I think it's one of those things where you need to roll with the punches. For the app I'm building, we just do "normal" approach first; then we when we performance becoming an issue, we take steps to fix that particular interface.

And, Yes, we have seem a few issues with performance, typically with large datasets and complex user interactions. But, for the most part, performance has not been a concern at all.


Feb 15, 2013 at 4:47 PM // reply »
11,233 Comments

... and, for cases where things to become a concern, I've begun investing the use of more "delegate"-style approaches, such as using jQuery's event delegation:

http://www.bennadel.com/blog/2448-Using-jQuery-Event-Delegation-In-AngularJS.htm

... but, honestly, it hasn't been much a problem except for a few very complex UIs.


Feb 15, 2013 at 4:48 PM // reply »
28 Comments

@Ben,

Yeah, I figured it would be pretty quick and I'm not against the idea (actually dig it) but that was the first thing that came to mind.

Keep rockin' Angular posts man. Loving them.


Feb 15, 2013 at 4:53 PM // reply »
11,233 Comments

@John,

Thanks my man! I'll keep at it - hopefully get some good stuff out here :)


Feb 18, 2013 at 8:31 AM // reply »
7 Comments

Reminds me of backbone where a list is often times is a view + model - even if just a smaller set of code.

It does give it much more of a OO approach


Feb 18, 2013 at 9:23 AM // reply »
11,233 Comments

@Steve,

Yeah, I definitely like the granularity in the control. As I've been learning about AngularJS, I've created a few HUGE Controllers. It would be nice to go back and see if a good splitting / refactoring into smaller Controllers would feel good. Still trying to figure out where the balance is.


Feb 27, 2013 at 2:38 PM // reply »
3 Comments

@Ben,

nice example, however I think this kind of selection can be easily achieved without having to instantiate a controller for each repeater item. Having the selection logic in one place makes it easier to read and understand (as you don't have to do this "implicit" parent scope method call, e.g line #245 in your example).

To illustrate my approach I made plunker with your (refactored) example: http://plnkr.co/edit/HUsWAwk2HlemssUncr5H?p=preview

Great blog!


Feb 27, 2013 at 2:48 PM // reply »
3 Comments

Btw, I've found your blog via my github where someone posted link to this entry: https://github.com/matys84pl/angularjs-requirejs-lazy-controllers/issues/4


Feb 27, 2013 at 2:55 PM // reply »
11,233 Comments

@Mateusz,

Definitely not all situations merit the use of a repeat-based controller. It's all about trade-offs. For example, in your Plunkr, you tie your dynamic class to an isSelected() method in the View. This means that AngularJS has to call that method every single time it runs a $digest (which can be very often). In this example, that work is fairly trivial; in a larger example with a larger collection, it may not be.

Sometimes, I use your approach, but I actually augment the "model" itself to use view-based properties. So, I'll actually just inject the "isSelected" property into the friend itself:

ng-class="{ selected: friend.isSelected }"

... then my select/unselect methods will just toggle the property on the friend object directly. This makes it very easy to read; but, it requires more upfront preparation to make sure that all your friends are injected with the default property.

Definitely, each approach has a pros/cons. I don't feel terribly strongly for any one.


Feb 27, 2013 at 3:09 PM // reply »
28 Comments

@Ben,

Agreed on calling a method for every digest. I'm working with our team to use that sparingly.

Easy or "can" is not always the best "should" answer. Simply put, as you say, pros and cons.


Feb 27, 2013 at 3:16 PM // reply »
7 Comments

I realized something else with this sample - what I originally thought was happening was that for each item in the repeater it would have it only controller AND partial view loaded.

This was what I was thinking reminded me of Backbone, but I think it's not the same. ie. in Backbone, you would have a collection and then for each item it has an object and it's partial view.

Now I see, it's just a second controller, but still same view, just spreading out the functionality across multiple controllers ?

ie. my sample - was let's say you had a controller that is loaded from a route and contains a list of 'people'.

Then for each person - you want to split that view/edit code out into it's own partial and controller to separate concerns.

PeopleController
ie ng-repeat=person in people

inside each person:
PersonController
it has a partial view


Feb 27, 2013 at 3:27 PM // reply »
3 Comments

@Ben and @John,

of course I agree with you, it's never a black or white decision as you can have multiple solutions and all with their own drawbacks.

Regarding the digest cycle, your "{selected: isSelected}" and "isShowingNickname" also will be checked/validated every time. That's why I've used a map to store selected values to keep the complexity of the check at minimum.

friend.isSelected - I always try to avoid storing view specific (?) data in domain objects, but I have to agree that this is the simples possible way.


Feb 27, 2013 at 3:33 PM // reply »
11,233 Comments

@Steve,

Yeah, exactly correct. The one caveat with AngularJS is that each "clone" of the ngRepeat loop gets its own $scope, which extends the $scope of its parent container. So, each "sub controller" that gets instantiated gets its own unique "sub $scope." This can be a bit funky when it comes to read/writes to the scope as it can lead to unexpected outcomes if you don't fully grasp prototypal inheritance and how properties get read vs. how they get written.


Feb 27, 2013 at 3:35 PM // reply »
11,233 Comments

@Mateusz,

Ahh, sorry, I read your Plunkr too fast :) If you are caching the selectedness of a given friend in a hash, then yeah, that would be very performant. I love using hashes as look-up tables for things. Good move!


Mar 11, 2013 at 5:50 PM // reply »
1 Comments

I recently jumped into Angular and Ben your blog has been extremely insightful. I've been trying to figure out whether to use ng-repeat with a cloned controller like you have here, or use a complex series of directives.

Basically our app is a 4 level form setup with a RESTful design. There are various templates possible for each level and we would need to be able to create new forms, load existing forms to edit/delete, and dynamically add/delete objects within each level.

Do you think it would make sense to have controllers for each object of each level? Here is an example of the code we're working with.

Example object previously loaded via REST:
example = {
...
"pages" : ['/api/alpha/page/1', '/api/alpha/page2', ... ],
...
}

In the HTML:
<div ng-repeat="pageURL in example.pages" ng-init="expand()" ng-controller="PageCtrl"></div>

In controllers.js:
function ($scope, $http) PageCtrl {
$scope.expand = function () {
$scope.response = $http({method:"GET", url:$scope.pageURL});
}
}


Mar 23, 2013 at 9:28 AM // reply »
11,233 Comments

@Michael,

I'm glad you're enjoying the AngularJS posts. I've been loving AngularJS; but some of it is definitely complex! This (your) kind of nested data display is something I've been trying to think a lot about lately. In an app that I'm building, we have several displays where we have a "master" entity, and then several tabs, each of which has its own data. Trying to figure out where to use Controllers, Directives, and how granular to make those Directives has been an ongoing journey.

The more I play around with nested Controllers, the more I like them. But, with a nested controller, there is the question about where does the data come from? Does the master controller load the data and then simply use the nested controllers for help in display? Or, can the nested controllers also make requests for data such that they render a combination of inherited ($scope) data and $resource data?

I'm still grappling with this question. Currently, I tend to treat controllers as either 100% inherited data; or, 100% independent data. Meaning, a Controller either gets all of its data from the parent controller; or, it gets all of its data from the service layer. I haven't really experimented with a mixed-data-source approach yet; but, I feel like I need to think more deeply about it. The complete separation, one way or the other, has simply been the easiest way that I can think about the scope of the behavior.

Now, Directives are also very interesting.

I'd say when it comes to directives, try to think about the smallest behavior first, to see if you can create a directive that enables JUST that behavior. A lot of times, you can use 2 or more small, independent behaviors to create complex behavior.

When you need to "coordinate" lots of behavior, this small directive approach can start to fall short. When I get to that kind of a situation, I start to create UI "helpers". As in, "contact-form-helper", that will help coordinate the UI transitions and behaviors across a cohesive UI.

Sorry, I know I'm kind of rambling here; and that's because I don't really have a good answer. Sometimes, directives can be completely abstract; sometimes, they have to be tightly tied to a given interface. Some of it is trial and error; some of it is just learning to think more "generically" about behavior.

I'll try to come up with some more nested Controller examples as this is something I really want to investigate myself.


Apr 5, 2013 at 1:31 AM // reply »
9 Comments

in order to have a per item behaviour, I used to contrive a directive. That was before I read this valuable post of you :)


Apr 5, 2013 at 9:23 AM // reply »
11,233 Comments

@Aladdin,

Awesome - glad I could help provide an alternate implementation.



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools