Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS - Cannot change directive template dynamically in link function

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

})()
like image 775
Gargaroz Avatar asked Oct 29 '22 09:10

Gargaroz


1 Answers

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.

like image 118
31piy Avatar answered Nov 11 '22 13:11

31piy