1

I'm with a problem with binding an object of a Factory and a Controller and it's view.

I am trying to get the fileUri of a picture selected by the user. So far so good. The problem is that I am saving the value that file to overlays.dataUrl. But I am referencing it on the view and it isn't updated. (I checked and the value is actually saved to the overlays.dataUrl variable.

Here goes the source code of settings.service.js:

(function () {
    "use strict";
    angular
    .module("cameraApp.core")
    .factory("settingsService", settingsService);

    settingsService.$inject = ["$rootScope", "$cordovaFileTransfer", "$cordovaCamera"];

    function settingsService($rootScope, $cordovaFileTransfer, $cordovaCamera) {

         var overlays = {
             dataUrl: "",
             options: {
                 sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
                 destinationType: Camera.DestinationType.FILE_URI
             }
         };

         var errorMessages = [];

         var service = {
             overlays: overlays,
             selectOverlayFile: selectOverlayFile,
             errorMessages: errorMessages
         };

         return service;

         function selectOverlayFile() {
             $cordovaCamera.getPicture(overlays.options).then(successOverlay, errorOverlay);
         }

         //Callback functions
         function successOverlay(imageUrl) {
        //If user has successfully selected a file
             var extension = "jpg";
             var filename = getCurrentDateFileName();
             $cordovaFileTransfer.download(imageUrl, cordova.file.dataDirectory + filename + '.' + extension, {}, true)
                 .then(function (fileEntry) {
                      overlays.dataUrl = fileEntry.nativeURL;
                 }, function (e) {
                      errorMessages.push(e);
                 });
    }

    function errorOverlay(message) {
        //If user couldn't select a file
        errorMessages.push(message);
        //$rootScope.$apply();
    }
    }
})();

Now the controller:

(function () {
    angular
    .module("cameraApp.settings")
    .controller("SettingsController", SettingsController);

    SettingsController.$inject = ["settingsService"];

    function SettingsController(settingsService) {
        var vm = this;
        vm.settings = settingsService;

        activate();

        //////////////////

        function activate(){
           // Nothing here yet
        }
    }
})();

Finnally on the view:

<h1>{{vm.settings.overlays.dataUrl}}</h1>
<button id="overlay" class="button"
                ng-click="vm.settings.selectOverlayFile()">
            Browse...
</button>

Whenever I change the value in the factory, it doesn't change in the view.

Thanks in advance!

Rafael Lourenço
  • 106
  • 1
  • 10

2 Answers2

1

Unfortunately Factories in angularjs are not meant to be used as two way bindings. Factories and Services are only singletons. They are only there to be used when called.

Ex Factory:

app.factory('itemFactory', ['$http', '$rootScope', function($http, $rootScope) {
var service = {};

service.item = null;

service.getItem = function(id) {
    $http.get(baseUrl + "getitem/" + id)
        .then(function successCallback(resp) {
                service.item = resp.data.Data;
                $rootScope.$broadcast("itemready");
        }, function errorCallback(resp) {
            console.log(resp)
        });
};

return service;
}]);

I use the $broadcast so if I call getItem my controller knows to go get the fresh data.

Ex Directive:

angular.module("itemApp").directive("item", ['itemFactory', '$routeParams', '$location', '$rootScope', '$timeout', function (itemFactory, $routeParams, $location, $rootScope, $timeout) {
return {
    restrict: 'E',
    templateUrl: "components/item.html",
    link: function (scope, elem, attr) {
        scope.item = itemFactory.item;

        scope.changeMade = function(){
           itemFactory.getItem(1);
        }

        scope.$on("itemready", function () {
            scope.item = itemFactory.item;
        })
    }
}

}]);

So as you can see in my code above anytime I need a fresh item I use $broadcast and $on to update my service and directive. I hope this makes sense, feel free to ask any questions.

Ohjay44
  • 897
  • 7
  • 20
  • On my answer in the directive, when I do `vm.overlay = settingsService.overlay` it doesn't pass the updated. Only when I pass the updated value as a function argument (in the `$on`), and do: `$on("overlaysUpdate", function (event, overlays) { vm.overlays = overlays; });` I get the updated value... Is this normal? – Rafael Lourenço Jul 06 '16 at 23:28
  • 1
    @RafaelLourenco this is correct except you don't need to pass the updated value through your $on if you don't want to, alternatively you could just set vm.overlay = settingsService.overlay again once the callback completes. The original variable vm.overlay will not update automatically because that is the nature of singletons and it needs to be forced to update whether you do that by passing it through $on or just resetting it once you get a call back from $on. – Ohjay44 Jul 07 '16 at 13:34
0

As pointed by Ohjay44, the factory is not updated on the view. The way to do it is using a directive (also as Ohjay44 said). To use $broadcast, $emit and $on and keep the encapsulation I did what is recommended by John Papa's Angular Style Guide: created a factory (in my case a named it comms).

Here goes the newly created directive (overlay.directive.js):

(function () {
    angular
    .module('cameraApp.settings')
    .directive('ptrptSettingsOverlaysInfo', settingsOverlaysInfo);

    settingsOverlaysInfo.$inject = ["settingsService", "comms"];

    function settingsOverlaysInfo(settingsService, comms) {

        var directive = {
            restrict: "EA",
            templateUrl: "js/app/settings/overlays.directive.html",
            link: linkFunc,
            controller: "SettingsController",
            controllerAs: "vm",
            bindToController: true // because the scope is isolated
        };

        return directive;

        function linkFunc(scope, element, attrs, vm) {
            vm.overlays = settingsService.overlays;

            comms.on("overlaysUpdate", function (event, overlays) {
                vm.overlays = overlays;
            });
        }
    }
})();

I created overlay.directive.html with:

<div class="item item-thumbnail-left">
    <img ng-src="{{vm.overlays.dataUrl}}">
    <h2>{{vm.overlays.dataUrl}}</h2>
</div>

And finally I put an $emit on the settingsService where the overlay is updated:

(function () {
"use strict";
angular
.module("cameraApp.core")
.factory("settingsService", settingsService);

settingsService.$inject = ["comms", "$cordovaFileTransfer", "$cordovaCamera"];

function settingsService(comms, $cordovaFileTransfer, $cordovaCamera) {

         var overlays = {
             dataUrl: "",
             options: {
                 sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
                 destinationType: Camera.DestinationType.FILE_URI
             }
         };

         var errorMessages = [];

         var service = {
             overlays: overlays,
             selectOverlayFile: selectOverlayFile,
             errorMessages: errorMessages
         };

         return service;

         function selectOverlayFile() {
             $cordovaCamera.getPicture(overlays.options).then(successOverlay, errorOverlay);
         }

         //Callback functions
         function successOverlay(imageUrl) {
        //If user has successfully selected a file
             var extension = "jpg";
             var filename = getCurrentDateFileName();
             $cordovaFileTransfer.download(imageUrl, cordova.file.dataDirectory + filename + '.' + extension, {}, true)
                 .then(function (fileEntry) {
                      overlays.dataUrl = fileEntry.nativeURL;
                      // New code!!!!
                      comms.emit("overlaysUpdated", overlays);
                 }, function (e) {
                      errorMessages.push(e);
                 });
        }

        function errorOverlay(message) {
            //If user couldn't select a file
            errorMessages.push(message);
            //$rootScope.$apply();
        }
    }
})();

I used an $emit instead of a broadcast to prevent the bubbling as explained here: What's the correct way to communicate between controllers in AngularJS?

Hope this helps someone else too.

Cheers!

Community
  • 1
  • 1
Rafael Lourenço
  • 106
  • 1
  • 10