Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Aaron Foss

Using ngController With ngRepeat In AngularJS

By Ben Nadel on

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.

Tweet This Groovy post by @BenNadel - Using ngController With ngRepeat In AngularJS Thanks my man — you rock the party that rocks the body!



Reader 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?

Reply to this Comment

@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.

Reply to this Comment

@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.

Reply to this Comment

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

Reply to this Comment

@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.

Reply to this Comment

@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!

Reply to this Comment

@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.

Reply to this Comment

@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.

Reply to this Comment

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

Reply to this Comment

@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.

Reply to this Comment

@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.

Reply to this Comment

@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!

Reply to this Comment

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});
}
}

Reply to this Comment

@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.

Reply to this Comment

Thanks for post, it helped me re-factor Find a Dentist for a dental plan website and will should client very happy. Wound up using Mateus's plunker as I originally had selection issues when converting example to jade.
John

Reply to this Comment

I was wondering if you have time for a nube Angular.js question. I am trying to do the same type of thing you do here but everytime I put the ng-repeat on it doesn't print the data in the object.
The page

  • <script src="Scripts/angular.js"></script>
  • <script src="Scripts/FormLockingMonitorScripts.js"></script>
  • <div ng-app="myApp" style="width: 600px;">
  • <div ng-controller="MainCtrl">
  • <h2>{{ text }}</h2>
  • </div>
  • <div ng-controller="FormsCtrl" style="width: 100%;">
  • <table style="width: 100%;">
  • <thead>
  • <tr>
  • <th style="border: 1px solid blue;">User</th>
  • <th style="border: 1px solid blue;">Form</th>
  • <th style="border: 1px solid blue;">Locked Since</th>
  • <th style="border: 1px solid blue;"></th>
  • </tr>
  • </thead>
  • <tbody>
  • <hr />
  • <tr ng-repeat="Form in Form.details">
  • <td style="width: 30%;">{{ Form.details.UserName }}</td>
  • <td style="width: 30%; text-align: center;">{{ Form.details.FormName }}</td>
  • <td style="width: 30%; text-align: center;">{{ Form.details.FormLockedTime }}</td>
  • <td style="width: 10%;">
  • <input type="button" title="Unlock" value="Unlock" /></td>
  • </tr>
  • </tbody>
  • </table>
  • </div>
  • </div>

The Controller

  • myApp.controller('FormsCtrl', ['$scope', function ($scope) {
  • $scope.Form = {};
  • $scope.Form.details =
  • {"UserName": "Sanders_John", "FormName": "Page 1", "FormLockedTime": "5:30 pm"},
  • { "UserName": "Willsey_Bob", "FormName": "Admin Placement", "FormLockedTime": "6:30 am" };
  • }]);

Thanks in advance.
JD

Reply to this Comment

Thanks for this, I saw straight away why my required horizontal nav, was vertical, I'd put the directive on the ul and not the li. Kapow! (Though I still feel it should be on the ul...)
Thanks U

Reply to this Comment

Hi Ben,

I'm working on an Angular app write now. I'm using the controller on ng-repeat stuff but I found out that, for a reason I don't know yet, each controller is called 3 times !!
I would expect the controller to be instantiate one time per items.
I think it has something to do with Angular lifecycle and the $compile, $apply, $digest loops but I can't find the exact explanation and how to fix it !!!

The issue with this behaviour is that I set watchers in my controller (with $watch) but the controller is called 3 times for each items in my ng-repeat, the $watch is also called 3 times !! See what I mean ?

Perhaps you would have a good explanation for that ?

Thank you very much.
Boris.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.