4

I'm trying to figure out the "preferred" or "angular-way" of sharing properties or state between controllers/directives. There are several methods to implement this, but I want to keep with best-practice. Below are some banal examples of how this can be implemented:


1. Using $scope.$watch

// The parent controller/scope
angular.module('myModule').controller('parentController', ['$scope', function($scope) {
    $scope.state = {
        myProperty: 'someState'; // Default value to be changed by some DOM element
    };
}]);

// The child controller/scope.
angular.module('myModule').controller('childController', ['$scope', function($scope) {
    $scope.$watch('state.myProperty', function (newVal) {
        // Do some action here on state change
    });
}]);

Edit: Based on answers below, this is bad practice and should be avoided. It is untestable and places an unwanted DOM dependancy.


2. Using $broadcast

// The parent controller
angular.module('myModule').controller('parentController', ['$scope', function($scope) {
    var myProperty = 'someState';
    $scope.setState = function (state) {
        myProperty = state; // Set by some other controller action or DOM interaction.
        $scope.$broadcast('stateChanged', state); // Communicate changes to child controller
    }
}]);

// The child controller.
angular.module('myModule').controller('childController', ['$scope', function($scope) {
    $scope.$on('stateChanged', function (evt, state) {
        // Do some action here
    }
}]);

Edit: Equally bad practice as you need to know the placement of the controllers in the DOM in order to determine weather to use $broadcast (down the DOM) or $emit (up the DOM).


3. Using service

angular.module('myModule').factory('stateContainer', [function () {
    var state = {
            myProperty: 'defaultState'
        },
        listeners = [];

    return {
        setState: function (newState) {
            state.myProperty = newState;
            angular.forEach(listeners, function (listener) {
                listener(newState);
            });
        },
        addListener: function (listener) {
            listeners.push(listener);
        }
    }
}]);

// The parent controller
angular.module('myModule').controller('parentController', ['$scope', 'stateContainer', function($scope, stateContainer) {
    $scope.setState = function (state) {
        stateContainer.setState(state);
    };
}]);

// The child controller.
angular.module('myModule').controller('childController', ['$scope', 'stateContainer', function($scope, stateContainer) {
    stateContainer.addListener(function (newState) {
        // Do some action here
    });
}]);

There are probably some approaches I've missed here, but you get the idea. I'm trying to find the best approach. Although verbose, I personally lean towards #3 in the list here. But I come from a Java and jQuery background where listeners are widely used.

Edit: Answers below are insightful. One talks of sharing state between parent/child directives using the require directive configuration. The other talks of sharing service or service properties directly to the scope. I believe that depending on the need, they are both right in what is or is not best practice in Angular.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Øystein Amundsen
  • 3,993
  • 8
  • 44
  • 63

2 Answers2

5

Any of these will work if done correctly, but a variant on service is the preferred way AFAIK.

The question is, do you even need a listener in the service case? Angular itself will update any views (which is the purpose of the controller), so why do you need a listener or watch? It is sufficient to change the value itself for the view to be changed.

app.factory('stateService',function() {
  return {
     myState: "foo"
  }
})
.controller('one',function($scope,stateService) {
    $scope.changeState = function() {
      stateService.myState = $scope.state;
    };
})
.controller('two',function($scope,stateService) {
    $scope.svc = stateService;
})

You can then do the following in your view (incomplete):

<div ng-controller="one">
  <input name="state" ng-model="state"></input>
  <button type="submit" ng-click="changeState()">Submit</button>
</div>
<div ng-controller="two">{{svc.myState}}</div>

Truth is, you don't even need to go that far with having a button and a function. If you just tie the ng-model together it will work:

<div ng-controller="one">
  <input name="state" ng-model="svc.myState"></input>
</div>
<div ng-controller="two">{{svc.myState}}</div>

Try the following jsfiddle http://jsfiddle.net/cwt9L6vn/1/

Nate Anderson
  • 18,334
  • 18
  • 100
  • 135
deitch
  • 14,019
  • 14
  • 68
  • 96
  • Didn't think of applying the entire service to scope. Good one! :-) – Øystein Amundsen Jan 17 '15 at 18:18
  • 2
    This is called **scope bleeding** and it's a very bad practice. – Reactgular Jan 17 '15 at 18:27
  • Using a service for sharing data between controllers is fine. What I dislike of this example is how the service is directly exposed and bound in the view. – Maxime Morin Jan 17 '15 at 18:36
  • 1
    What do you propose then @MaximeMorin, accessors in the service? Then I'm back to having listeners fire each time properties change. – Øystein Amundsen Jan 17 '15 at 18:49
  • 2
    I believe the view shouldn't be aware of where the data comes from. In this example, the view knows the service. I think the controller should hold the state and then the view would only use the controller. In view: `{{ctrl.myState}}` (With ControllerAs syntax) In Ctrl: `ctrl.myState = myService.myState;` With this, the controller can decide to keep a copy of the state or the original reference. This opens the door to save/cancel, validation checks, ... – Maxime Morin Jan 17 '15 at 18:53
  • 2
    Yes, all of the commenters are 100% correct; exposing the service *directly* to the view is bad practice. This was meant just as a simple example showing that it works, but you are right. However, be careful of using Oystein's example, because if it is just text, you lose some of the watching. In general, you want your views to be looking at objects, which is why I picked the simplest path. I will update it above, but watch out for "I need a dot" http://nathanleclaire.com/blog/2014/04/19/5-angularjs-antipatterns-and-pitfalls/ – deitch Jan 18 '15 at 13:18
  • deitch, you said "I will update it above"; did you ever update it? – Nate Anderson Apr 09 '17 at 02:26
  • @TheRedPea I don't recall, it was 2 years ago! I am rereading it now to try and refresh my memory. – deitch Apr 09 '17 at 08:48
  • No, looks like I didn't. @MaximeMorin do you feel like updating it to show what you wanted? Take the fiddle I linked to? – deitch Apr 09 '17 at 08:54
  • I don't know what @MaximeMorin means by "With this, the controller can decide to keep a copy of the state or the original reference." -- if the concern is "In this example, the view knows the service." (i.e. can't reference `$scope.svc` in the view) , then I think any solution will actually resemble deitch's original answer. As @Øystein says, "... accessors in the service?Then I'm back to having listeners fire each time properties change." It requires functions to sync up with the `svc` property. [Here's my take.](http://jsfiddle.net/cwt9L6vn/13/) – Nate Anderson Apr 09 '17 at 17:33
  • @TheRedPea thanks for your take, but I am not sure that addresses what he is saying. I think MaximeMorin doesn't like having the service accessed directly in the view. – deitch Apr 10 '17 at 05:36
  • But my view (i.e. my HTML) **doesnt** reference the *service* directly (maybe my JSFiddle isnt working?)? My view only binds to the *controllers* properties and only calls the *controllers* methods? An alternative could us [ES5 `Object.defineProperty`](http://stackoverflow.com/a/28345044/1175496) (or [TS equivalent](http://stackoverflow.com/a/35859703/1175496)) to define the getter and setter (I guess as Oystein said, 2x methods *per controller* ) so user can only use `ng-model` i.e. avoid use of `ng-click` – Nate Anderson Apr 10 '17 at 13:29
2

There is no such thing as parent and child controllers in AngularJS. There are only parent and child directives, but not controllers. A directive can have a controller that it exposes as an API to other directives.

Controllers are not related to the DOM hierarchy so they can't have children. They also don't create their own scope. So you never know if you have to $broadcast or $emit to talk to other controllers.

If you start using $broadcast from a controller, then you're going to get stuck not knowing if the other controller is up or down. That's when people start doing stuff like $rootScope.$broadcast(..) which is a very bad practice.

What you are looking for are directives that require other directives.

var app = angular.modeul('myApp',[]);
// use a directive to define a parent controller
app.directive('parentDir',function() {
     return {
         controller: function($scope) {
             this.myFoo = function() {
                alert("Hello World");
             }
         }
     });
// use a directive to enforce parent-child relationship
app.directive('childDir',function() {
     return {
          require: '^parentDir',
          link: function($scope, $el, $attr, parentCtrl) {
             // call the parent controller
             parentCtrl.myFoo();
          }
     });

Using the require feature of a directive does two important things.

  1. Angular will enforce the relationship if it's not optional.
  2. The parent controller is injected into the child link function.

There is no need to $broadcast or $emit.

Another option that is also effective is to use directives to expose an API.

// this directive uses an API
app.directive('myDir',function() {
     return {
          scope: { 
            'foo': '&'
          },
          link: function($scope, $el, $attr) {
             // when needed, call the API
             $scope.foo();
          }
     });

 // in the template
 <div ng-controller="parentController">
     <div my-dir foo="parentController.callMyMethod();"></div>
 </div>
Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • I realize I use the term parent/child "controller" loosely here. What I mean is parent/child scope. The reason I'm asking is that I want to share application state between different angular objects. I understand the objection towards example #1 above here, but this does not only apply to directives (web components), as the application infrastructure concists of more than just directives. – Øystein Amundsen Jan 17 '15 at 18:31
  • ...but +1 for an additional option for inter-object comunicaiton. :-) – Øystein Amundsen Jan 17 '15 at 18:36
  • The key is to keep objects as **decoupled** from each other as possible. This improves testing and maintainability. When a controller or directive depends upon the `$scope` having a state that is maintained by some *magical* outside influence, or business logic is only executed when the `$scope` receives events from an unknown broadcaster, then you will find it very had to change, test and maintain that code in the future. – Reactgular Jan 17 '15 at 18:38
  • I agree completelly, if by "magical outside force" you mean $scope.$parent or $broadcast/$emit. But directives (IMHO) are _small_ reusable components. Not entire pages. I think that even directives can safely store state in services for reuse by other components. This should be as testable as if the directives were designed hierarchically with their own state-store. – Øystein Amundsen Jan 17 '15 at 18:47
  • I think we're agreeing with each other, but are using different terms so it sounds like we're talking about different things. – Reactgular Jan 17 '15 at 18:54
  • Based on your comment on @deitch's post that exposing services to scope is bad practice - I belive that we are not 100% in agreement. Or I at least do not 100% understand your proposal for sharing state further than just directives. – Øystein Amundsen Jan 17 '15 at 18:59