3

Assuming a controller is there to manipulate some data on the scope, what is the best practice to supply the controller with that data?

For example, if I have a list of items I might want a ListController to manipulate the list, and an ItemController to manipulate an individual item. But how do I give each ItemController an item to use?

In the case below, how would doSomethingToItem access the item. When nested in the ngRepeat the item is $scope.item, but in the selection view the item we want to manipulate is $scope.selection.

angular.module('MyApp', [])

    .controller('ListController', function($scope, $http) {

        $scope.list = $http.get(...);

        $scope.selection = null;

    })

    .controller('ItemController', function($scope) {

        $scope.doSomethingToItem = function() {
            ...
        };

    })


<div ng-controller="ListController">
    <div ng-repeat="item in list" ng-click="selection = item">
        <div ng-controller="ItemController">
            <button ng-click="doSomethingToItem()">Do Something</button>
        </div>
    </div>
    <div ng-show="selection"
        <div ng-controller="ItemController">
            <button ng-click="doSomethingToItem()">Do Something</button>
        </div>
    </div>
</div>

Isn't this a common structure, or is my thinking backwards?

2 Answers 2

2

You should understand that Angular would create n+1 ItemController in your case. N for items and 1 for the selection section.

To make passing the object that needs to be worked on easier you can change the method signature to

doSomethingToItem(item) and in html do

<button ng-click="doSomethingToItem(item)">Do Something</button> at both places.

Or else for the repeat case the item variable contains your object that you can access in ItemController

selection variable contains the reference to the select controller, which can be reference from the instance of the controller defined under selection section.

Update: The expression in ng-repeat and selection would differ

<button ng-click="doSomethingToItem(item)">Do Something</button>

and

 <div ng-show="selection"
        <div ng-controller="ItemController">
            <button ng-click="doSomethingToItem(selection)">Do Something</button>
        </div>
    </div>
Sign up to request clarification or add additional context in comments.

3 Comments

I don't think that would work either? When you use a variable in a template, it's bound to $scope. Therefore I would expect doSomethingToItem(item) to try and resolve $scope.item inside ItemController. That would be undefined.
Passing item into doSomethingToItem() is a good idea, but you lose a lot. At that point you might as well put the function on the ListController to avoid running the ItemController N+1 times like you point out. The reason you'd use an ItemController is to get that scope so you could do things like $scope.$on('some-event', function() { ... }); or $scope.$watch('expression', function() { ... }); How would those callbacks know what the item was?
For eventing this would be problem not on watch. Eventing needs to pass in the data object it is working on.
1

You could pass the item data model like this:

<div ng-init="instance = item" ng-controller="ItemController">
</div>

"instance" will be a reference to list array data model item in "ListController".

And you could access its property in your ItemController function closure:

.controller("ItemController", function($scope){
  $scope.instance={};
  $scope.doSomething = function(){
    console.log($scope.instance.name);
  }
  $scope.$watch('instance',function(){
    console.log("iitem changed");
  },true);
});

I'm not quite sure what feature do you want to achieve in your "selection" implementation.

I think you want to implement a selected list and list item will be added to it when user clicked the list item. You could try to create a "selected list" model to control the selected list view if you want to add the selected item to a list.

ListController.js

.controller("ListController", function($scope){
  $scope.selectedList = [];
  $scope.addItem = function(item){
    $scope.selectedList.push(item);
  }
});

HTML

<div ng-repeat="selected in selectedList">
  <div ng-init="instance = selected" ng-controller="ItemController">
    <span>{{instance.name}}</span>
    <button ng-click="doSomething()">selectedAction</button>
  </div>
</div>

I wrote a simple multi-selected list example as following:

HTML

<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>Nested controller</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="js/nestedController.js"></script>
</head>
<body>
<div ng-controller="parentCtrl">
    <h2>List</h2>
    <div ng-repeat="item in list">
        <input type="checkbox" ng-model="item.selected" ng-checked="item.selected"/>
        <div ng-init="instance = item" ng-controller="childCtrl">
            <span>{{instance.name}}</span>
            <button ng-click="doSomething()">doSomething</button>
        </div>          
    </div>
    <h2>Selected</h2>
    <div ng-repeat="selected in selectedList">
        <div ng-init="instance = selected" ng-controller="childCtrl">
            <span>{{instance.name}}</span>
            <button ng-click="selectedAction()">selectedAction</button>
        </div>
    </div>
</div>

JS

angular.module("myApp",[])
.controller("parentCtrl",function($scope){
  //test data
  $scope.list = [{name:'item1',age:'12',selected:false},{name:'item2',age:'18',selected:false}];
  //use model to control selected list view
  $scope.selectedList = [];
  //refresh the selected list model when the list checked stauts has been updated
  $scope.$watch('list',function(){
    console.log("parent controller detected change");
    $scope.selectedList = [];
    $scope.list.forEach(function(elem,index,array){
        if(elem.selected===true){
            $scope.selectedList.push(elem);
        }
    });
  },true);
})
.controller("childCtrl",function($scope){
  $scope.instance={}
  $scope.doSomething = function(){
    alert("I'm the item: "+$scope.instance.name);
  }

  $scope.selectedAction = function(){
    alert("I'm the selected item: "+$scope.instance.name);
  }
  //could register a watcher to monitor the model status
  $scope.$watch('instance',function(){
    console.log("child controller detected change");
  },true);
});

Here is the jsFiddle demo

Hope this is helpful.

1 Comment

That is a great idea. Thanks. I hesitate to use ngInit because of this line in the angularjs docs: "The only appropriate use of ngInit for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope." Where they used it for nested ngRepeats: ng-repeat="innerList in list" ng-init="outerIndex = $index". Though they don't explain why they make this comment.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.