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