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';
    }
  }

})()
2
  • why not do it this way stackoverflow.com/a/40230128/4315380 Commented Apr 5, 2017 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. Commented Apr 5, 2017 at 15:13

2 Answers 2

2
+100

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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
@Gargaroz - Please check if this answer solves your problem.
Thank you @31piy for even providing a source I couldn't find in the first place.
@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?
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);
        }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.