Как проверить, завершена ли компиляция $?

Я пишу функцию, которая может создать шаблон электронной почты из шаблона HTML и некоторую информацию, которая предоставляется. Для этого я использую $compile функцию Angular.

Есть только одна проблема, которую я не могу решить. Шаблон состоит из базового шаблона с неограниченным количеством ng-include. Когда я использую "лучшую практику" $timeout (здесь), он работает, когда я удаляю все ng-include. Так что это не то, что я хочу.

Пример $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;
     });

Когда я начинаю добавлять ng-include к шаблону, эта функция начинает возвращать шаблоны, которые еще не полностью скомпилированы (вложенные функции размещаются $timeout). Я считаю, что это из-за асинхронного характера ng-include.


Рабочий код

Этот код возвращает html-шаблон, когда выполняется рендеринг (функция теперь может быть повторно использована, см. этот вопрос для проблемы). Но это решение не стоит, поскольку он использует angular private $$phase для проверки наличия текущих $digest. Поэтому мне интересно, есть ли другое решение?

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

Что я хочу

Я хотел бы иметь функциональность, которая может обрабатывать неограниченное количество ng-inlude и возвращаться только при успешном создании шаблона. Я НЕ создаю этот шаблон и должен вернуть полностью скомпилированный шаблон.


Решение

После экспериментирования с ответом @estus я, наконец, нашел другой способ проверки при компиляции $. Это привело к приведенному ниже коду. Причина, по которой я использую $q.defer(), связана с тем, что шаблон разрешен в событии. Из-за этого я не могу вернуть результат, как нормальное обещание (я не могу сделать return scope.$on()). Единственная проблема в этом коде состоит в том, что она сильно зависит от ng-include. Если вы обслуживаете функцию, шаблон, который не имеет ng-include, $q.defer, никогда не будет заменен.

/**
 * 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;
}

Ответ 1

$compile - синхронная функция. Он просто компилирует данные DOM синхронно и не заботится о том, что происходит во вложенных директивах. Если вложенные директивы имеют асинхронно загруженные шаблоны или другие вещи, которые препятствуют тому, чтобы их содержимое было доступно на одном тике, это не относится к родительской директиве.

В связи с тем, как работает компилятор данных и Angular, нет четкого момента, когда DOM можно считать "полным", поскольку изменения могут происходить в любом месте и в любое время. ng-include может также включать привязки, и включенные шаблоны могут быть изменены и загружены в любой момент.

Реальная проблема здесь - это решение, которое не учитывало, как это будет реализовано позже. ng-include со случайным шаблоном подходит для прототипирования, но приведет к проблемам проектирования, и это один из них.

Один из способов справиться с этой ситуацией - добавить определенную уверенность в том, какие шаблоны задействованы; хорошо разработанное приложение не может позволить себе быть слишком свободным на своих участках. Фактическое решение зависит от того, откуда взялся этот шаблон, и почему он содержит случайные вложенные шаблоны. Но идея состоит в том, что используемые шаблоны должны быть помещены в шаблон, кешированный, прежде чем они будут использоваться. Это можно сделать с помощью таких инструментов построения, как gulp-angular-templates. Или, выполнив запросы до ng-include компиляции с помощью $templateRequest (который по существу выполняет $http запрос и помещает его в $templateCache) - выполнение $templateRequest в основном означает ng-include.

Хотя $compile и $templateRequest являются синхронными при кэшировании шаблонов, ng-include нет - он полностью скомпилируется при следующем тике, т.е. $timeout с нулевой задержкой (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());
  })
});

Обычно класть шаблоны, используемые для кеширования, является предпочтительным способом избавиться от асинхронности, которую Angular шаблоны привносят в жизненный цикл компиляции - не только для ng-include, но и для любых директив.

Другой способ - использовать ng-include события. Таким образом, приложение становится более свободным и основанным на событиях (иногда это хорошо, но чаще всего это не так). Поскольку каждый ng-include испускает событие, события нужно учитывать, а когда они есть, это означает, что иерархия директив ng-include была полностью скомпилирована (a бухнуться):

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

Обратите внимание, что оба параметра будут обрабатывать только асинхронность, вызванную асинхронными запросами шаблонов.

Ответ 2

Я думаю, что вы застряли в цепи обещания и компилируете событие. Я последовал за серией ваших вопросов, и это, возможно, то, что вы ищете, скомпилированную строку шаблона с рекурсивным ng-include.

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

// 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)


  }

В приведенном ниже примере directive-one - это контейнерный элемент, который завершает весь выходной шаблон, который мне нужен, поэтому вы можете изменить его на любой элемент, который вам нравится. Используя $q из Angular, я открою функцию обещания для захвата выходного шаблона, так как он работает async.

$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

В дополнение, если вы используете TypeScript 2.1, вы можете использовать async/await для сделайте код более чистым, вместо обратного вызова. Это было бы что-то вроде

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