6

How do we access the data from the resolve function without relading the controller?

We are currently working on a project which uses angular-ui-router. We have two seperated views: on the left a list of parent elements, on the right that elements child data.

If selecting a parent on the left, we resolve it's child data to the child-view on the right.

With the goal not to reaload the childs controller (and view), when selecting a different parent element, we set notify:false.

We managed to 're-resolve' the child controllers data while not reloading the controller and view, but the data (scope) won't refresh.

We did a small plunker to demonstrate our problem here

First click on a number to instantiate the controllers childCtrl. Every following click should change the child scopes data - which does not work. You might notice the alert output already has the refreshed data we want to display.

nilsK
  • 4,323
  • 2
  • 26
  • 40

3 Answers3

2

Based on sielakos answer using an special service i came up with this solution. First, i need a additional service which keeps a reference of the data from the resovle.

Service

.service('dataLink', function () {
  var storage = null;

  function setData(data) {
      storage = data;
  }

  function getData() {
      return storage;
  }

  return {
      setData: setData,
      getData: getData
  };
})

Well, i have to use the service in my resolve function like so

Resolve function

resolve: {
    detailResolver: function($http, $stateParams, dataLink) {
        return $http.get('file' + $stateParams.id + '.json')
            .then(function(response) {
                alert('response ' + response.data.id);
                dataLink.setData(response.data);
                return response.data;
            });
    }
}

Notice the line dataLink.setData(response.data);. It keeps the data from the resolve in the service so I can access it from within the controller.

Controller

I modified the controller a little. I wrapped all the initialisation suff in an function i can execute when the data changes. The second thing is to watch the return value of the dataLink.getData();

As of https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch $scope.$watch provides functionality to watch return values of functions.

Here is some Q&D example:

.controller('childCtrl', function($scope, $log, detailResolver, $interval, dataLink) {
    initialise();
    /*
    * some stuff happens here
    */

    $interval(function() {
        console.log(detailResolver.id)
    }, 1000);

    $scope.$watch(dataLink.getData, function(newData) {
        detailResolver = newData;
        initialise();
    });

    function initialise() {
        $log.info('childCtrl detailResolver.id == ' + detailResolver);
        $scope.id = detailResolver;
    }
})

The line $scope.$watch(dataLink.getData, function(newData) { ... }); does the trick. Every time the data in the dataLink service changes the callback kicks in and replaces the old data with the new one. Ive created a plunker so you can give it a try https://plnkr.co/edit/xyZKQgENrwd4uEwS9QIM

You don't have to be afraid of memory leaks using this solution cause angular is removing watchers automatically. See https://stackoverflow.com/a/25114028/6460149 for more information.

Community
  • 1
  • 1
DaVarga
  • 311
  • 2
  • 8
1

Not so pretty, but working solution would be to use events. Well, maybe it is not that bad, at least it is not complicated. https://plnkr.co/edit/SNRFhaudhsWLKUNMFos6?p=preview

angular.module('app',[
    'ui.router'
  ])
  .config(function($stateProvider) {
    $stateProvider.state('parent', {
      views:{
        'parent':{
          controller: 'parentCtrl',
          template: '<div id="parent">'+
            '<button ng-click="go(1)">1</button><br>'+
            '<button ng-click="go(2)">2</button><br>'+
            '<button ng-click="go(3)">3</button><br>'+
          '</div>'
        },
      },
      url: ''
    });


    $stateProvider.state('parent.child', {
      views:{
        'child@':{
          controller: 'childCtrl',
          template:'<b>{{ id }}</b>'
        }
      },
      url: '/:id/child',
      resolve: {
        detailResolver: function($http, $stateParams, $rootScope) {
          return $http.get('file'+$stateParams.id+'.json')                
            .then(function(response) {
              alert('response ' + response.data.id);

              $rootScope.$broadcast('newData', response.data);

              return response.data;
            });
        }
      }
    });
  })
  .controller('parentCtrl', function ($log, $scope, $state) {
    $log.info('parentCtrl');
    var notify = true;
    $scope.go = function (id) {
      $state.go('parent.child', {id: id}, {notify:notify});
      notify = false;
    };
  })
  .controller('childCtrl', function ($scope, $log, detailResolver, $interval) {
    /*
     * some stuff happens here
     */

    $log.info('childCtrl detailResolver.id == ' + detailResolver);

    $scope.$on('newData', function (event, detailResolver) {
      $scope.id = detailResolver;
    });

    $scope.id = detailResolver;
    $interval(function(){
      console.log(detailResolver.id)
    },1000)
  })
;

EDIT: A little bit more complicated solution, that requires changing promise creator function into observables, but works: https://plnkr.co/edit/1j1BCGvUXjtv3WhYN84T?p=preview

angular.module('app', [
    'ui.router'
  ])
  .config(function($stateProvider) {
    $stateProvider.state('parent', {
      views: {
        'parent': {
          controller: 'parentCtrl',
          template: '<div id="parent">' +
            '<button ng-click="go(1)">1</button><br>' +
            '<button ng-click="go(2)">2</button><br>' +
            '<button ng-click="go(3)">3</button><br>' +
            '</div>'
        },
      },
      url: ''
    });


    $stateProvider.state('parent.child', {
      views: {
        'child@': {
          controller: 'childCtrl',
          template: '<b>{{ id }}</b>'
        }
      },
      url: '/:id/child',
      resolve: {
        detailResolver: turnToObservable(['$http', '$stateParams', function($http, $stateParams) { //Have to be decorated either be this or $inject
          return $http.get('file' + $stateParams.id + '.json')
            .then(function(response) {
              alert('response ' + response.data.id);
              return response.data;
            });
        }])
      }
    });
  })
  .controller('parentCtrl', function($log, $scope, $state) {
    $log.info('parentCtrl');
    var notify = true;
    $scope.go = function(id) {
      $state.go('parent.child', {id: id}, {notify: notify});
      notify = false;
    };
  })
  .controller('childCtrl', function($scope, $log, detailResolver, $interval) {
    /*
     * some stuff happens here
     */

    $log.info('childCtrl detailResolver.id == ' + detailResolver);

    detailResolver.addListener(function (id) {
      $scope.id = id;
    });
  });

function turnToObservable(promiseMaker) {
  var promiseFn = extractPromiseFn(promiseMaker);
  var listeners = [];

  function addListener(listener) {
    listeners.push(listener);

    return function() {
      listeners = listeners.filter(function(other) {
        other !== listener;
      });
    }
  }

  function fireListeners(result) {
    listeners.forEach(function(listener) {
      listener(result);
    });
  }

  function createObservable() {
    promiseFn.apply(null, arguments).then(fireListeners);

    return {
      addListener: addListener
    };
  }

  createObservable.$inject = promiseFn.$inject;

  return createObservable;
}

function extractPromiseFn(promiseMaker) {
  if (angular.isFunction(promiseMaker)) {
    return promiseMaker;
  }

  if (angular.isArray(promiseMaker)) {
    var promiseFn = promiseMaker[promiseMaker.length - 1];
    promiseFn.$inject = promiseMaker.slice(0, promiseMaker.length - 1);

    return promiseFn;
  }
}
sielakos
  • 2,406
  • 11
  • 13
  • Sure, using a global event will work. This will be our plan-b. I hoped to find a more elegant soultion though. I will wait some more time, hoping someone will provide a solution without spamming events via rootScope. – nilsK Jul 18 '16 at 10:08
  • 1
    @nilsK This was rather interesting problem, so I came with other solution to this problem, it requires changing your function that returns promises to function that return observable object, but it works. Although it depends on some globally available functions, but you could probably hack them to be providers if you wish to do so. – sielakos Jul 18 '16 at 15:04
  • This is still, a little bit hacky and requires decorated functions, but it was fun to do so. – sielakos Jul 18 '16 at 15:06
  • I like the second approach with promises. But i am not sure, how would i derigster all listeners? The resolving (child-)state will change a lot and users will work many hours with their browser window open. I am afraid of memory leaks. – nilsK Jul 19 '16 at 07:09
  • @nilsK Well, that is one of problems with this solution, you have to dereigster all listener manually on ``$scope.$on('$destroy'``. But that is not that different than deregistering listeners used with ``$rootScope`` directly. You can also probably try to modify ``turnToObservable`` function so that it destroys all listeners when state changes. It might be possible, although tricky. – sielakos Jul 19 '16 at 07:15
  • Another solution would be to add to observable function ``bindToScope`` so that when scope is destroyed, all listeners created with this observable are also destroyed. There are a lot of possibilities. – sielakos Jul 19 '16 at 07:18
1

1) For current task ng-view is not needed (IMHO). If you need two different scopes then redesign ng-views to become directives with their own controllers. This will prevent angular to reload them

2) if you need to share data between scopes then service could be used to store data (see helperService in the following code)

3) if we talk about current code simplification then it could be done so: use service from 2) and just use one controller:

(function() {
  angular.module('app',[
    'ui.router'
  ]);
})();

(function() {
  angular
    .module('app')
    .service('helperService', helperService);

  helperService.$inject = ['$http', '$log'];
  function helperService($http, $log) {
    var vm = this;

    $log.info('helperService');

    vm.data = {
      id: 0
    };
    vm.id = 0;
    vm.loadData = loadData;

    function loadData(id) {
      vm.id = id;

      $http
        .get('file'+id+'.json')
        .then(function(response) {
          alert('response ' + response.data.id);
          vm.data = response.data;
        });
    }
  }
})();

(function() {
  angular
    .module('app')
    .controller('AppController', ParentController);

  ParentController.$inject = ['helperService', '$log'];
  function ParentController(helperService, $log) {
    var vm = this;

    $log.info('AppController');

    vm.helper = helperService;
  }
})();

4) interval, watch, broadcast, etc are not needed as well

Full code is here: plunker

P.S. don't forget about angularjs-best-practices/style-guide

marbug
  • 340
  • 2
  • 10