2017-06-12 144 views
10

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

只有一个问题,我似乎无法解决。该模板包含一个无限量的基本模板ng-include's。当我使用'最佳做法'$timeoutadvised here)它适用于我删除所有ng-include's。所以这不是我想要的。

的$超时例如:

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的异步性质。


工作代码

此代码返回,当它完成渲染(功能现在可以重复使用,see this question for the problem)HTML模板。但是这个解决方案是一个很大的问题,因为它使用角度专用的$$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; 
} 

回答

3

$compile同步功能。它只是同步编译给定的DOM,并不关心嵌套指令中发生了什么。如果嵌套指令具有异步加载的模板或其他防止其内容在同一记号上可用的内容,则这不是父指令的问题。

由于数据绑定和角度编译器是如何工作的,有当DOM可以肯定认为是“完整的”无明显的时刻,因为在每一个地方可能会发生变化,任何时间。 ng-include也可能涉及绑定,并且可能随时更改和加载包含的模板。

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

一种方法是添加一些确定性上的模板都参与;精心设计的应用程序不能太过松散。实际的解决方案取决于这个模板来自哪里以及为什么它包含随机嵌套模板。但是,这个想法是,使用的模板在使用之前应该放到模板缓存中。这可以使用构建工具完成,如gulp-angular-templates。或者通过ng-include之前的请求$templateRequest(其实质上是$http请求并将其放到$templateCache) - 做$templateRequest基本上是什么ng-include

虽然$compile$templateRequest是当模板缓存同步,ng-include是不是 - 它变得完全零延时(plunk)编制的下一个节拍,即$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()); 
    }) 
}); 

在使用中一般把模板缓存是摆脱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()); 
    } 
    }); 
}) 

注意两个选项将只处理异步模板请求导致的异步性。

+0

谢谢你的回答。然而,我似乎无法找到将第二种解决方案整合到我的功能中的方法(请参阅我的主题问题)。问题是,当我在创建的作用域对象上设置事件监视时,事件从未触发任何事件。你有一个例子,我应该如何将其整合到我的功能? 哦,你的plunkr不起作用。它不会给我任何html输出。 –

+0

庞克工程。它有'console.log'语句。检查控制台。我不确定你的整合意味着什么。您需要在范围上设置观察者并调用$ compile,就是这样。这里的顺序应该不重要,但是要先设置观察者。考虑提供一个可以重新创建问题的plunk,如果这不适合你。无论如何,ng-include是1.0以来的传统指令,如果可能的话应该避免,因为它不符合当前的Angular最佳实践。 – estus

+0

我刚刚发现,由于我使用$ rootScope。$ new()(我在服务中没有任何范围)事件不会被触发。你知道为什么,如果$ rootScope导致它,你知道任何解决方案吗?请参阅http://plnkr.co/edit/ZEVSG7TBpYirR77UDxcF?p=preview –

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,我将公开承诺函数来捕获输出模板,因为它工作的是异步。

$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

在加时赛中,如果您使用的是打字稿2.1,你可以使用async/await使代码看起来更清洁,而不是使用回调。它会像

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

你是否暗示$ compile函数是异步的,但没有实现任何“完成”回调? –

+1

@EricMORAND $ compile是一个异步函数,它没有任何可以告诉你何时完成的钩子。它与模板中的元素也是异步(例如:ng-include)并且没有任何钩子有关。由于这个$编译不能告诉你什么时候完成。推荐使用$ timeout,因为它将一个事件添加到浏览器堆栈的末尾。大多数情况下,执行$ timeout时执行$ compile。不幸的是ng-include包括ruens,因为它也是异步的,并在浏览器堆栈的末端创建事件。 –

+0

@Telvin Nguyen, 谢谢你的回答。然而这个例子并不适用于我,因为我不知道模板中导入了什么(包含多少ng)。由于这个原因,我无法确定在哪里放置ID,它会告诉我的函数编译完成。另外它使用jQuery。一个我在这个项目中无法访问的库。 –

相关问题