Search code examples
angularjscordovaionic-frameworkcordova-pluginsangular-promise

Returning Data from Service to Controller with Asynchronous Callback


I'm trying to upload image from Camera or gallery. I wrote all the code in the service. The controller must receive the complete url from the camera or gallery to show it in a slide box, but now nothing receive.

Everything works, but I couldn't send back to the controller the url of the image. I think the error is because I'm trying to return the value before that the function "obtenerImagen" execute.

I'm trying to implement Callbacks, but I think I did not implemented correctly.

The variable that I want to return with the url of the image is urlImagen

This is my controller that calls the service:

 //generar el popup para seleccionar el origen de la imagen: cámara o galería
    function seleccionarImagen() {
        cambiarImagenesService.seleccionarImagen()
            .then(reemplazarImagen);
    }

And the service:

(function() {
'use strict';

angular
    .module('example.cambiarimagenes')
    .factory('cambiarImagenesService', cambiarImagenesService);

cambiarImagenesService.$inject = ['remoteDataService','$q', '$ionicPopup','$cordovaCamera', '$cordovaFile', '$cordovaFileTransfer', '$cordovaDevice', '$rootScope'];

/* @ngInject */
function cambiarImagenesService(remoteDataService,$q, $ionicPopup, $cordovaCamera, $cordovaFile,  $cordovaFileTransfer, $cordovaDevice, $rootScope){ 
    var dias = [];
    var mensaje = '';
    var image = null;
    var urlImagen = '';
    var service = {
        obtenerHorariosComplejo: obtenerHorariosComplejo,
        seleccionarImagen: seleccionarImagen
    };
    return service;

    //cargar una nueva imagen
    function seleccionarImagen() {
        var popup = seleccionarImagenPopup();
        return $ionicPopup.show(popup).then(function(result) {
            if (result == -1) {
                return false;
            }
            return urlImagen;
        });
    }

    function obtenerImagen(sourceType, callback){
        var options = {
            callback: callback,
            quality: 100,
            destinationType: Camera.DestinationType.FILE_URI,
            sourceType: sourceType,
            saveToPhotoAlbum: false
        };

        $cordovaCamera.getPicture(options).then(function(imagePath) {
            // Grab the file name of the photo in the temporary directory
            var currentName = imagePath.replace(/^.*[\\\/]/, '');

            //Create a new name for the photo
            var d = new Date(),
            n = d.getTime(),
            newFileName =  n + ".jpg";

            // If you are trying to load image from the gallery on Android we need special treatment!
            if ($cordovaDevice.getPlatform() == 'Android' && sourceType === Camera.PictureSourceType.PHOTOLIBRARY) {
                window.FilePath.resolveNativePath(imagePath, function(entry) {
                    window.resolveLocalFileSystemURL(entry, success, fail);
                    function fail(e) {
                        console.error('Error: ', e);
                    }
                    function success(fileEntry) {
                        var namePath = fileEntry.nativeURL.substr(0, fileEntry.nativeURL.lastIndexOf('/') + 1);
                        // Only copy because of access rights
                        $cordovaFile.copyFile(namePath, fileEntry.name, cordova.file.dataDirectory, newFileName).then(function(success){
                            image = cordova.file.dataDirectory + newFileName;
                            urlImagen = image;
                        }, function(error){
                            $scope.showAlert('Error', error.exception);
                        });
                    };
                }
                );
            } else {
                var namePath = imagePath.substr(0, imagePath.lastIndexOf('/') + 1);
                // Move the file to permanent storage
                $cordovaFile.moveFile(namePath, currentName, cordova.file.dataDirectory, newFileName).then(function(success){
                    image = cordova.file.dataDirectory + newFileName;
                    urlImagen = image;
                }, function(error){
                    $scope.showAlert('Error', error.exception);
                });
            }
        },
        function(err){
            console.log("error en el serivicio o cancelacion:"+err);
            // Not always an error, maybe cancel was pressed...
        })
    }

    //poopup para cargar nuevo imagen
    function seleccionarImagenPopup() {
        var scope = $rootScope.$new();
        scope.data = {
            tipo: null
        };
        return {
            templateUrl: 'scripts/complejo/agenda/nuevo-turno.html',
            title: "¿De dónde desea obtener la imagen?",
            scope: scope,
            buttons: [{
                text: 'Cancelar',
                onTap: function(e) {
                    scope.tipo = -1
                    return scope.tipo;
                }
            }, {
                text: '<b>Cámara</b>',
                type: 'button-positive',
                onTap: function(e) {
                    scope.tipo = Camera.PictureSourceType.CAMERA;
                    obtenerImagen(scope.tipo, function(val){
                        urlImagen = val;
                    });
                    console.log("el valor de la imagen al tocar la camara es:"+image);
                    return urlImagen;
                }
            }, {
                text: '<b>Galería</b>',
                type: 'button-positive',
                onTap: function(e) {
                    scope.tipo = Camera.PictureSourceType.PHOTOLIBRARY;
                    obtenerImagen(scope.tipo, function(val){
                        urlImagen = val;
                    });
                    console.log("el valor de la imagen al tocar la galeria es:"+image);
                    return urlImagen;
                }
            }]
        };
    }

    //generar error si hubo un problema
    function generarError(e){
        console.log("error!!!!!!:"+e);
        if (e.message) {
            return $q.reject(e.message);
        }
        return $q.reject('Ups! Hubo un problema al conectarse al servidor.');
    }

}
})();

Thanks for helping me!

//EDIT//

This is now my service:

(function() {
'use strict';

angular
.module('example.cambiarimagenes')
.factory('cambiarImagenesService', cambiarImagenesService);

cambiarImagenesService.$inject = ['remoteDataService','$q', '$ionicPopup','$cordovaCamera', '$cordovaFile', '$cordovaFileTransfer', '$cordovaDevice', '$rootScope'];

/* @ngInject */
function cambiarImagenesService(remoteDataService,$q, $ionicPopup,$cordovaCamera, $cordovaFile,  $cordovaFileTransfer, $cordovaDevice, $rootScope){ 
var dias = [];
var mensaje = '';
var image = null;
var urlImagen = '';
var service = {
    obtenerHorariosComplejo: obtenerHorariosComplejo,
    seleccionarImagen: seleccionarImagen
};
return service;

//cargar una nueva imagen
function seleccionarImagen() {
    var popup = seleccionarImagenPopup();
    return $ionicPopup.show(popup).then(function(result) {
        if (result == -1) {
            return false;
        }
        return urlImagen;
    });
}

function obtenerImagen(sourceType){
    var options = {
        quality: 100,
        destinationType: Camera.DestinationType.FILE_URI,
        sourceType: sourceType,
        saveToPhotoAlbum: false
    };

    return $cordovaCamera.getPicture(options).then(function(imagePath) {
        // Grab the file name of the photo in the temporary directory
        var currentName = imagePath.replace(/^.*[\\\/]/, '');

        //Create a new name for the photo
        var d = new Date(),
        n = d.getTime(),
        newFileName =  n + ".jpg";

        // If you are trying to load image from the gallery on Android we need special treatment!
        if ($cordovaDevice.getPlatform() == 'Android' && sourceType === Camera.PictureSourceType.PHOTOLIBRARY) {
            window.FilePath.resolveNativePath(imagePath, function(entry) {
                window.resolveLocalFileSystemURL(entry, success, fail);
                function fail(e) {
                    console.error('Error: ', e);
                }
                function success(fileEntry) {
                    var namePath = fileEntry.nativeURL.substr(0, fileEntry.nativeURL.lastIndexOf('/') + 1);
                    // Only copy because of access rights
                    $cordovaFile.copyFile(namePath, fileEntry.name, cordova.file.dataDirectory, newFileName).then(function(success){
                        image = cordova.file.dataDirectory + newFileName;
                        return image;

                    }, function(error){
                        $scope.showAlert('Error', error.exception);
                    });
                };
            }
            );
        } else {
            var namePath = imagePath.substr(0, imagePath.lastIndexOf('/') + 1);
            // Move the file to permanent storage
            $cordovaFile.moveFile(namePath, currentName, cordova.file.dataDirectory, newFileName).then(function(success){
                image = cordova.file.dataDirectory + newFileName;
                return image;
            }, function(error){
                $scope.showAlert('Error', error.exception);
            });
        }
    },
    function(err){
        console.log("error en el serivicio o cancelacion:"+err);
        // Not always an error, maybe cancel was pressed...
    })
}

//poopup para cargar nuevo imagen
function seleccionarImagenPopup() {
    var scope = $rootScope.$new();
    scope.data = {
        tipo: null
    };
    return {
        templateUrl: 'scripts/complejo/agenda/nuevo-turno.html',
        title: "¿De dónde desea obtener la imagen?",
        scope: scope,
        buttons: [{
            text: 'Cancelar',
            onTap: function(e) {
                scope.tipo = -1
                return scope.tipo;
            }
        }, {
            text: '<b>Cámara</b>',
            type: 'button-positive',
            onTap: function(e) {
                scope.tipo = Camera.PictureSourceType.CAMERA;
                var promise = obtenerImagen(scope.tipo)
                .then(function(val){
                    // asignamos el valor asincrónico
                    urlImagen = val;
                    // retornamos el valor a la cadena
                    return val;
                });
                // retornamos la promesa de manera síncrona
                return promise;
            }
        }, {
            text: '<b>Galería</b>',
            type: 'button-positive',
            onTap: function(e) {
                scope.tipo = Camera.PictureSourceType.PHOTOLIBRARY;
                var promise = obtenerImagen(scope.tipo)
                .then(function(val){
                    // asignamos el valor asincrónico
                    urlImagen = val;
                    // retornamos el valor a la cadena
                    return val;
                });
                // retornamos la promesa de manera síncrona
                return promise;
            }
        }]
    };
}

//generar error si hubo un problema
function generarError(e){
    console.log("error!!!!!!:"+e);
    if (e.message) {
        return $q.reject(e.message);
    }
    return $q.reject('Ups! Hubo un problema al conectarse al servidor.');
}

}
})();

Solution

  • The problem is that the callback function is executed asynchronously.

    //ERRONEOUS
    onTap: function(e) {
        scope.tipo = Camera.PictureSourceType.CAMERA;
        obtenerImagen(scope.tipo, function(val){
            //ASYNCHRONOUSLY ASSIGNED
            urlImagen = val;
        });
        console.log("el valor de la imagen al tocar la camara es:"+image);
        //SYNCHRONOUSLY RETURNED
        return urlImagen;
    }
    

    The value is returned before the value is assigned. Subsequent code executes before the value is defined.

    The obtenerImagen function needs to be refactored to return a promise and the promise needs to be returned.

    //GOOD
    onTap: function(e) {
        scope.tipo = Camera.PictureSourceType.CAMERA;
        var promise = obtenerImagenPromise(scope.tip)
          .then(function(val){
            //ASYNCHRONOUSLY ASSIGNED
            urlImagen = val;
            //return value to chain
            return val;
        });
        //SYNCHRONOUSLY RETURN PENDING PROMISE
        return promise;
    }
    

    By returning a promise, subsequent code can use the .then method of the promise to delay execution until the value is defined.

    Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.

    -- AngularJS $q Service API Reference - Chaining Promises

    Also see SO: Why are Callbacks from Promise .then Methods an Anti-Pattern


    what changes shall I need to do in "obtenerImagenPromise" to returns the promise correctly? Because now I have the following error "TypeError: Cannot read property 'then' of undefined" I think I need to return image in the function "obtenerImagenPromise"

    Start with returning the derived promise from $cordovaCamera.getPicture:

    //function obtenerImagen(sourceType, callback){
    function obtenerImagenPromise(sourceType){
        var options = {
            //callback: callback,
            quality: 100,
            destinationType: Camera.DestinationType.FILE_URI,
            sourceType: sourceType,
            saveToPhotoAlbum: false
        };
    
        //$cordovaCamera.getPicture(options).then(function(imagePath) {
        return $cordovaCamera.getPicture(options).then(function(imagePath) {
      //^^^^^^ ---- return derived promise
            // Grab the file name of the photo in the temporary directory
    

    The .then method always returns a new derived promise. That promise needs to be returned to the parent function. Also make sure that the functions inside the .then method return a value or promise. Failure to return something will result in the promise resolving as undefined.


    I set the returns but always the controller receive undefined

    Debugging hint: Put console.log statements to see intermediate values:

        //$cordovaCamera.getPicture(options).then(function(imagePath) {
        return $cordovaCamera.getPicture(options)
      //^^^^^^ ---- return derived promise
    
        .then(
            function(imagePath) {
                //ADD console.log to see intermediate data 
                console.log("getPicture success handler called");
                console.log("imagePath= "+imagePath);
    
                // Grab the file name of the photo in the temporary directory
                var currentName = imagePath.replace(/^.*[\\\/]/, '');
    
                //...
    
                //Always return something
                return "something";
    
            },
            function(err){
                console.log("error en el serivicio o cancelacion:"+err);
                // Not always an error, maybe cancel was pressed...
    
                //throw to chain error
                throw "error en el serivicio o cancelacion:"+err
            }
        );
    };
    

    Also make sure that the functions inside the .then method return a value or promise. Failure to return something will result in the promise resolving as undefined.

    The rule of thumb with functional programming is -- always return something.