如何检查$ compile是否已完成?

时间:2017-06-12 06:49:34

标签: javascript angularjs typescript asynchronous

我正在编写一个可以从HTML模板创建电子邮件模板的函数以及给出的一些信息。为此我使用Angular的$compile函数。

我只能解决一个问题。该模板由一个基本模板组成,其中包含无限量的ng-include个。当我使用“最佳做法”$timeoutadvised here)时,我删除所有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添加到模板时,此函数开始返回尚未完全编译的模板(workarround嵌套$timeout函数)。我相信这是因为ng-include的异步性质。

工作代码

此代码在完成渲染时返回html模板(现在可以重用函数see this question for the problem)。但是这个解决方案是一个很大的问题,因为它使用有角度的私有$$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回答后,我终于找到了另一种检查$ compile完成的方法。这导致了下面的代码。我使用$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;
}

2 个答案:

答案 0 :(得分:3)

$compile同步功能。它只是同步编译给定的DOM,而不关心嵌套指令中发生了什么。如果嵌套指令具有异步加载的模板或其他阻止其内容在同一刻度上可用的内容,则不需要考虑父指令。

由于数据绑定和Angular编译器的工作原理,没有明显的时刻可以将DOM视为“完整”,因为任何时候都可能在每个地方都发生变化。 ng-include也可能涉及绑定,并且随时可以更改和加载包含的模板。

这里的实际问题是没有考虑以后如何管理这个问题的决定。带有随机模板的ng-include可以用于原型设计,但会导致设计问题,这就是其中之一。

处理这种情况的一种方法是在涉及哪些模板时添加一些确定性;精心设计的应用程序不能在其零件上过于松散。实际的解决方案取决于此模板的来源以及包含随机嵌套模板的原因。但是我们的想法是,在使用模板之前,应该将使用过的模板放到缓存模板中。这可以使用gulp-angular-templates等构建工具完成。或者通过ng-include汇编$templateRequest之前的请求(基本上$http请求并将其放到$templateCache) - 执行$templateRequest基本上是ng-include 1}}确实。

虽然模板缓存时$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 events。通过这种方式,应用程序变得更加松散且基于事件(有时它是一件好事,但大多数情况下并非如此)。由于每个ng-include都会发出一个事件,因此需要对事件进行计数,当这些事件发生时,这意味着ng-include指令的层次结构已经完全编译(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());
    }
  });
})

请注意,这两个选项只会处理由异步模板请求引起的异步性。

答案 1 :(得分:1)

我认为你会受到承诺和编译事件的束缚。我按照你的问题的序列,这可能是你正在寻找的,带有递归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,我将公开promise函数来捕获输出模板,因为它工作异步。

$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')