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

时间:2022-05-14 21:01:45

I am writing a function that can create an email template from a HTML template and some information that is given. For this I am using the $compile function of Angular.

我正在编写一个函数,可以从HTML模板中创建电子邮件模板,并提供一些信息。为此,我使用$compile函数的角度。

There is only one problem I cannot seem to solve. The template consists of a base template with an unlimited amount of ng-include's. When I use the 'best practice' $timeout (advised here) It works when I remove all the ng-include's. So that is not what I want.

只有一个问题我似乎无法解决。模板由一个基本模板组成,其中包含了无限数量的ng-include。当我使用“最佳实践”$timeout(建议在这里)时,当我删除所有ng-include时,它会工作。这不是我想要的。

The $timeout example:

美元的超时的例子:

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

When I start to add ng-include's to the template this function starts to return templates that are not yet fully compiled (a workarround is nesting $timeout functions). I believe this is because of the async nature of a ng-include.

当我开始向模板添加ng-include时,这个函数开始返回尚未完全编译的模板(一个工作轮正在嵌套$timeout函数)。我认为这是因为ng-include是异步的。


Working code

工作代码

This code returns the html template when it is done rendering (function can now be reused, see this question for the problem). But this solution is a big no go since it is using the angular private $$phase to check if there are any ongoing $digest's. So I am wondering if there is any other solution?

此代码在完成呈现时返回html模板(现在可以重用函数,请参见问题)。但是这个解决方案是一个大的失败,因为它正在使用$$ $阶段来检查是否有任何正在进行的$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;
     });

What I want

我想要的

I would like to have a functionality that could handle an unlimited amount of ng-inlude's and only return when the template has succesfully been created. I am NOT rendering this template and need to return the fully compiled template.

我希望有一个功能,可以处理无限数量的ng-inlude,并且只有在模板成功创建时才返回。我没有呈现此模板,需要返回完全编译的模板。


Solution

解决方案

After experimenting with @estus answer I finally found an other way of checking when $compile is done. This resulted in the code below. The reason I am using $q.defer() is due to the fact that the template is resolved in an event. Due to this I cannot return the result like a normal promise (I cannot do return scope.$on()). The only problem in this code is that it depends heavily on ng-include. If you serve the function a template that doesn't have an ng-include the $q.defer is never resovled.

在试用了@estus的答案之后,我终于找到了另一种检查何时完成$compile的方法。这导致了下面的代码。我之所以使用$q. deferred()是因为模板是在事件中解析的。因此,我不能像正常的承诺那样返回结果(我不能返回范围。$on())这段代码中唯一的问题是它严重依赖于ng-include。如果您为函数提供了一个没有ng的模板(包括$q. deferred),则不会被resovled。

/**
 * 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 个解决方案

#1


3  

$compile is synchronous function. It just compiles given DOM synchronously and doesn't care about what's going on in nested directives. If nested directives have asynchronously loaded templates or other things that prevents their content from being available on the same tick, this is not a concern for parent directive.

美元编译是同步的功能。它只是同步地编译给定的DOM,并不关心嵌套指令中发生了什么。如果嵌套指令具有异步加载的模板或其他阻止它们的内容在同一时刻可用的东西,这不是父指令所关心的。

Due to how data binding and Angular compiler work, there's no distinct moment when DOM can be considered certainly 'complete', because changes may occur in every place, any time. ng-include may involve bindings too, and included templates may be changed and loaded at any moment.

由于数据绑定和角化编译器的工作方式,没有明显的时刻可以认为DOM是“完整的”,因为任何地方、任何时间都可能发生更改。ng-include也可能涉及绑定,包含的模板可能随时被修改和加载。

The actual problem here is the decision that didn't take into account how this will be managed later. ng-include with random template is ok for prototyping but will lead to design problems, and this is one of them.

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

One way to handle this situation is to add some certainty on which templates are involved; well-designed application cannot afford to be too loose on its parts. The actual solution depends on where this template originates from and why it contains random nested templates. But the idea is that used templates should be put to template cached before they will be used. This can be done with build tools like gulp-angular-templates. Or by doing requests prior to ng-include compilation with $templateRequest (which essentially does $http request and puts it to $templateCache) - doing $templateRequest is basically what ng-include does.

处理这种情况的一种方法是添加一些确定的模板;设计良好的应用程序不能承受部件太松。实际的解决方案取决于该模板来自何处,以及它为什么包含随机嵌套模板。但是我们的想法是,在使用模板之前,应该先将使用过的模板放到缓存的模板中。这可以通过构建工具如gulp-angular模板来实现。或者通过在ng之前执行请求——包含$templateRequest的编译(本质上是$http请求并将其放入$templateCache)—做$templateRequest基本上就是ng-include所做的事情。

Although $compile and $templateRequest are synchronous when templates are cached, ng-include is not - it becomes fully compiled on the next tick, i.e. $timeout with zero delay (a plunk):

虽然$compile和$templateRequest在缓存模板时是同步的,但ng-include不是——它在下一个滴答声中被完全编译,即$timeout与零延迟(一个错误):

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

Generally putting templates in use to cache is the preferable way to get rid of asynchronicity that Angular templates bring to compilation lifecycle - not only for ng-include but for any directives.

一般来说,将模板用于缓存是摆脱角模板给编译生命周期带来的异步性的最好方法——不仅用于ng-include,还用于任何指令。

Another way is to use ng-include events. This way the application becomes more loose and event based (sometimes it is a good thing but most times it's not). Since each ng-include emits an event, the events need to be counted, and when they are, this means that a hierarchy of ng-include directives has been completely compiled (a plunk):

另一种方法是使用ng-include事件。通过这种方式,应用程序变得更加松散和基于事件(有时是好事,但大多数时候不是)。由于每个ng-include都发出一个事件,所以需要对事件进行计数,当这些事件被计数时,这意味着一个ng-include指令的层次结构已经被完整地编译(一个扑通声):

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

Notice that both options will only handle asynchronicity that is caused by asynchronous template requests.

注意,这两个选项都只处理异步性,异步模板请求导致异步性。

#2


1  

I think you get stuck by chain the promise and compile event. I followed the serial of your questions and this maybe what you are looking for, the compiled template string with recursive ng-include.

我觉得你被这个承诺和编译事件困住了。我跟踪了你的一系列问题,这可能就是你要找的,用递归的ng-include编译的模板字符串。

First, we need to define ourself the function to detect when the compile is completed, there are couple ways to achieve that, but the duration checking is my best bet.

首先,我们需要定义自己函数来检测编译完成时的情况,有几种方法可以实现这一点,但我的最佳选择是持续时间检查。

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


  }

In below example, directive-one is the container element to wrap up all of the output template that I need, so you could change it to what-ever element that you like. By using $q of Angular, I will expose the promise function to capture the output template since it works async.

在下面的例子中,direct -one是将我需要的所有输出模板封装起来的容器元素,因此您可以将它更改为您喜欢的任何元素。通过使用$q的角度,我将公开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

TL,博士;我的演示砰砰作响

In extra, if you are using the TypeScript 2.1, you could use async/await to make the code looks more cleaner instead of using callback. It would be something like

另外,如果您正在使用TypeScript 2.1,您可以使用async/ wait来使代码看起来更干净,而不是使用回调。就像

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

#1


3  

$compile is synchronous function. It just compiles given DOM synchronously and doesn't care about what's going on in nested directives. If nested directives have asynchronously loaded templates or other things that prevents their content from being available on the same tick, this is not a concern for parent directive.

美元编译是同步的功能。它只是同步地编译给定的DOM,并不关心嵌套指令中发生了什么。如果嵌套指令具有异步加载的模板或其他阻止它们的内容在同一时刻可用的东西,这不是父指令所关心的。

Due to how data binding and Angular compiler work, there's no distinct moment when DOM can be considered certainly 'complete', because changes may occur in every place, any time. ng-include may involve bindings too, and included templates may be changed and loaded at any moment.

由于数据绑定和角化编译器的工作方式,没有明显的时刻可以认为DOM是“完整的”,因为任何地方、任何时间都可能发生更改。ng-include也可能涉及绑定,包含的模板可能随时被修改和加载。

The actual problem here is the decision that didn't take into account how this will be managed later. ng-include with random template is ok for prototyping but will lead to design problems, and this is one of them.

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

One way to handle this situation is to add some certainty on which templates are involved; well-designed application cannot afford to be too loose on its parts. The actual solution depends on where this template originates from and why it contains random nested templates. But the idea is that used templates should be put to template cached before they will be used. This can be done with build tools like gulp-angular-templates. Or by doing requests prior to ng-include compilation with $templateRequest (which essentially does $http request and puts it to $templateCache) - doing $templateRequest is basically what ng-include does.

处理这种情况的一种方法是添加一些确定的模板;设计良好的应用程序不能承受部件太松。实际的解决方案取决于该模板来自何处,以及它为什么包含随机嵌套模板。但是我们的想法是,在使用模板之前,应该先将使用过的模板放到缓存的模板中。这可以通过构建工具如gulp-angular模板来实现。或者通过在ng之前执行请求——包含$templateRequest的编译(本质上是$http请求并将其放入$templateCache)—做$templateRequest基本上就是ng-include所做的事情。

Although $compile and $templateRequest are synchronous when templates are cached, ng-include is not - it becomes fully compiled on the next tick, i.e. $timeout with zero delay (a plunk):

虽然$compile和$templateRequest在缓存模板时是同步的,但ng-include不是——它在下一个滴答声中被完全编译,即$timeout与零延迟(一个错误):

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

Generally putting templates in use to cache is the preferable way to get rid of asynchronicity that Angular templates bring to compilation lifecycle - not only for ng-include but for any directives.

一般来说,将模板用于缓存是摆脱角模板给编译生命周期带来的异步性的最好方法——不仅用于ng-include,还用于任何指令。

Another way is to use ng-include events. This way the application becomes more loose and event based (sometimes it is a good thing but most times it's not). Since each ng-include emits an event, the events need to be counted, and when they are, this means that a hierarchy of ng-include directives has been completely compiled (a plunk):

另一种方法是使用ng-include事件。通过这种方式,应用程序变得更加松散和基于事件(有时是好事,但大多数时候不是)。由于每个ng-include都发出一个事件,所以需要对事件进行计数,当这些事件被计数时,这意味着一个ng-include指令的层次结构已经被完整地编译(一个扑通声):

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

Notice that both options will only handle asynchronicity that is caused by asynchronous template requests.

注意,这两个选项都只处理异步性,异步模板请求导致异步性。

#2


1  

I think you get stuck by chain the promise and compile event. I followed the serial of your questions and this maybe what you are looking for, the compiled template string with recursive ng-include.

我觉得你被这个承诺和编译事件困住了。我跟踪了你的一系列问题,这可能就是你要找的,用递归的ng-include编译的模板字符串。

First, we need to define ourself the function to detect when the compile is completed, there are couple ways to achieve that, but the duration checking is my best bet.

首先,我们需要定义自己函数来检测编译完成时的情况,有几种方法可以实现这一点,但我的最佳选择是持续时间检查。

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


  }

In below example, directive-one is the container element to wrap up all of the output template that I need, so you could change it to what-ever element that you like. By using $q of Angular, I will expose the promise function to capture the output template since it works async.

在下面的例子中,direct -one是将我需要的所有输出模板封装起来的容器元素,因此您可以将它更改为您喜欢的任何元素。通过使用$q的角度,我将公开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

TL,博士;我的演示砰砰作响

In extra, if you are using the TypeScript 2.1, you could use async/await to make the code looks more cleaner instead of using callback. It would be something like

另外,如果您正在使用TypeScript 2.1,您可以使用async/ wait来使代码看起来更干净,而不是使用回调。就像

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