Подтвердить что ты не робот

AngularJS: асинхронно инициализировать фильтр

У меня возникли проблемы с инициализацией фильтра с помощью асинхронных данных.

Фильтр очень прост, ему нужно перевести пути для имени, но для этого ему нужен соответствующий массив, который мне нужно извлечь с сервера.

Я могу сделать что-то в определении фильтра, прежде чем возвращать функцию, но асинхронный аспект предотвращает это

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

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

Использование обещания может быть жизнеспособным, но у меня нет четкого понимания того, как angular загружает фильтры. Это сообщение объясняет, как достичь такой магии с помощью сервисов, но возможно ли это сделать для фильтров?

И если у кого-то есть лучшая идея о том, как перевести эти пути, я все уши.

EDIT:

Я пробовал с обещанием, но что-то не так, и я не вижу, что:

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;
      }
    )
  };
});

Я действительно не знаком с promises, это правильный способ их использования?

4b9b3361

Ответ 1

Вот пример:

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);
    }
});

Этот fiddle является демонстрацией с использованием тайм-аутов вместо сервисов.


РЕДАКТИРОВАТЬ: В соответствии с комментарием sgimeno необходимо проявлять особую осторожность, чтобы не вызывать услугу более одного раза. См. Изменения serviceInvoked в приведенном выше коде и скриптах. См. Также forked fiddle с Angular 1.2.1 и кнопкой, чтобы изменить значения и циклы циклов запуска: разветвленная скрипка


РЕДАКТИРОВАТЬ 2: Согласно комментарию Миха Эржен, это решение не делает работу с logner для Angular 1.3. Решение почти тривиально, но, используя флаг фильтра $stateful, задокументированный здесь в разделе "Фильтры состояния" и необходимый раздвоенная скрипка.

Обратите внимание, что это решение повредило бы производительность, так как фильтр называется каждым циклом дайджест. Снижение производительности может быть незначительным или нет, в зависимости от конкретного случая.

Ответ 2

Давайте начнем с понимания того, почему исходный код не работает. Я немного упростил исходный вопрос, чтобы сделать его более понятным:

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

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

}

В принципе, фильтр вызывает функцию async, которая возвращает обещание, а затем возвращает свое значение. Фильтр в angular ожидает, что вы вернете значение, которое можно легко распечатать, например, строку или номер. Однако в этом случае, даже если кажется, что мы возвращаем response из getCorresp, мы на самом деле возвращаем новое обещание. Возвращаемое значение любая функция then() или catch() является обещанием.

Angular пытается преобразовать объект обещания в строку с помощью кастинга, не получая ничего разумного в обратном направлении и отображая пустую строку.


Так что нам нужно сделать, вернуть временное значение string и изменить его асинхронно, например:

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;
});

Каждый раз, когда angular пытается выполнить фильтр, он будет проверять, были ли уже переведены переводы, а если они не были, он вернет значение "Загрузка...". Мы также используем значение isWaiting для предотвращения вызова службы более одного раза.

Приведенный выше пример отлично работает для angular 1.2, однако среди изменений в angular 1.3 есть улучшение производительности, которое изменяет поведение фильтров. Раньше функция фильтра вызывалась каждый цикл дайджеста. Начиная с 1.3, однако, он вызывает только фильтр, если значение было изменено, в нашем последнем примере он никогда не вызовет фильтр снова - 'WelcomeTo' никогда не изменится.

К счастью, исправление очень просто, вам просто нужно добавить в фильтр следующее:

JSFiddle

myFilter.$stateful = true;

Наконец, имея дело с этой проблемой, у меня возникла другая проблема: мне нужно было использовать фильтр для получения значений async, которые может изменить. В частности, мне нужно было получить переводы для одного языка, но как только пользователь изменил язык, мне нужно было получить новый набор языков. Выполнение этого оказалось немного сложнее, хотя концепция такая же. Это код:

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;
});