2017-06-12 128 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。一個我在這個項目中無法訪問的庫。 –

相關問題