aizquier
aizquier

Reputation: 567

Angular expression selecting a directive

I need to render a angular directive, selecting it by appealing to a string previously defined at a variable (usually declared in the controller). Despite such a variable is accessible as an Angular expression, when I try to use for selecting a directive it doesn't work:

<!DOCTYPE html>
<html ng-app="app">
<body ng-controller="TextController">

<!-- item.dir is accessible: -->
<div>Value of item: {{item.dir}}</div>

<!-- It works. the directive "hello" is rendered -->
<div class="hello"></div>
<hello></hello>

Here you should see additional text:
<!-- Doesn't work item.dir is not recognized-->
<!-- as a class -->
<div class="{{item.dir}}"></div>

<!-- as an attribute-->
<div {{item.dir}}></div>

<!-- trying ng-class (it fails)-->
<div ng-class="item.dir"></div>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.5/angular.min.js"></script>

<script>

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

  // The directive to render
  appModule.directive('hello', function() {
        return {
          restrict: 'ACE',
          template: '<div>works: Transcoded text</div>',
          replace: true
        };
      });

  appModule.controller('TextController', function ($scope) {
    $scope.item = {dir: 'hello'}; // the name of the directive, the idea is to use it for referring to many future directives.
  });
</script>
</body>
</html>

Here's a plunker of the code: http://plnkr.co/edit/tM73sY3vTfPFHmn6iCYE?p=preview

So, what I am missing? How do I achieve to get Angular using string interpolation when using a directive? Thanks!

Upvotes: 3

Views: 190

Answers (2)

aizquier
aizquier

Reputation: 567

Apparently, the tg-dynamic-directive does the trick.

Upvotes: 0

Bernard
Bernard

Reputation: 281

For directives to work, Angular needs to compile your html (something automatically done when a page is loaded).

Having a way to control freely which directive to instantiate is a bit like pulling the rug under your feet and is atypical. One of the issue is that the compilation "destroy" the internal binding/watchers data and some of the original DOM and thus there is not enough information to "recompile" a DOM node.

NOTE: you cannot change attribute or element names (only attribute values) with angular using this type of binding: {{ }} But ng-class="..." and class="{{...}}" works.

I do not understand what you are trying to achieve exactly. If the intention is really about modifying the item.dir's value and having Angular "reconfigure" your application, it is "possible" but I highly suspect it will induce "state" drawbacks.

Nevertheless, here's a working "hack" that "remembers" the original DOM html and recompiles it when needed. This is done in 2 compilation phases: First phase is to restore the original bindings and a second phase that runs after the $digest cycle so the original bindings finished populating the class name from the scope (i.e. to have item.dir in effect). The drawback of course is if you made modifications to the enclosing DOM, this will wipe them! Alternatively, it might be possible to remember only specific attributes and revert "that" only while keeping other portion of the DOM intact (but might create other issues).

  appModule.directive('forceRecompilation', ['$timeout', '$compile', function($timeout, $compile) {
    return {
      restrict: 'A',
      link: function(scope, element, attr) {

        var originalHtml = element.html();

        scope.$watch(attr.forceRecompilation, function(){
            // restore original HTML and compile that
            element.html(originalHtml);
            $compile(element.contents())(scope);

            // wait for all digest cycles to be finished to allow for "binding" to occur
            $timeout(function(){
              // recompile with bounded values
              $compile(element.contents())(scope);
            });
          });
      }
    };
  }]);

...to be used as an enclosing tag of the section of the DOM to be acted upon. It will "revert & recompile" everything under it when the expression changes. (here "item.dir"):

<div force-recompilation="item.dir">
    <div class="{{item.dir}}">
</div>

Plunker: http://plnkr.co/edit/TcMhzFpErncbHSG6GgZp?p=preview

In the plunker, there are 2 directives "hello" and "hello2". Change the text to "hello" and back to "hello2" to see the effect.

EDIT: The following is a directive that allows inserting markup to be compiled as described in a comment below. This is just a slightly modified version of Angularjs - inline directives with ng-bind-html-unsafe

  angular.module('bindHtmlExample', ['ngSanitize'])
    .controller('ExampleController', ['$scope',
      function($scope) {

        $scope.test = false;
        $scope.dir = "ng-click";
        $scope.clicked = function() {
          $scope.test = !$scope.test
        }

        $scope.myHTML =
          'I am an <b ng-show="test">invisible</b> HTML string with ' +
          '<a href="#" ' + $scope.dir + '="clicked()">links!</a> and other <em>stuff</em>';
      }
    ])

  // modified plunker taken from https://stackoverflow.com/questions/18063280/angularjs-inline-directives-with-ng-bind-html-unsafe
  //
  // Allows an attribute's value to be evaluated and compiled against the scope, resulting
  // in an angularized template being injected in its place.
  //
  // Note: This directive is prefixed with "unsafe" because it does not sanitize the HTML. It is up
  // to the developer to ensure that the HTML is safe to insert into the DOM.
  //
  // Usage:
  //     HTML: <div unsafe-bind-html="templateHtml"></div>
  //     JS: $scope.templateHtml = '<a ng-onclick="doSomething()">Click me!</a>';
  //     Result: DIV will contain an anchor that will call $scope.doSomething() when clicked.
  .directive('unsafeBindHtml', ['$compile',
    function($compile) {
      return function(scope, element, attrs) {
        scope.$watch(
          function(scope) {
            // watch the 'compile' expression for changes
            return scope.$eval(attrs.unsafeBindHtml);
          },
          function(value) {
            // when the 'compile' expression changes
            // assign it into the current DOM element
            element.html(value);

            // compile the new DOM and link it to the current
            // scope.
            // NOTE: we only compile .childNodes so that
            // we don't get into infinite loop compiling ourselves
            $compile(element.contents())(scope);
          }
        );
      };
    }
  ]);
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Example</title>


  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.5/angular.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.5/angular-sanitize.js"></script>
  <script src="script.js"></script>



</head>

<body ng-app="bindHtmlExample">
  <div ng-controller="ExampleController">
    <p unsafe-bind-html="myHTML"></p>

    (click on the link to see <code>ng-click</code> in action)
  </div>
</body>

</html>

Upvotes: 2

Related Questions