Jacopo
Jacopo

Reputation: 1021

AngularJS ngIf prevents finding element inside directive

I have an AngularJS directive that includes an ngIf and I would like to modify some of the DOM inside the ngIf in the directive link function. Unfortunately it seems that ngIf prevents me from finding DOM elements within it in the link function.

Here is the code for the directive:

directive('column', function () {
    return {
      templateUrl: 'views/column.html',
      restrict: 'E',
      scope: {
        column: '='
      },
      controller: ['$scope', function ($scope) {

        $scope.editing = true;
        $scope.toggleEditing = function () {
          $scope.editing = !$scope.editing;
        };

      }],
      link: function postLink(scope, element) {
        var select = element.find('select');
        console.log(select); // See if it can find the select element
        // var types = scope.column.types();
        // add types as options to the select element
      }
    };
  });

And here is the simplified html of the directive:

<div class="column">
    <div>{{ column.title }}</div>
    <form name="columnForm" role="form" ng-if="editing">
        <select></select>
    </form>
</div>

Here is the link to the jsFiddle example http://jsfiddle.net/dedalusj/Y49Xx/1/

The element.find call in the link function returns an empty array but as soon as I remove the ngIf from the form it returns the proper select DOM element. I have the feeling that I'm doing this the wrong way.

UPDATE

Thanks for the answers but I found another solution. I simply created another directive that encapsulate the form, added it to the column directive template with ng-if="editing".

The form directive doesn't have it's own scope so it effectively operates out of the column directive scope and has always access to the select element because it's inside its DOM tree. I pay the cost of an extra directive but I don't have to use the $timeout hack. I created a new jsFiddle to illustrate the solution http://jsfiddle.net/dedalusj/nx3vX/1/

Thanks @Michael but I can't simply use the ng-option because the types array comes from an XML file and its elements are other angular.element objects which cannot be inserted easily with ng-option.

Upvotes: 23

Views: 11261

Answers (4)

Marcosocon
Marcosocon

Reputation: 13

I was facing this same issue and i was able to resolve it using ng-show, this prevents this issue because ngIf removes the element it's applied to the DOM, so you won't be able to find it when it's not there.

so in your case:

<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-show="editing">
    <select></select>
</form>

will work OK.

Cheers.

Upvotes: 1

Robert Knight
Robert Knight

Reputation: 2927

The ngIf directive works by using Angular's transclusion feature. What happens during the compile/link cycle is:

  1. The content inside the ngIf is removed from the DOM when it is compiled
  2. Angular runs the link functions. The ngIf's link function is run before the link function of the directive using it. When ngIf's link function runs, it uses $scope.$watch() to watch the value of the ng-if attribute.
  3. Your directive's link function runs, at this point the content of the ngIf is not part of the DOM
  4. The watch set up in step (2) is called, and ngIf will then call the $transclude function to insert the contents of the ngIf into the DOM if the ng-if attribute value is truthy.
  5. Any watch functions, $timeout calls or use of $scope.$evalAsync that you registered in your directive's link function will run.

So if you want to access elements inside the ngIf's content, the code needs to run after step 4 above. This means that any functions registered with $scope.$watch, $timeout or $scope.$evalAsync in your directive's link function will work. For a one-time piece of setup code, I would probably opt for $scope.$evalAsync:

angular.directive('yourDirective', function () {
  return {
    ...
    link: function(scope, elem) {
      scope.$evalAsync(function () {
        // code that runs after conditional content
        // with ng-if has been added to DOM, if the ng-if
        // is enabled
      });
    }
  };
});

Upvotes: 12

Arthur Shpakov
Arthur Shpakov

Reputation: 23

You can put your code from the link function inside $timeout.

$timeout(function(){
     var select = element.find('select');
     console.log(select);
});

Don't forget to inject $timeout in your directive

directive('column', function ($timeout) {

Upvotes: 2

Michael Benford
Michael Benford

Reputation: 14104

As @moderndegree has said, ngIf removes the element it's applied to from the DOM, so you won't be able to find it when it's not there. But, you could write your directive in a way to workaround that:

controller: function ($scope, $element, $timeout) {
  $scope.toggleEditing = function () {
    $scope.editing = !$scope.editing;
    $timeout(function() {
      var select = $element.find('select');
      select.append('<option>Value 1</option>')
            .append('<option>Value 2</option>')
            .append('<option>Value 3</option>');
    });            
  };
}

Updated jsFiddle here.

The trick here is to delay the find() call by using $timeout with a 0 interval in order to wait for Angular to update the DOM.

UPDATE

After giving some more thought to your code, I realize that perhaps you can let Angular do the hard work for you:

Javascript

directive('column', function () {
  return {
    templateUrl: 'views/column.html',
    restrict: 'E',
    scope: {
      column: '='
    },
    controller: ['$scope', function ($scope) {
      $scope.editing = true;
      $scope.toggleEditing = function () {
        $scope.editing = !$scope.editing;
      };
    }],
  };
});

HTML

<div class="column">
  <div>{{ column.title }}</div>
  <form name="columnForm" role="form" ng-if="editing">
    <select ng-model="type" ng-options="type for type in column.types"></select>
  </form>
</div>

jsFiddle

Now you don't need to worry about finding the select element at the right time and populating it. Angular does all of that for you. :)

Upvotes: 2

Related Questions