16

I am pretty new to the AngularUI Router and would like the use it for the following scenario:

The layout common to all pages includes a top navbar containing a menu with buttons on the right and a content section filling the space below. The page has several pages that I map to UI Router states (page1, page2, ...). Each page can have its own menu items and its own content. The menu needs to share the scope with the content since they interact (e.g. the save button submits the form in the content, it should only be enabled if the form is valid).

mockup

The HTML roughly looks like this:

<body>
    <nav class="...">
        <h1>my site</h1>
        <div>MENU SHOULD GO HERE</div>
    </nav>
    <div class="row">
        <div class="column ...">
            CONTENT SHOULD GO HERE
        </div>
    </div>        
</body>

Right now, I am using two parallel views and two controllers for each state. But this way, the two scopes/controllers cannot interact.

So how would you accomplish that?

jack_kerouac
  • 1,482
  • 2
  • 15
  • 30

2 Answers2

22

$scope is not the model, its a reference to a model, glue in between the data & the view. If $scopes in two, or more, controllers need to share one model/state/data use a singleton object instance by registering a angular service. That one service/factory can be injected into as many controllers as you like, and then everything can work off that one source of truth.

Heres a demo of 1 factory linking $scopes in navbar & body with ui-router http://plnkr.co/edit/P2UudS?p=preview (left tab only)

ui-router (viewC is navbar):

$stateProvider
.state('left', {
  url: "/",
  views: {
    "viewA": {
      controller: 'LeftTabACtrl',
      template: 'Left Tab, index.viewA <br>' +
                '<input type="checkbox" ng-model="selected2.data" />' +
                '<pre>selected2.data: {{selected2.data}}</pre>'
    },
    {...},
    "viewC": {
      controller: 'NavbarCtrl',
      template: '<span>Left Tab, index.viewC <div ui-view="viewC.list"></div>' +
                '<input type="checkbox" ng-model="selected.data" />' +
                '<pre>selected.data: {{selected.data}}</pre></span>'
    }
  }
})

Factory & Controllers:

app.factory('uiFieldState', function () {
    return {uiObject: {data: null}}
});

app.controller('NavbarCtrl', ['$scope', 'uiFieldState', '$stateParams', '$state',
    function($scope, uiFieldState, $stateParams, $state) {
        $scope.selected = uiFieldState.uiObject;
    }
]);

app.controller('LeftTabACtrl', ['$scope', 'uiFieldState', '$stateParams', '$state',
    function($scope, uiFieldState, $stateParams, $state) {
        $scope.selected2 = uiFieldState.uiObject;
    }
]);

As you can see, the factory object {uiObject: {data: null}} is injected into the controller with uiFieldState & then its simply $scope.selected = uiFieldState.uiObject; for connecting the factory to the scope ng-model="selected.data" .`

cheekybastard
  • 5,535
  • 3
  • 22
  • 26
  • I ended up doing exactly that. I realized that I lacked the knowledge about how to connect parts of an AngularJS app in general (so the question is actually not tied to UI router so much). – jack_kerouac Feb 27 '14 at 13:11
  • I just wanted the `data` so I tried setting `app.factory('uiFieldState', function () { return {data: null}})` and `$scope.data=uiFieldState.data`. This, of course, didn't work, as an object is needed to pass data between scopes like this. – jsruok Jul 17 '15 at 10:15
5

You should use:

$on and $emit

The emit controller, that sends the data.

angular.module('MyApp').controller('MyController', ['$scope', '$rootScope', function ($scope, $rootScope){

  $rootScope.$emit('SomeEvent', data);
}]);

An example of how to implement $rootScope a safe way so it destroys and fixes stuff after use:

angular
.module('MyApp')
.config(['$provide', function($provide){
    $provide.decorator('$rootScope', ['$delegate', function($delegate){

        Object.defineProperty($delegate.constructor.prototype, '$onRootScope', {
            value: function(name, listener){
                var unsubscribe = $delegate.$on(name, listener);
                this.$on('$destroy', unsubscribe);
            },
            enumerable: false
        });


        return $delegate;
    }]);
}]);

And the controller with the data that should be treated.

angular.module('MyApp')
.controller('MySecondController', ['$scope', function MyController($scope) {

        $scope.$onRootScope('SomeEvent', function(event, data){
            console.log(data);
        });
    }
]);

You could pass in $rootScope instead of using the $scopes method $onRootScope that we defined in the config. However, this is not a recommended way of using $emit and $onRootScope.

Instead of using $emit, you could always use $broadcast. This would however give you very huge performance issues as your app grows. Since it bubbles the data through all controllers.

If your controllers are parents and child to each other, they don't have to use the $rootScope.

Here is and example of the difference between $emit and $broadcast: jsFiddle

And their is really performance differences:

enter image description here

petur
  • 1,366
  • 3
  • 21
  • 42
  • 1
    Thanks for the comprehensive response! Is there another way using UI router so that I end up with the same scope for both views? That would be more convenient than using events on the root scope... – jack_kerouac Feb 20 '14 at 10:47
  • 1
    If you send in the scope as the data variable as shown, the data will bind back to the other one. For example in the index file, you can have a mehtod called "sendUserData({{userData}}" inside a ng-click directive. In the controller, you have the $scope.sendUserData = function(data){$rootScope.$emit('LoadUser', data);} and from the second controller, you just bind the data to your new, current scope: $scope.$onRootScope('LoadUser', function(event, data){$scope.user = data;} The data will be bound. – petur Feb 20 '14 at 11:25
  • Consider marking it as an answer if you think it helped you. – petur Feb 20 '14 at 13:05
  • I realized that I lacked the knowledge about how to connect parts of an AngularJS app in general. Your answer is interesting and gave me some insight into events and the difference between emit and broadcast. Thanks. The other answer just better solved my problem. – jack_kerouac Feb 27 '14 at 13:12