4

In my Angular 1.5.11 app, I'm trying to programmatically compile a template and get the result as a HTML string. The content of the template is

<div ng-if="foo">
  <div>foo is {{foo}}, bar is {{bar}}</div>
</div> 

My attempt at compiling it to a HTML string:

app.controller('MainCtrl', function($scope, $rootScope, $compile) {

  function compileHtmlTemplate (templateContent, model) {
    var templateScope = $rootScope.$new(true);
    angular.extend(templateScope, model);

    return $compile(templateContent)(templateScope);
  }

  var templateContent = $('#template').html();
  var model = {foo: 'fooValue', bar: 'barValue'};
  var compiledHtml = compileHtmlTemplate(templateContent, model);
  var htmlAsString = $('<div></div>').html(compiledHtml).html();

  $scope.result = htmlAsString;
});

However, as you can see in this Plunker example, it doesn't work. I need to compile the template rather than just interpolating it, because it contains an ng-if directive.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Dónal
  • 185,044
  • 174
  • 569
  • 824
  • Trying to invent wheel? Or what you try to get? As a commented in your previous question you probably want $interpolate not $compile. – Petr Averyanov Apr 07 '17 at 11:19
  • you need to $eval the values after compiling – xelilof Apr 07 '17 at 11:22
  • @PetrAveryanov `$interpolate` won't work because the template contains directives – Dónal Apr 07 '17 at 11:37
  • @Dónal quick reality check - what do you actually want to achieve? Is it [XY Problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem)? You have very high reputation (0.12% overall) so I'm wondering why `$('#template').html()` - it's jQuery, not Angular way. – Mars Robertson Apr 10 '17 at 09:46
  • @MichalStefanow directives in the template will not be evaluated if I use `$('#template').html()` – Dónal Apr 10 '17 at 11:51
  • @Dónal I still don't understand what do you really want to achieve? I mentioned your very high reputation, but it's mostly Java-related - http://stackoverflow.com/users/2648/d%C3%B3nal?tab=tags - in Angular there are certain conventions of doing things and if you explain what is your end goal there might be much better, easier, more maintainable way... But first things first - what happens if you use `$sce` as described here: http://stackoverflow.com/questions/18340872/how-do-you-use-sce-trustashtmlstring-to-replicate-ng-bind-html-unsafe-in-angu Also try `ng-include` :) – Mars Robertson Apr 10 '17 at 12:29
  • @Dónal - please have a look - https://plnkr.co/edit/rI4prJn9nsZOKI3IzOPq?p=preview - is that something you want to achieve? If yes I'll publish as an answer, but first I need to understant your goal... _(see my previous comment about XY problem)_ – Mars Robertson Apr 10 '17 at 12:50
  • @MichalStefanow actually, most of my reputation has been earned for Groovy and Grails, rather than Java, but I don't see how this is relevant to this question. I think you're implying that I don't have much Angular experience, but in truth, I mostly stopped using stackoverflow around the same time I started working with Angular (about 2 years ago). – Dónal Apr 11 '17 at 11:20
  • @Dónal For the third time I'll ask the question - **WHAT DO YOU REALLY WANT TO ACHIEVE** _(avoiding XY problem)_ - the code in question is rather unconventional and I did assume that you specialise in something else than Angular... Linkt to **[plnkr](https://plnkr.co/edit/rI4prJn9nsZOKI3IzOPq?p=preview)** once again but not sure if that serves your need. – Mars Robertson Apr 11 '17 at 11:49
  • So what's the expected output? An empty string or something? – blackmiaool Apr 11 '17 at 11:59
  • @MichalStefanow regarding your Plunker example, I can't use `ng-include` because I want to assign the result of compiling the template to a variable, rather than including it in the DOM. – Dónal Apr 11 '17 at 13:00
  • 1
    @blackmiaool `foo` has a value so `ng-if`evaluates as true. – K Scandrett Apr 12 '17 at 03:18
  • I think you should use `ngBindHtml` or sanitise will remove it, secondly `$compile` isn't working very well with the `ngIf` – maurycy Apr 12 '17 at 11:39
  • @Dónal `programmatically compile a template and get the result as a HTML string` + `assign the result of compiling the template to a variable, rather than including it in the DOM` = what do you really want to achieve? _(same question repeated over for the 4th time)_ Maybe use the DOM, `display:none`, get your string from there? – Mars Robertson Apr 12 '17 at 15:03
  • @MichalStefanow I want to achieve what I described in the question. No more, no less. – Dónal Apr 14 '17 at 13:30
  • @Dónal It's OK, just the `$scope.result = htmlAsString;` made me think... What are you actually trying to do with compiled string. I'm like a record stuck on repeat, I'm only suggesting more conventional Angular way - if only you could explain, more or less :) – Mars Robertson Apr 14 '17 at 21:41
  • You obviously have XY problem - you can't compile it to string and maintain bindings at the same time. 'what do you really want to achieve' is perfectly valid question here and it still wasn't answered in the question. You will get the answers of a better quality if you will explain how you are actually using this string. Showing it as `{{ result }}` doesn't explain anything. – Estus Flask Apr 15 '17 at 21:54
  • 1
    @estus The OP didn't say it wasn't a useful, valid question; just that they don't want to explain it further. That's entirely their prerogative. Details could be commercially sensitive, for one. Also, at 100k they have been around long enough to have some level of experience under the hood. – K Scandrett Apr 16 '17 at 05:13

4 Answers4

2

Angular's digest cycle needs to complete before you'll have access to the value. You can achieve that by using $timeout.

I've used $templateCache to retrieve the template since that's the official way, but that's just a personal preference.

var app = angular.module('plunker', ['ngSanitize']);

app.controller('MainCtrl', function($scope, $rootScope, $compile, $timeout, $templateCache) {

  function compileHtmlTemplate (templateContent, model) {
    var templateScope = $rootScope.$new(true);
    angular.extend(templateScope, model);

    var compiled = $compile(templateContent)(templateScope);

    var parent = $("<div>").html(compiled);

    console.log("Won't work! Digest hasn't finished:", parent[0].innerHTML);

    $timeout(function(){ // must wait till Angular has finished digest
      console.log('Will work, digest has now completed:', parent[0].innerHTML);

      $scope.result = parent[0].innerHTML;
    }, 0);
  }

  // either do something with the compiled value within the $timeout, or watch for it
  $scope.$watch('result', function(newValue, oldValue) {
      if (newValue !== undefined) {
        console.log("Do something in the $timeout or here... " + newValue);
      }
  });

  // you can access the template via the template cache if you wanted
  var templateContent = $templateCache.get('template');
  var model = {foo: 'fooValue', bar: 'barValue'};
  var compiledHtml = compileHtmlTemplate(templateContent, model);
});

Updated Plunker: https://plnkr.co/edit/ajC3Iqkxpi6O9gfieHeR?p=preview

K Scandrett
  • 16,390
  • 4
  • 40
  • 65
  • Updated Plunk to compare results with the rendered the version of the template. Also added commented out test case with `foo: false` – K Scandrett Apr 12 '17 at 03:09
0

Yes you can use $interpolate like this here

var app = angular.module('plunker', ['ngSanitize']);

app.controller('MainCtrl', function($scope, $rootScope, $compile, $interpolate) {

  function compileHtmlTemplate (templateContent, model) {
    var templateScope = $rootScope.$new(true);
    angular.extend(templateScope, model);

    var tpl = $compile(templateContent)(templateScope);
    console.log(tpl);

    return $interpolate(tpl.html(),model)
  }

  var templateContent = $('#template').html();

  var model = {foo: 'fooValue', bar: 'barValue'};
  var compiledHtml =compileHtmlTemplate(templateContent)(model);
  //console.log(compiledHtml)
  var htmlAsString = $('<div></div>').html(compiledHtml).html();

  $scope.result = htmlAsString;
});

Returning the compiled html with model interpolation then using same.

$compile returns an element not an html string

https://plnkr.co/edit/TPvOXb5TWHGUunBMFubF?p=preview

Vinod Louis
  • 4,812
  • 1
  • 25
  • 46
  • Thanks for your answer. I forgot to mention that the template has a directive in it, so I need to compile it. I tried your solution after adding an `ng-if` directive to the template, but it doesn't work. I've updated my question and the Plunker example to include a directive in the template. – Dónal Apr 07 '17 at 11:40
0

If you want to display an HTML you can't simply use {{}} you would need to use ng-bind-html or utilise the $sanitaze service of angular

I've updated your plunker https://plnkr.co/edit/GwM5CEemo961s2CeUsdA?p=preview

var compiledHtml = compileHtmlTemplate(templateContent, model);
var element = angular.element('<div></div>').html(compiledHtml)
$timeout(function() {
  $scope.result = element.html()
})

Because of the ngIf in template the compiledHtml returned only ngIf comment, I've fought it before and the only solution I came up with is as above when you append the compiledHtml to newly created element and retrieving the html in $timeout without time specified as it will happen after the appending and $digest cycle. Hope that helps

maurycy
  • 8,455
  • 1
  • 27
  • 44
-1

below is the working version of your code (most of change are related to do things in angular way)

  var templateContent = $templateCache.get("template");
  //var templateContent = $('#template').html();
  //comment: get html in angular way, not using jquery

  var model = {foo: 'fooValue', bar: 'barValue'};
  var templateScope = $rootScope.$new(true);
  templateScope = angular.extend(templateScope, model);
  //angular.extend(templateScope, model);
  //comment: make sure to capture result of angular.extend

  var compiledElement = $compile(templateContent)(templateScope);
  //var compiledHtml = $compile(templateContent)(templateScope);
  //comment: result of compilation is not an html but an angular element

  var targetElement = angular.element("#result");
  //var htmlAsString = $('<div></div>').html(compiledHtml).html();
  //comment: find the element using angular and not jquery


  targetElement.append(compiledElement);
  //$scope.result = htmlAsString;
  //comment: don't just assign string, append an angular element

update plunker link

a small change in html is

  replaced
  <div>{{result}}</div>

  with
  <div id="result"></div>
harishr
  • 17,807
  • 9
  • 78
  • 125