Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 912
Jacopo Avatar asked Feb 04 '14 01:02

Jacopo


People also ask

Does ngIf remove element from DOM?

The ngIf directive removes or recreates a portion of the DOM tree based on an {expression}. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.

Does ngIf work on Div?

The Angular ngIf directive works essentially as an if statement for HTML, adding this missing feature to the language under the form of the special ngIf attribute. In this example, the container div will only be shown to the user if the user is logged in, otherwise the whole content of the div is not visible.

What happens when component is made visible again using ngIf?

Your div will be rendered and visible once the change detection is triggered. When a change is detected, the whole lifecycle is ran again. If you want to run something, you should hook on one of the events of the lifecycle.

Is ngIf a directive?

NgIflink. A structural directive that conditionally includes a template based on the value of an expression coerced to Boolean.


4 Answers

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
      });
    }
  };
});
like image 167
Robert Knight Avatar answered Sep 30 '22 07:09

Robert Knight


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. :)

like image 26
Michael Benford Avatar answered Sep 29 '22 07:09

Michael Benford


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) {
like image 41
Arthur Shpakov Avatar answered Sep 28 '22 07:09

Arthur Shpakov


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.

like image 41
Marcosocon Avatar answered Sep 27 '22 07:09

Marcosocon