2017-06-12 16 views
10

Piszę funkcję, która może utworzyć szablon wiadomości e-mail z szablonu HTML i podanych informacji. W tym celu korzystam z funkcji Angular z zakresu $compile.

Jest tylko jeden problem, którego nie mogę rozwiązać. Szablon składa się z szablonu bazowego z nieograniczoną liczbą ng-include. Kiedy używam "najlepszej praktyki" $timeout (advised here) Działa po usunięciu wszystkich ng-include. Więc nie tego chcę.

Przykład $ timeout:

return this.$http.get(templatePath) 
    .then((response) => { 
     let template = response.data; 
     let scope = this.$rootScope.$new(); 
     angular.extend(scope, processScope); 

     let generatedTemplate = this.$compile(jQuery(template))(scope); 
     return this.$timeout(() => { 
      return generatedTemplate[0].innerHTML; 
     }); 
    }) 
    .catch((exception) => { 
     this.logger.error(
      TemplateParser.getOnderdeel(process), 
      "Email template creation", 
      (<Error>exception).message 
     ); 
     return null; 
    }); 

Kiedy zacznę dodawać ng-include do danego szablonu funkcja ta rozpoczyna powrót szablony, które nie są jeszcze w pełni skompilowane (a workarround jest zagnieżdżanie $timeout funkcje). Uważam, że dzieje się tak z powodu asynchronicznej natury modelu ng-include.


kod

Ten kod zwraca szablon HTML, gdy jest dokonywane renderingu (funkcja może być teraz użyte ponownie, see this question for the problem) Praca. Ale to rozwiązanie nie ma sensu, ponieważ używa on kątowego prywatnego $$phase, aby sprawdzić, czy są jakieś trwające $digest. Zastanawiam się, czy istnieje jakieś inne rozwiązanie?

return this.$http.get(templatePath) 
    .then((response) => { 
     let template = response.data; 
     let scope = this.$rootScope.$new(); 
     angular.extend(scope, processScope); 

     let generatedTemplate = this.$compile(jQuery(template))(scope); 
     let waitForRenderAndPrint =() => { 
      if (scope.$$phase || this.$http.pendingRequests.length) { 
       return this.$timeout(waitForRenderAndPrint); 
      } else { 
       return generatedTemplate[0].innerHTML; 
      } 
     }; 
     return waitForRenderAndPrint(); 
    }) 
    .catch((exception) => { 
     this.logger.error(
      TemplateParser.getOnderdeel(process), 
      "Email template creation", 
      (<Error>exception).message 
     ); 
     return null; 
    }); 

Co chcę

chciałbym mieć funkcjonalność, która mogłaby obsługiwać nieograniczoną ilość ng-inlude „s, a jedynie powrót gdy szablon został pomyślnie utworzony. NIE renderuję tego szablonu i muszę zwrócić w pełni skompilowany szablon.


Rozwiązanie

Po eksperymentach z @estus odpowiedź końcu znalazłem inny sposób na sprawdzenie, kiedy $ kompilacji jest wykonywana. W ten sposób powstał poniższy kod. Przyczyna, dla której używam $q.defer(), wynika z faktu, że szablon został rozstrzygnięty w przypadku zdarzenia. Z tego powodu nie mogę zwrócić wyniku jak normalna obietnica (nie mogę zrobić return scope.$on()). Jedynym problemem w tym kodzie jest to, że zależy on w znacznym stopniu od ng-include. Jeśli podasz tę funkcję, szablon, który nie ma wartości ng-include, nigdy nie zostanie ponownie przesłany.

/** 
* Using the $compile function, this function generates a full HTML page based on the given process and template 
* It does this by binding the given process to the template $scope and uses $compile to generate a HTML page 
* @param {Process} process - The data that can bind to the template 
* @param {string} templatePath - The location of the template that should be used 
* @param {boolean} [useCtrlCall=true] - Whether or not the process should be a sub part of a $ctrl object. If the template is used 
* for more then only an email template this could be the case (EXAMPLE: $ctrl.<process name>.timestamp) 
* @return {IPromise<string>} A full HTML page 
*/ 
public parseHTMLTemplate(process: Process, templatePath: string, useCtrlCall = true): ng.IPromise<string> { 
    let scope = this.$rootScope.$new(); //Do NOT use angular.extend. This breaks the events 

    if (useCtrlCall) { 
     const controller = "$ctrl"; //Create scope object | Most templates are called with $ctrl.<process name> 
     scope[controller] = {}; 
     scope[controller][process.__className.toLowerCase()] = process; 
    } else { 
     scope[process.__className.toLowerCase()] = process; 
    } 

    let defer = this.$q.defer(); //use defer since events cannot be returned as promises 
    this.$http.get(templatePath) 
     .then((response) => { 
      let template = response.data; 
      let includeCounts = {}; 
      let generatedTemplate = this.$compile(jQuery(template))(scope); //Compile the template 

      scope.$on('$includeContentRequested', (e, currentTemplateUrl) => { 
         includeCounts[currentTemplateUrl] = includeCounts[currentTemplateUrl] || 0; 
         includeCounts[currentTemplateUrl]++; //On request add "template is loading" indicator 
        }); 
      scope.$on('$includeContentLoaded', (e, currentTemplateUrl) => { 
         includeCounts[currentTemplateUrl]--; //On load remove the "template is loading" indicator 

      //Wait for the Angular bindings to be resolved 
      this.$timeout(() => { 
       let totalCount = Object.keys(includeCounts) //Count the number of templates that are still loading/requested 
        .map(templateUrl => includeCounts[templateUrl]) 
        .reduce((counts, count) => counts + count); 

       if (!totalCount) { //If no requests are left the template compiling is done. 
        defer.resolve(generatedTemplate.html()); 
       } 
       }); 
      }); 
     }) 
     .catch((exception) => {     
      defer.reject(exception); 
     }); 

    return defer.promise; 
} 

Odpowiedz

3

$compile jest synchroniczny funkcja. Po prostu kompiluje dany DOM synchronicznie i nie dba o to, co dzieje się w zagnieżdżonych dyrektywach. Jeśli dyrektywy zagnieżdżone mają asynchronicznie załadowane szablony lub inne rzeczy, które uniemożliwiają ich zawartość na tym samym tikku, nie jest to problemem dla dyrektywy nadrzędnej.

Ze względu na sposób powiązania danych i kompilator Angular nie ma wyraźnego momentu, w którym DOM można uznać za "kompletny", ponieważ zmiany mogą wystąpić w każdym miejscu i czasie. ng-include może również wiązać się z powiązaniami, a szablony mogą być zmieniane i ładowane w dowolnym momencie.

Rzeczywistym problemem jest tutaj decyzja, która nie uwzględniała późniejszego zarządzania. ng-include z losowym szablonem jest w porządku do prototypowania, ale prowadzi do problemów projektowych, a to jest jedna z nich.

Jednym ze sposobów poradzenia sobie z tą sytuacją jest zwiększenie pewności, które szablony są zaangażowane; dobrze zaprojektowana aplikacja nie może sobie pozwolić na zbytnią swobodę w swoich częściach. Rzeczywiste rozwiązanie zależy od tego, skąd pochodzi ten szablon i dlaczego zawiera losowe szablony zagnieżdżone. Ale pomysł polega na tym, że używane szablony powinny być umieszczane w szablonach w pamięci podręcznej, zanim zostaną użyte. Można to zrobić za pomocą narzędzi do budowania, takich jak gulp-angular-templates. Lub wykonując żądania przed kompilacją ng-include z $templateRequest (co w istocie robi $http żądanie i umieszcza je na $templateCache) - robi $templateRequest jest w zasadzie to, co robi ng-include.

Chociaż $compile i $templateRequest są synchroniczne gdy szablony są buforowane, ng-include nie jest - staje się w pełni opracowane na następnym kleszcza, tzn $timeout z zerowym opóźnieniem (a plunk):

var templateUrls = ['foo.html', 'bar.html', 'baz.html']; 

$q.all(templateUrls.map(templateUrl => $templateRequest(templateUrl))) 
.then(templates => { 
    var fooElement = $compile('<div><ng-include src="\'foo.html\'"></ng-include></div>')($scope); 

    $timeout(() => { 
    console.log(fooElement.html()); 
    }) 
}); 

Generalnie wprowadzenie szablonów w użyciu do pamięci podręcznej jest lepszym sposobem na pozbycie się asynchroniczności, jaką szablony kątowe wprowadzają do cyklu życia kompilacji - nie tylko dla ng-include, ale dla dowolnych dyrektyw.

Innym sposobem jest użycie ng-include events. W ten sposób aplikacja staje się bardziej luźna i oparta na zdarzeniach (czasami jest to dobra rzecz, ale najczęściej nie jest).Ponieważ każdy ng-include emituje imprezę, wydarzenia muszą być policzone, a gdy są, oznacza to, że hierarchia ng-include dyrektyw została całkowicie skompilowany (a plunk):

var includeCounts = {}; 

var fooElement = $compile('<div><ng-include src="\'foo.html\'"></ng-include></div>')($scope); 

$scope.$on('$includeContentRequested', (e, currentTemplateUrl) => { 
    includeCounts[currentTemplateUrl] = includeCounts[currentTemplateUrl] || 0; 
    includeCounts[currentTemplateUrl]++; 
}) 
// should be done for $includeContentError as well 
$scope.$on('$includeContentLoaded', (e, currentTemplateUrl) => { 
    includeCounts[currentTemplateUrl]--; 

    // wait for a nested template to begin a request 
    $timeout(() => { 
    var totalCount = Object.keys(includeCounts) 
    .map(templateUrl => includeCounts[templateUrl]) 
    .reduce((counts, count) => counts + count); 

    if (!totalCount) { 
     console.log(fooElement.html()); 
    } 
    }); 
}) 

Zauważmy, że obie opcje będą obsługiwać tylko asynchroniczność spowodowana przez asynchroniczne żądania szablonu.

+0

Dziękuję za odpowiedź. Jednak nie mogę znaleźć sposobu na zintegrowanie drugiego rozwiązania z moją funkcją (zobacz moje pytanie na temat). Problem polega na tym, że zdarzenie nigdy nie jest wyzwalane w żadnym ze zdarzeń po ustawieniu obserwatora zdarzeń na utworzonym obiekcie zasięgu. Czy masz przykład, w jaki sposób powinienem zintegrować to z moją funkcją? oh i twój plunkr nie działa. To nie daje mi żadnego wyjścia HTML. –

+0

The plunk działa. Ma instrukcje 'console.log'. Sprawdź konsolę. Nie jestem pewien, co masz na myśli mówiąc o integracji. Musisz ustawić obserwatorów na zasięgu i zadzwonić do $ compile, właśnie to. Kolejność nie powinna mieć znaczenia, ale najpierw spróbuj ustawić obserwatorów. Rozważ dostarczenie pluna, które może odtworzyć problem, jeśli to nie działa. Jakikolwiek sposób, ng-include jest wersją starszą od 1.0 i powinno się jej unikać, jeśli jest to możliwe, ponieważ nie jest zgodne z najlepszymi praktykami kątowymi. – estus

+0

Właśnie dowiedziałem się, że ze względu na fakt, że używam $ rootScope. $ New() (nie mam żadnego zakresu w usłudze) Zdarzenia nie są uruchamiane. Czy wiesz, dlaczego i jeśli $ rootScope to powoduje, czy znasz jakieś rozwiązanie? zobacz http://plnkr.co/edit/ZEVSG7TBpYirR77UDxcF?p=preview –

1

Myślę, że utknąłeś przez łańcuch obietnicy i wydarzenie kompilacji. Poszedłem za serią twoich pytań, a to może, czego szukasz, skompilowanym łańcuchem szablonów z rekurencyjnym ng-include.

Po pierwsze, musimy zdefiniować się funkcję wykrywania, kiedy kompilacja jest ukończona, istnieje kilka sposobów na osiągnięcie tego, ale kontrola czasu jest moim najlepszym wyborem.

// pass searchNode, this will search the children node by elementPath, 
// for every 0.5s, it will do the search again until find the element 
function waitUntilElementLoaded(searchNode, elementPath, callBack){ 

    $timeout(function(){ 

     if(searchNode.find(elementPath).length){ 
      callBack(elementPath, $(elementPath)); 
     }else{ 
     waitUntilElementLoaded(searchNode, elementPath, callBack); 
     } 
     },500) 


    } 

W poniższym przykładzie directive-one jest elementem pojemnik owinąć całą szablonu wyjściowego, że muszę, więc można go zmienić na co-kiedykolwiek element, który chcesz. Używając $ q Angular, ujawnię funkcję obietnicy, aby uchwycić szablon wyjściowy, ponieważ działa on asynchronicznie.

$scope.getOutput = function(templatePath){ 


    var deferred = $q.defer(); 
    $http.get(templatePath).then(function(templateResult){ 
     var templateString = templateResult.data; 
     var result = $compile(templateString)($scope) 


    waitUntilElementLoaded($(result), 'directive-one', function() { 

     var compiledStr = $(result).find('directive-one').eq(0).html(); 
     deferred.resolve(compiledStr); 
    }) 

    }) 

    return deferred.promise; 


    } 



    // usage 

    $scope.getOutput("template-path.html").then(function(output){ 
     console.log(output) 
    }) 

TL; DR; My Demo plunker

W ekstra, jeśli używasz maszynopis 2.1, można użyć async/await aby kod wygląda bardziej czystsze zamiast korzystania zwrotnego. Byłoby coś

var myOutput = await $scope.getOutput('template-path') 
+0

Czy sugerujesz, że funkcja $ compile jest asynchroniczna, ale nie wykonuje żadnego "wykonanego" wywołania zwrotnego? –

+1

@EricMORAND $ compile to funkcja asynchroniczna, która nie ma żadnych haków, które mogą powiedzieć, kiedy to się stanie. Ma to związek z tym, że elementy w szablonie są również asynchroniczne (przykład: ng-include), a także nie mają żadnych haków. z powodu tej kompilacji $ nie może powiedzieć kiedy to się stanie. Polecenie $ timeout jest zalecane, ponieważ dodaje zdarzenie na końcu stosu przeglądarki. Zazwyczaj kompilacja $ jest wykonywana, gdy wykonywany jest $ timeout. Niestety ng-include ransuje to ze względu na asynchronizację i tworzenie zdarzeń na końcu stosu przeglądarki. –

+0

@Telvin Nguyen, Dziękuję za odpowiedź. Jednak ten przykład nie działa dla mnie, ze względu na to, że nie wiem, co jest zaimportowane w szablonie (ile zawiera ng-include). z tego powodu nie mogę określić, gdzie umieścić identyfikator, który powie dla mojej funkcji, że jest wykonywane kompilowanie. Również używa jQuery. Biblioteka, do której nie mam dostępu w tym projekcie. –

Powiązane problemy