5

BEWARE

The accepted solution will break a directive with replace: true: HTML won't be replaced, specific CSS selectors in use will no longer work, etc.


I want my directive to dynamically change its template by watching a string received as attribute from the parent controller, so I used $compile from this answer together with $observe from this small part of an interesting tutorial, but alas it's not working, like shown in this plunkr.

About the error

If jQuery is included before AngularJS in the scripts, the replaceWith call throws me the following error:

TypeError: Cannot read property 'ownerDocument' of undefined

But if I remove jQuery, forcing AngularJS to use its jqLite, the same part throws this error, making things clearer to a total jQuery agnostic like me:

TypeError: Failed to execute 'replaceChild' on 'Node': parameter 1 is not of type 'Node'.

Even if it's clear to me that I'm not passing a valid 'Node' type object to the replaceWith, I don't know how to handle this situation, since I was expecting $compile to do the job.

The only things I know are that the console.log(tplContent) looks like this (a promise am I right?):

Object
{
  config: Object
  data: "<script type="text/ng-template" id="templateId.html">
  ↵  <p>TEMPLATE A</p>
  ↵</script>"
  headers: function (d)
  ng339: 10
  status: 200
  statusText: "OK"
}

while the console.log($compile(tplContent)(scope)) returns an array with that same object as first and only item:

[Object]
0: {
  config: Object
  data: "<script type="text/ng-template" id="templateId.html">
  ↵  <p>TEMPLATE A</p>
  ↵</script>"
  headers: function (d)
  ng339: 10
  status: 200
  statusText: "OK"
},
length: 1

I do really want to avoid using any of the following two fallbacks, have you got any idea of what I'm doing wrong here?


The fallbacks a.k.a. don't tell me to do this

I know I could split the directive into two directives and ng-if them like this:

(function() {
  'use-strict';

  angular.module('app')
  .directive('dynamicTemplateA', dynamicTemplate);

  DynTplCtrl.$inject = ['$http', '$templateCache', '$compile', '$parse'];

  function dynamicTemplate($http, $templateCache, $compile, $parse) {
    var directive = {
      restrict: 'E',
      templateUrl: 'template-a.html',
      scope: {},
      bindToController: {
        tpl: '@',
        i: '='
      },
      controller: DynTplCtrl,
      controllerAs: 'dyntplctrl',
      link: linkFunc
    }

    return directive;

    function linkFunc(scope, el, attrs, ctrl) {}
  }

  DynTplCtrl.$inject = [];

  function DynTplCtrl() {}

})()

(function() {
  'use-strict';

  angular.module('app')
  .directive('dynamicTemplateB', dynamicTemplate);

  DynTplCtrl.$inject = ['$http', '$templateCache', '$compile', '$parse'];

  function dynamicTemplate($http, $templateCache, $compile, $parse) {
    var directive = {
      restrict: 'E',
      templateUrl: 'template-b.html',
      scope: {},
      bindToController: {
        tpl: '@',
        i: '='
      },
      controller: DynTplCtrl,
      controllerAs: 'dyntplctrl',
      link: linkFunc
    }

    return directive;

    function linkFunc(scope, el, attrs, ctrl) {}
  }

  DynTplCtrl.$inject = [];

  function DynTplCtrl() {}

})()

and then in the controller.html:

<div ng-repeat="i in [1,2,3]">
  <dynamic-template-a ng-if="mainctrl.tpl === 'a'" tpl="{{mainctrl.tpl}}" i="i"></dynamic-template-a>
  <dynamic-template-b ng-if="mainctrl.tpl === 'b'" tpl="{{mainctrl.tpl}}" i="i"></dynamic-template-b>
</div>

I also know I could use ng-include like this:

(function() {
  'use-strict';

  angular.module('app')
  .directive('dynamicTemplateA', dynamicTemplate);

  DynTplCtrl.$inject = ['$http', '$templateCache', '$compile', '$parse'];

  function dynamicTemplate($http, $templateCache, $compile, $parse) {
    var directive = {
      restrict: 'E',
      template: '<div ng-include="dyntplctrl.getTemplateUrl()"></div>',
      scope: {},
      bindToController: {
        tpl: '@',
        i: '='
      },
      controller: DynTplCtrl,
      controllerAs: 'dyntplctrl',
      link: linkFunc
    }

    return directive;

    function linkFunc(scope, el, attrs, ctrl) {}
  }

  DynTplCtrl.$inject = [];

  function DynTplCtrl() {
    var vm = this;
    vm.getTemplateUrl = _getTemplateUrl;

    function _getTemplateUrl() {
      return 'template-' + vm.tpl + '.html';
    }
  }

})()
Gargaroz
  • 313
  • 9
  • 28
  • why not do it this way http://stackoverflow.com/a/40230128/4315380 – tanmay Apr 05 '17 at 14:41
  • It's the second fallback, and the actual implementation I adopted **and I'm trying to get away from it**; but the first problem I'm facing is one of the templates cannot reach the style rules contained within the `directive.less` file in its same folder (you won't find this issue in the plunkr, I didn't update it yet). And of course, it was able to reach them when it had not to switch dynamically between the two templates. – Gargaroz Apr 05 '17 at 15:13

2 Answers2

2

Credits to this question.

You need to change your code a bit while replacing the template:

el.html(tplContent.data);
$compile(el.contents())(scope);

This will replace the element's contents (though you need to handle sanitization here), and then compiles the template in the directive's scope.

Also, for testing, I've removed the <script> tags from the template-a.html, and template-b.html.

Here is a forked plunker which has the changes mentioned above.

31piy
  • 23,323
  • 6
  • 47
  • 67
  • even if I'm sad to said it (as I can't get the bounty); this is ; in my point of view the correct answer. Upvoted – aorfevre Jun 13 '17 at 14:41
  • @Gargaroz - Please check if this answer solves your problem. – 31piy Jun 14 '17 at 05:12
  • Thank you @31piy for even providing a source I couldn't find in the first place. – Gargaroz Jun 14 '17 at 09:57
  • @31piy there's only one flaw in this solution: it will break a directive with `replace: true`, meaning nothing will be replaced and specific CSS selectors in use will no longer work. I googled a bit about this issue, but alas I couldn't find a solution to keep HTML replaced even with a dynamic template: do you have any insight/suggestion about it? – Gargaroz Jun 15 '17 at 15:07
0

You don't have to put your HTML in script tag. Just store the plain HTML in your files like

template-a.html

<p>TEMPLATE A</p>

And modify your code a little bit to achieve what you want.

       function(tplContent) {
           var content = $compile(tplContent.data)(scope);
           if(el[0].childNodes.length){
             el[0].removeChild(el[0].childNodes[0]);
           }
          el.append(content);
        }
Himanshu Mittal
  • 794
  • 6
  • 19