Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Aaron Lerch
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Aaron Lerch

Using ngController With ngRepeat In AngularJS

By
Published in Comments (34)

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.

Want to use code from this post? Check out the license.

Reader Comments

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?

15,902 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.

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.

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

15,902 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.

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!

15,902 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.

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.

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

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.

15,902 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.

15,902 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!

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

15,902 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.

1 Comments

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

1 Comments

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

1 Comments

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

1 Comments

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.

1 Comments

I have a list of values, in which one of the value is " not applicable".. i am using ng repeat to fill in the values.. Now, when I check the checkbox of " not applicable" , I want the other check boxes to be disabled...
and the vice versa....i.e., when I check any of the other values other than " not applicable" , I want the " not applicable"checkbox to be disabled..
can u please tell how do I do this...

2 Comments

Great post and really useful for my current problem: I wanted to load additional information for each item in my collection. By attaching a controller to each item, this solves the problem very elegantly. The asynchronous attempt didn't destroy the responsiveness of my side. Thanks a lot!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel