19

I want to use a controller on 2 seperated HTML elements, and use the $rootScope to keep the 2 lists in sync when one is edited:

HTML

<ul class="nav" ng-controller="Menu">
    <li ng-repeat="item in menu">
        <a href="{{item.href}}">{{item.title}}</a>
    </li>
</ul>

<div ng-controller="Menu">
    <input type="text" id="newItem" value="" />
    <input type="submit" ng-click="addItem()" />
    <ul class="nav" ng-controller="Menu">
        <li ng-repeat="item in menu">
            <a href="{{item.href}}">{{item.title}}</a>
        </li>
    </ul>    
</div>

JS

angular.module('menuApp', ['menuServices']).
    run(function($rootScope){
        $rootScope.menu = [];
    });

angular.module('menuServices', ['ngResource']).
    factory('MenuData', function ($resource) {
        return $resource(
            '/tool/menu.cfc', 
            {
                returnFormat: 'json'
            },
            {
                getMenu: {
                    method: 'GET',
                    params: {method: 'getMenu'}
                },
                addItem: {
                    method: 'GET',
                    params: {method: 'addItem'}
                }
            }
        );
    });

function Menu($scope, MenuData) {

    // attempt to add new item
    $scope.addNewItem = function(){
        var thisItem = $('#newItem').val();

        MenuData.addItem({item: thisItem},function(data){
            $scope.updateMenu();
        });
    }   

    $scope.updateMenu = function() {
        MenuData.getMenu({},function(data){
            $scope.menu = data.MENU;
        });         
    }

    // get menu data
    $scope.updateMenu();
}

When the page loads, both the UL and the DIV display the correct contents from the database, but when i use the addNewItem() method only the DIV gets updated.

Is there a better way to structure my logic, or can I do something to make sure the $scope.menu in the UL gets updated at the same time?

Here's an example of something similar: http://plnkr.co/edit/2a55gq

I159
  • 29,741
  • 31
  • 97
  • 132
Pete
  • 4,542
  • 9
  • 43
  • 76
  • You could hold the menu in a service along with the methods to update the menu itself, and have the service broadcast the updated menu via $rootScope.$broadcast. The controllers have listen the event with $scope.$on and update the locale reference to the menu via $scope.menu = ... – David Riccitelli Nov 01 '12 at 16:03
  • I updated my answer in case you wanna know how to do it in directive. – maxisam Nov 01 '12 at 16:37
  • This answer includes a very helpful video as well: http://stackoverflow.com/a/13882619/109941 – Jim G. Sep 05 '13 at 00:58

3 Answers3

43

I would suggest to use a service that holds the menu and its methods. The service will update the menu which is referenced by the controller(s).

See a working plunker here: http://plnkr.co/edit/Bzjruq

This is the sample JavaScript code:

angular
 .module( 'sampleApp', [] )
 .service( 'MenuService', [ '$rootScope', function( $rootScope ) {

   return {
      menu: [ 'item 1' ],
      add: function( item ) {
        this.menu.push( item );
      } 
   };

 }])
 .controller( 'ControllerA', [ 'MenuService', '$scope', function( MenuService, $scope ) {

   $scope.menu = MenuService.menu;

   $scope.addItem = function() {
    MenuService.add( $scope.newItem );  
   };

 }]);

And the sample Html page:

<!DOCTYPE html>
<html>

  <head lang="en">
    <meta charset="utf-8">
    <title>Custom Plunker</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-app="sampleApp">

    <div ng-controller="ControllerA">
      <ul>
        <li ng-repeat="item in menu">{{item}}</li>
      </ul>
      <input type="text" ng-model="newItem" /><input type="submit" ng-click="addItem()" />
    </div>

    <div ng-controller="ControllerA">
      <ul>
        <li ng-repeat="item in menu">{{item}}</li>
      </ul>
    </div>

  </body>

</html>
David Riccitelli
  • 7,491
  • 5
  • 42
  • 56
  • That's the stuff, I was just looking into this after you posted that comment, thanks! – Pete Nov 01 '12 at 16:16
  • 2
    Won't `$scope.menu` automatically be updated when `MenuSerice.add` is called, even without the `$rootScope.$broadcast` and `$scope.$on` stuff? Since `$scope.menu` points directly to `MenuService.menu`, it's not a copy of it. – KajMagnus Apr 15 '14 at 03:18
  • @KajMagnus thanks, I updated the answer and the related Plunker. – David Riccitelli Apr 15 '14 at 06:35
  • So is it a very bad practice to attach same controller to many view elements? Or are there any times during development, that such solution is acceptable? I see no errors in webdev tools console, when running webapp which has such dirty hack (?). – spaffy Apr 30 '15 at 16:32
5

Edit:

Here is the updated version plunker. it works in two controller.

Main idea is using service and broadcast to sync the data with the directive.

app.service('syncSRV', function ($rootScope) {
    "use strict";
    this.sync = function (data) {
        this.syncData = data;
        $rootScope.$broadcast('updated');
    };
});
app.controller('MainCtrl1', ['$scope', function ($scope) {
    }])
    .controller('MainCtrl2', ['$scope', function ($scope) {
    }]);
app.directive('sync',function (syncSRV) {
    "use strict";
    return {
        template: '<div><input ng-model="syncdata" type="text" /></div> ',
        controller: function ($scope, $element, $attrs) {
            $scope.$watch('syncdata', function (newVal, oldVal, $scope) {
                syncSRV.sync(newVal);
            }, true);
        }
    };
}).directive('dataview', function (syncSRV) {
    "use strict";
    return {
        template: '<div>Sync data : {{data}}</div> ',
        controller: function ($scope, $element, $attrs) {
            $scope.$on('updated', function () {
                $scope.data = syncSRV.syncData;
            });
        }
    };
});


<div ng-controller="MainCtrl1">
    <fieldset>
        <legend> Controller 1</legend>
        <div dataview></div>
        <div sync></div>
    </fieldset>
</div>
<div ng-controller="MainCtrl2">
    <fieldset>
        <legend> Controller 2</legend>
        <div dataview></div>
        <div sync></div>
    </fieldset>
</div>

Here is what I would do for this case.

I will create a directive for

<ul class="nav" ng-controller="Menu">
        <li ng-repeat="item in menu">
            <a href="{{item.href}}">{{item.title}}</a>
        </li>
</ul> 

so once item is updated, it will be updated in both directive.

small example

maxisam
  • 21,975
  • 9
  • 75
  • 84
  • Your example only has 1 controller, would that still work across 2 different controllers? e.g.: http://plnkr.co/edit/2a55gq. I'm restricted in how I can modify my page HTML – Pete Nov 01 '12 at 15:57
1

I just want to update and simplify the selected answer. It seems you can reduce this by deleting this line: $rootScope.$broadcast( 'MenuService.update', this.menu );

and this chunk:

$scope.$on( 'MenuService.update', function( event, menu ) {
 $scope.menu = menu;
});

The reason being, we are already using a Service, and that basically binds the two identical controllers, so no need to use $rootScope.$broadcast and add an observable.

Working plunk here: http://plnkr.co/edit/1efEwU?p=preview

You only need to link the service, when I refactor the code I was able to reduce it to 13 lines instead of 22.

chriz
  • 1,826
  • 2
  • 24
  • 28