web-dev-qa-db-fra.com

AngularJS: initialiser le filtre de manière asynchrone

J'ai du mal à essayer d'initialiser un filtre avec des données asynchrones.

Le filtre est très simple, il doit traduire les chemins en nom, mais pour ce faire, il a besoin d'un tableau de correspondance, que je dois récupérer sur le serveur.

Je pourrais faire des choses dans la définition du filtre, avant de retourner la fonction, mais l'aspect asynchrone l'empêche

angular.module('angularApp').
  filter('pathToName', function(Service){
    // Do some things here

    return function(input){
      return input+'!'
    }
  }

Utiliser une promesse peut être viable mais je n'ai pas de compréhension claire sur la façon dont angular charge les filtres. Ce post explique comment réaliser une telle magie avec les services, mais est -il possible de faire de même pour les filtres?

Et si quelqu'un a une meilleure idée de la façon de traduire ces chemins, je suis à l'écoute.

ÉDITER:

J'ai essayé avec la promesse d'approche, mais quelque chose ne va pas et je ne vois pas quoi:

angular.module('angularApp').filter('pathToName', function($q, Service){

  var deferred = $q.defer();
  var promise = deferred.promise;

  Service.getCorresp().then(function(success){
    deferred.resolve(success.data);
  }, function(error){
    deferred.reject();
  });

  return function(input){
    return promise.then(
      function(corresp){
        if(corresp.hasOwnProperty(input))
          return corresp[input];
        else
          return input;
      }
    )
  };
});

Je ne connais pas vraiment les promesses, est-ce la bonne façon de les utiliser?

22
Davounet

Voici un exemple:

app.filter("testf", function($timeout) {
    var data = null, // DATA RECEIVED ASYNCHRONOUSLY AND CACHED HERE
        serviceInvoked = false;

    function realFilter(value) { // REAL FILTER LOGIC
        return ...;
    }

    return function(value) { // FILTER WRAPPER TO COPE WITH ASYNCHRONICITY
        if( data === null ) {
            if( !serviceInvoked ) {
                serviceInvoked = true;
                // CALL THE SERVICE THAT FETCHES THE DATA HERE
                callService.then(function(result) {
                    data = result;
                });
            }
            return "-"; // PLACEHOLDER WHILE LOADING, COULD BE EMPTY
        }
        else return realFilter(value);
    }
});

Ce violon est une démonstration utilisant des délais d'attente au lieu de services.


EDIT: Selon le commentaire de sgimeno, des précautions supplémentaires doivent être prises pour ne pas appeler le service plus d'une fois. Voir les changements de serviceInvoked dans le code ci-dessus et les violons. Voir aussi violon fourchu avec Angular 1.2.1 et un bouton pour changer la valeur et déclencher des cycles de digest: violon fourch


EDIT 2: Selon le commentaire de Miha Eržen, cette solution ne fonctionne pas de logger pour Angular 1.3. La solution est cependant presque triviale, en utilisant le $stateful drapeau de filtre, documenté ici sous "Filtres avec état", et le nécessaire violon fourch .

Notez que cette solution nuirait aux performances, car le filtre est appelé à chaque cycle de résumé. La dégradation des performances peut être négligeable ou non, selon le cas spécifique.

42

Commençons par comprendre pourquoi le code d'origine ne fonctionne pas. J'ai un peu simplifié la question d'origine pour la rendre plus claire:

angular.module('angularApp').filter('pathToName', function(Service) {

    return function(input) {
        return Service.getCorresp().then(function(response) {
            return response;
        });
    });

}

Fondamentalement, le filtre appelle une fonction asynchrone qui renvoie la promesse, puis renvoie sa valeur. Un filtre en angular attend de vous que vous renvoyiez une valeur qui peut être facilement imprimée, par exemple une chaîne ou un nombre. Cependant, dans ce cas, même s'il semble que nous retournons le response de getCorresp, nous sommes en fait renvoyant un nouvelle promesse - La valeur de retour de toute then() ou catch() fonction est un promesse.

Angular essaie de convertir un objet de promesse en chaîne via le cast, n'obtenant rien de sensé en retour et affiche une chaîne vide.


Donc, ce que nous devons faire, c'est retourner une valeur temporaire - chaîne et la changer de manière asynchrone, comme ceci:

JSFiddle

HTML:

<div ng-app="app" ng-controller="TestCtrl">
    <div>{{'WelcomeTo' | translate}}</div>
    <div>{{'GoodBye' | translate}}</div>
</div>

Javascript:

app.filter("translate", function($timeout, translationService) {

    var isWaiting = false;
    var translations = null;

    function myFilter(input) {

        var translationValue = "Loading...";
        if(translations)
        {
            translationValue = translations[input];
        } else {
            if(isWaiting === false) {
                isWaiting = true;
                translationService.getTranslation(input).then(function(translationData) {
                    console.log("GetTranslation done");
                    translations = translationData;
                    isWaiting = false;
                });
            }
        }

        return translationValue;
    };

    return myFilter;
});

À chaque fois Angular essaie d'exécuter le filtre, il vérifierait si les traductions ont déjà été récupérées et si ce n'était pas le cas, il retournerait la valeur "Loading ...". Nous utilisons également le isWaiting valeur pour éviter d'appeler le service plus d'une fois.

L'exemple ci-dessus fonctionne bien pour Angular 1.2, cependant, parmi les changements de Angular 1.3, il y a une amélioration des performances qui modifie le comportement des filtres. Auparavant, le La fonction de filtrage a été appelée à chaque cycle de résumé. Depuis la version 1.3, cependant, elle n'appelle le filtre que si la valeur a été modifiée, dans notre dernier échantillon, elle n'appellerait plus jamais le filtre - 'WelcomeTo' ne changerait jamais.

Heureusement, le correctif est très simple, il vous suffit d'ajouter au filtre les éléments suivants:

JSFiddle

myFilter.$stateful = true;

Enfin, lors de la résolution de ce problème, j'ai eu un autre problème - j'avais besoin d'utiliser un filtre pour obtenir des valeurs asynchrones qui pourraient changer - Plus précisément, j'avais besoin de récupérer des traductions pour une seule langue, mais une fois l'utilisateur a changé la langue, j'avais besoin de récupérer un nouvel ensemble de langues. Cela s'est avéré un peu plus délicat, bien que le concept soit le même. Voici ce code:

JSFiddle

var app = angular.module("app",[]);
debugger;

app.controller("TestCtrl", function($scope, translationService) {
    $scope.changeLanguage = function() {
        translationService.currentLanguage = "ru";
    }
});

app.service("translationService", function($timeout) {
    var self = this;

    var translations = {"en": {"WelcomeTo": "Welcome!!", "GoodBye": "BYE"}, 
                        "ru": {"WelcomeTo": "POZHALUSTA!!", "GoodBye": "DOSVIDANYA"} };

    this.currentLanguage = "en";
    this.getTranslation = function(placeholder) {
        return $timeout(function() {
            return translations[self.currentLanguage][placeholder];
        }, 2000);
    }
})

app.filter("translate", function($timeout, translationService) {

    // Sample object: {"en": {"WelcomeTo": {translation: "Welcome!!", processing: false } } }
    var translated = {};
    var isWaiting = false;

    myFilter.$stateful = true;
    function myFilter(input) {

        if(!translated[translationService.currentLanguage]) {
            translated[translationService.currentLanguage] = {}
        }

        var currentLanguageData = translated[translationService.currentLanguage];
        if(!currentLanguageData[input]) {
            currentLanguageData[input] = { translation: "", processing: false };
        }

        var translationData = currentLanguageData[input];
        if(!translationData.translation && translationData.processing === false)
        {
            translationData.processing = true;
            translationService.getTranslation(input).then(function(translation) {
                console.log("GetTranslation done");
                translationData.translation = translation;
                translationData.processing = false;
            });
        }

        var translation = translationData.translation;
        console.log("Translation for language: '" + translationService.currentLanguage + "'. translation = " + translation);
        return translation;
    };

    return myFilter;
});
20
VitalyB