Michael
Michael

Reputation: 715

Angular - getting ng-bind to work on nested directives

I've built a small pattern that recursively creates different sub-directives based on the model. I'm using $compile to build child directives recursively, then append them to the parent.
The directive building itself seems to work just fine, but for some reason, embedded expressions or ng-bind or interpolation doesn't seem to work on nested directive.

Here's a snippet:

app.directive("child", function ($compile) {
  function getTemplate(depth) {
    if (depth % 2 == 0) {
      return "<even depth='deeper'/>"
    } else {
      return "<odd depth='deeper'/>"
    }
  }
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element) {
      if ($scope.depth == 0) {
        var child = angular.element("<span ng-bind='depth'/>");
        child = $compile(child)($scope);
        $element.append(child);
      } else {
        $scope.deeper = $scope.depth - 1;
        var child = angular.element(getTemplate($scope.depth));
        child = $compile(child)($scope);
        $element.append(child);
      }
    }
  }
})

Basically in this test the directive will recursively dive down until the depth reaches 0, then spit out a <span> element.
The expected result should be a span element with the value of 0. But it doesn't seem to evaluate. Using <span>{{depth}}</span> also results in in a literal html instead of evaluating the contents.
I'm trying to achieve a result of nested <even><odd><even> directives removing the surrounding <child> - directive.

Here's a complete jsFiddle: https://jsfiddle.net/eg1e1aLz/

The resulting DOM should look like this:

<test depth="4" class="ng-isolate-scope">
  <even depth="depth-1" class="ng-scope ng-isolate-scope">
    <odd depth="depth-1" class="ng-scope ng-isolate-scope">
      <even depth="depth-1" class="ng-scope ng-isolate-scope">
        <odd depth="depth-1" class="ng-scope ng-isolate-scope"><span ng-bind="depth" class="ng-binding ng-scope">0</span></odd>
      </even>
    </odd>
  </even>
</test>

Upvotes: 3

Views: 668

Answers (6)

Mathew Berg
Mathew Berg

Reputation: 28750

Sorry if I'm missing the context of what you're trying to accomplish, but I don't believe you need to use sub directives or anything like that. You can just use the test directive and continuously build out the children and then bind the scope to the last one:

angular.module("app", [])
    .directive("test", function ($compile) {
        return {
            scope: {
                depth: "="
            },
            link: function linker($scope, $element, $attrs) {
                var element = $element;
                for (var x = 0; x < $scope.depth; x++) {
                    var elementType = x % 2 === 0 ? 'even' : 'odd';
                    var subElement = angular.element(document.createElement(elementType));
                    element.append(subElement);
                    element = subElement;
                }
                var span = angular.element('<span ng-bind="depth" />');
                element.append(span);
                $compile(span)($scope);
            }
        }
    })

Fiddle: https://jsfiddle.net/ffyv0zmy/

I realise in your example you want the output to be "0" for the depth but you can hard code that as I'm not sure as to it's purpose.

Resulting HTML:

<test depth="4" class="ng-isolate-scope">
    <even>
        <odd>
            <even>
                <odd>
                    <span ng-bind="depth" class="ng-binding ng-scope">4</span>
                </odd>
            </even>
        </odd>
    </even>
</test>

The nice thing about this is you don't have to attach scope to the even/odd elements unless you want too.

You can still wire up directives to each of the odd/even elements but then you'll have to $compile the $element instead to attach all the scopes.

Upvotes: 0

maurycy
maurycy

Reputation: 8465

Your jsfiddle is perfectly fine, the only problem is a small bit of logic.

When you compile the child directive you shouldn't append child.html() simply because that will append an HTML without bindings and watchers connected to them. Instead, you should append the whole child

https://jsfiddle.net/eg1e1aLz/2/

child = $compile(child)($scope);
$element.append(child);

Upvotes: 0

K Scandrett
K Scandrett

Reputation: 16540

Based on your comments below and the update to the question, how about the following...

It uses the template function getTemplate to build the structure, and identical directives: odd and even as placeholders for building the functionality.

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

app.directive("test", function($compile) {
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element, $attrs) {

      // template accessible to the child directives
      $scope.getTemplate = function(depth) {
        if (depth <= 0) {
          return "<span ng-bind='depth'/>"; // also bindings like {{depth}} work
        } else if (depth % 2 === 0) {
          return "<even depth='depth-1'></even>"; // bindings like {{depth}} work
        } else {
          return "<odd depth='depth-1'></odd>";
        }
      }

      var child = angular.element($scope.getTemplate($scope.depth));
      $compile(child)($scope);
      $element.append(child);
    }
  }
});
app.directive("odd", function($compile) {
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element) {
      $scope.getTemplate = $scope.$parent.getTemplate; // bring template into current scope
      var child = angular.element($scope.getTemplate($scope.depth));
      $compile(child)($scope);
      $element.append(child);
    }
  }
})
app.directive("even", function($compile) {
  return {
    scope: {
      depth: "=",
    },
    link: function linker($scope, $element) {
      $scope.getTemplate = $scope.$parent.getTemplate; // bring template into current scope
      var child = angular.element($scope.getTemplate($scope.depth));
      $compile(child)($scope);
      $element.append(child);
    }
  }
})

var controller = app.controller("controller", function($scope) {});

Updated Fiddle: https://jsfiddle.net/bda411fj/15/

Result:

<test depth="4" class="ng-isolate-scope">
  <even depth="depth-1" class="ng-binding ng-scope ng-isolate-scope">
    <odd depth="depth-1" class="ng-binding ng-scope ng-isolate-scope">
      <even depth="depth-1" class="ng-binding ng-scope ng-isolate-scope">
        <odd depth="depth-1" class="ng-binding ng-scope ng-isolate-scope">
          <span ng-bind="depth" class="ng-binding ng-scope">0</span>
        </odd>
      </even>
    </odd>
  </even>
</test>

Upvotes: 3

Sagar V
Sagar V

Reputation: 12478

Since you don't want to the html(), don't use child.html().

Use the child itself.

Check the documentation here

Check the snippet below. Added based on the sample snippet in your question

var app = angular.module("app", []);
app.directive("test", function($compile) {
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element, $attrs) {
      var child = angular.element("<child depth='depth'/>");
      child = $compile(child)($scope);
      $element.append(child);
    }
  }
});
app.directive("child", function($compile) {
  function getTemplate(depth) {
      return depth % 2 == 0 ? "<even depth='depth-1'/>" : "<odd depth='depth-1'/>"
  }
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element) {
      if ($scope.depth == 0) {
        var child = angular.element("<span ng-bind='depth'/>");
        child = $compile(child)($scope);
        $element.append(child);
      } else {
        var child = angular.element(getTemplate($scope.depth));
        child = $compile(child)($scope);
        $element.append(child);
      }
    }
  }
})
app.directive("odd", function($compile) {
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element) {
      var child = angular.element("<child depth='depth'/>");
      child = $compile(child)($scope);
      $element.append(child);
    }
  }
})
app.directive("even", function($compile) {
  return {
    scope: {
      depth: "="
    },
    link: function linker($scope, $element) {
      var child = angular.element("<child depth='depth'/>");
      child = $compile(child)($scope);
      $element.append(child);
    }
  }
})
var controller = app.controller("controller", function($scope) {});
body {
  padding: 5px 5px 5px 5px !important;
  font-size: 30px;
}

test,
child,
even,
odd {
  padding: 5px 5px 5px 5px;
  margin: 5px 5px 5px 5px;
  border: 1px solid black;
}

test {
  background-color: aliceblue !important;
}

child {
  background-color: beige !important;
}

even {
  background-color: lightgreen !important;
}

odd {
  background-color: lightcoral !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js"></script>
<body ng-app="app" ng-controller="controller">
  <test depth="4"></test>
</body>

Upvotes: 0

Yogesh Kumar
Yogesh Kumar

Reputation: 11

The compiled part of your template need to refer as child only instead on child.html().
Change your code at 3 places from $element.append(child.html()); to $element.append(child);

This will start printing the depth value you are looking for. Is there anything else you are looking for?

Upvotes: 1

Vikash Kumar
Vikash Kumar

Reputation: 1718

Your html might not be sanitized that's why it is not compiling try using ngSanatize ti might solve your problem https://docs.angularjs.org/api/ngSanitize/service/$sanitize it will remove all the potentially dangerous tokens and will return you a proper html.

Upvotes: 0

Related Questions