5

I have a page where one controller shows all of the logged in user's teams, and another controller allows the user to update a team. When the user updates the team name, I want the controller that displays all the teams to notice that a team has been updated and update it's variable accordingly.

I've been googling around and it seems there's lots of questions and lots of different ways to do this. Ideally i'd like to be able to just update a factory variable and all the controllers would notice that the value has been updated. Not sure if that is how angular works though.

Example:

var myapp= angular.module('myapp', []);

...

// This gets the teams that a user is associated with
myezteam.factory('teams', function($http) {

    // Get the teams associated with the logged in user
    return {
        getTeams: function(callback) {
            $http.get(baseUrl+'v1/teams/all' + apiKey)
                .success(function(response) {
                    callback(response);
                });
        }
    }

});

Controller which gets all the teams

// This controller is used to set the user profile links
myapp.controller('TemplateProfileController', ['$scope', '$http'', 'teams', function($scope, $http, teams) {

    // Gets all of a user's teams from the "teams" factory
    getTeams = function() {
        teams.getTeams(function(response) {
            $scope.teams = response;    
        });
    }

    $scope.teams = getTeams();  // Call on page load

}]);

Controller which handles the editing of a team

// Controller for editing a team
myapp.controller('EditTeamController', ['$scope', '$http', '$routeParams', 'teams', function($scope, $http, $routeParams, teams) {

    // Get the team that we're editing
    getTeam = function() {    
        $http.get(baseUrl+'v1/teams/' + $routeParams.id + apiKey)
            .success(function(response) {
                $scope.team = response;
            });
    }

    // Update the team and refresh the list of all teams
    $scope.updateTeam = function() {        
        $http.post(baseUrl+'v1/teams' + apiKey, $scope.team)
            .success(function(response) {
                // NEED TO SOMEONE TRIGGER THE TempalteProfileController to get the teams from the factory again
            })
    } 

    getTeam();  // Call on page load;

}]);
Catfish
  • 18,876
  • 54
  • 209
  • 353
  • Factories/services are the Angular approach- you might check out this ["AngularJS: How to watch service variables"](http://stackoverflow.com/questions/12576798/angularjs-how-to-watch-service-variables) and here's some good thoughts on ["angularjs watch pub sub best practices"](http://eburley.github.io/2013/01/31/angularjs-watch-pub-sub-best-practices.html) – KayakDave Dec 30 '13 at 21:25
  • perfect use case for `$resource`. Once you retrieve all teams, all the crud methods are already bound to each object in array. Then your `getTeam` method in editController would just filter from original group to display the one needed – charlietfl Dec 30 '13 at 22:08
  • @charlietfl Can you provide a simple example? If i'm understanding you correctly, I think we're on different pages. EditController updates the name of a team so the entire list needs to be updated (or at a minimum one team in the list). – Catfish Dec 30 '13 at 22:21
  • see docs http://docs.angularjs.org/api/ngResource.$resource – charlietfl Dec 31 '13 at 00:39

5 Answers5

11

This is actually very simple to do as long as you understand the difference between variables and objects.

Say I have a service that keeps track of a counter and provides a way to increment it:

app.factory("mySharedService", function(){
  var values = {
    mySharedValue: 0
  }; 

  return{
    getValues: function(){
      return values;
    },

    incrementValue: function(){
      values.mySharedValue++;
    }
  };
});

Notice that rather than just declaring a simple variable, I declare an object and hang my actual value from the object. This object reference will remain the same for the lifetime of the service and only the values inside of it will change.

This means any controller who calls getValues() will receive the same object reference and can use that in their scope:

app.controller('Controller1', function($scope, mySharedService) {
  $scope.values = mySharedService.getValues();
});

app.controller('Controller2', function($scope, mySharedService) {
  $scope.values = mySharedService.getValues();
});

app.controller('Controller3', function($scope, mySharedService) {
  $scope.values = mySharedService.getValues();
});

Whenever the value in mySharedValue changes, all controllers will see it equally (because they are all sharing the same object reference). This means that any bindings in that controller will update instantly, and you can use $scope.$watch() just like you do with any other scope variable.

Here is an example plunker:

http://plnkr.co/edit/FA3MbfQQpiOtp5mGqqAq?p=preview

EDIT:

If you want to preserve encapsulation and only able to edit value through the provided methods you could do something like:

app.factory("mySharedService", function(){
  var mySharedValue = 0;

  var values = {
    getMySharedValue: function(){
       return mySharedValue;
    }
  }; 

  return{
    getValues: function(){
      return values;
    },

    incrementValue: function(){
      mySharedValue++;
    }
  };
});
Daniel Tabuenca
  • 13,147
  • 3
  • 35
  • 38
  • Can you explain what you mean about using `$scope.$watch()`? I don't see that anywhere in your plunker. – Catfish Jan 04 '14 at 07:07
  • 1
    If you are just using the value in the template you don't need $scope.$watch. For example if you do {{values.mySharedValue}} It already sets up a $watch for you. If you wanted to be notified when the value changes for any reason you could do $scope.$watch('values.mySharedValue', function(newValue){....do something.....}) in your controller – Daniel Tabuenca Jan 04 '14 at 15:44
  • Is there any way to change the example, so that mySharedValue's encapsulation is not broken? So that controllers are not allowed to change it, but through the incrementValue() function provided. – Alex Che Feb 12 '14 at 14:36
  • Thanks for edit. So, controllers and views will bind and $watch $scope.values.getMySharedValue, won't they? Does watching a getter function differ from watching a variable in Angular? – Alex Che Feb 13 '14 at 15:19
  • 1
    You can watch it directly by doing $scope.$watch($scope.values.getMySharedValue, callback) or by passing a string expression $scope.$watch("values.getMySharedValue()", callback) – Daniel Tabuenca Feb 13 '14 at 16:20
2

The easiest thing to do would be to put teams on the $rootScope, that way it's available to all controllers that will use teams. I find this is the best solution for CRUD apps where you have a "list" controller and a "new" controller, "edit", etc...

It took me a while to get comfortable with it, as I see using $rootScope like using global variables; however, I heard Misko (creator of angularjs) answer someone's question once during a presentation by saying "why not just put it on the rootScope". He didn't seem as concerned as I was.

The main thing is - when you put something on the $rootScope it makes it just makes the code cleaner - I started with broadcasting and listening to events and the code was way messier.

Ultimately, the way I see it, if your page / app's sole purpose is to show teams - and you have multiple controllers dealing with it - putting it on the $rootScope makes sense.

So long story short, replace $scope.teams with $rootScope.teams and make sure you include $rootScope in the controller constructor functions for dependency injection.

xdotcommer
  • 793
  • 1
  • 7
  • 14
  • I added a demo about using $rootScope http://jsfiddle.net/u9b6L/ . In this demo, I utilize scope inheritance instead of injecting the rootScope, but the idea is basically the same. – Khanh TO Jan 05 '14 at 01:28
  • I thought scope was could only be shared if the child controller is _under_ a parent controller in the DOM. Can I ask how you got around this? Is it by setting up the team object on the rootScope in the run block? – xdotcommer Jan 05 '14 at 18:50
  • `Is it by setting up the team object on the rootScope in the run block` yes, as you can see in the demo. – Khanh TO Jan 06 '14 at 14:37
1

@dtabuenc and @xdotcommer proposed some solution that work. But if you want to do something in controller when the data is updated you can use $rootScope.$emit + $rootScope.$on .

In your factory :

$rootScope.$emit('dataChangedEvent', data);

In your Controller:

  $rootScope.$on('dataChangedEvent', function(event, data) {console.log(data)});
Alborz
  • 6,843
  • 3
  • 22
  • 37
1

@Catfish

You can add a $watch to the service variable, exactly as @dtabuenc mentioned in his answer. Here's what it would look like:

app.controller('Controller1', function($scope, mySharedService) {
  $scope.values = mySharedService.getValues();

  $scope.$watch( function () { return mySharedService.getValues(); }, function () {
    alert('data changed');
  }, true);

});
0

You can use a service to achieve that. See it working here http://plnkr.co/edit/TAIJlm?p=preview

HTML:

<body ng-app='myapp'>
  <div id="col1" ng-controller="MainCtrl">
    <h1>MainCtrl teams:</h1>
    <ul>
      <li ng-repeat='t in teams'>{{t}}</li>
    </ul>
    <button ng-click="updateTeam()">Add team</button>
  </div>
  <div id="col2" ng-controller="OtherCtrl">
    <h1>OtherCtrl teams:</h1>
    <ul>
      <li ng-repeat='t in otherTeams'>{{t}}</li>
    </ul>
  </div>
</body>

JS:

angular.module('myapp', [])
  .service('Teams', function($http, $q) {
    var _teams = [];
    var defer = $q.defer();
    $http.get('teams.json')
      .success(function(response) {
        _teams = JSON.parse(response[0]);
        defer.resolve(_teams);
      })
      .error(function(err) {
        defer.reject(err);
      });
    // _teams = [1,2,3,4,5];
    // Get the teams associated with the logged in user
    return {
      getTeams: function getTeams() {
        return defer.promise;
      }
    }

  })
  .controller('MainCtrl', function($scope, Teams) {
    Teams.getTeams().then(function(teams) {
      $scope.teams = teams;
    });

    // Update the team and refresh the list of all teams
    $scope.updateTeam = function() {        
      $scope.teams.push($scope.teams.length+1);
    };
  })
  .controller('OtherCtrl', function($scope, Teams) {
    Teams.getTeams().then(function(teams) {
      $scope.otherTeams = teams;
    });
  });

Two-way data binding does its magic by updating the view when the array referenced by the scopes changes.

Jonas
  • 1,692
  • 13
  • 17