Search code examples
javascriptangularjsgridster

ng-show method not executing inside custom, $compile'd directive


I inherited this codeset and I think I understand what's happening but I cannot seem to get ng-show within a custom, $compile'd directive to work. I suspect it may have something to do with gridster, but I've tried the line you'll see commented out to the same effect.

Here is a very simplified version since I cannot make the code I'm working on public. I'm not really making a dinosaur app, although that might be nice.

I cannot use templateUrl for this because we need to add each result to gridster. There's much more going on. Just trust that I can't :(

app.directive("tarpit", ["$compile", "gridster", function($compile, gridster) {
    return {
        scope: {
            dinosaurs: "="
        },
        restrict: "A",
        link: function(scope, element, attrs) {
            var i, dino;

            scope.$watch("dinosaurs", function(dinosaurs, oldDinosaurs) {
               for(i = 0; i < dinosaurs.length; i++) {
                   // scope.dinosaurs is set from a provider higher up our scope chain.  each dinosaur contains HTML
                  dino = angular.element(dinosaurs[i].html);
                  dino.attr("dinosaur-id", dinosaurs[i].id);

                  // i suspect these are the problem areas
                  dino = $compile(dino)(scope);
                  gridster.add(dino, dinosaurs[i].x, dinosaurs[i].y, dinosaurs[i].col, dinosaurs[i].row);

                  // i've also tried the following which didnt work
                  // element.append(dino);
               }
            }
        }
    };
}]);

app.directive("dinosaur", function() {
   return {
      scope: {
          dinosaurs: "="
      },
      restrict: "A",
      link: function(scope, element, attrs) {
          scope.isExtinct = function() {
              // there's more to it than this but you get the idea
              for(var i = 0; i < scope.dinosaurs.length; i++) {
                  if(scope.dinosaurs[i].id == attrs.dinosaurId) {
                     return true;
                  }
              }
              return false;
          };
      }
   };
});

The page starts as:

<div tarpit dinosaurs="dinosaurs"></div>

When the dinosaur web service finishes and we hit the $watch, the page will eventually become:

<div tarpit dinosaurs="dinosaurs">
     <div dinosaur dinosaurs="dinosaurs" dinosaur-id="111">
         <div ng-show="isExtinct()">I'm dead</div>
         <div ng-show="!isExtinct()">It's a miracle</div>
     </div>
     ...
     <div dinosaur dinosaurs="dinosaurs" dinosaur-id="999">
         <div ng-show="isExtinct()">I'm dead</div>
         <div ng-show="!isExtinct()">It's a miracle</div>
     </div>
</div>

The problem is that the isExtinct method never gets invoked.

I have tried the following, with the corresponding results:

  1. Moved isExtinct in to 'tarpit' - it gets executed, but I lose the context of which dinosaur I am. I tried passing $element in to it but it remains undefined.
  2. ng-show="$parent.isExtinct()" - it does not get invoked.
  3. Using a flag instead of a method. Same results as above.
  4. Putting isExtinct in a controller() inside of the tarpit directive. Same result as #1. The method is invoked, but without knowledge of which dinosaur invoked it.

I'd settle for #1 at this point as long as I could maintain an idea of which dinosaur I am, such as passing $element to the parent method, or even just the attr.dinosaurId. I cannot rely on the order that they are appended mapping to the array of dinosaurs, because gridster will rearrange them.


Solution

  • I figured this out. The origin of the problem was on the line:

                  dino = $compile(dino)(scope);
    

    Here, we are creating the dinosaur directive and we are doing so with the tarpit scope. When we later did

                  scope.isExtinct = function() { ... }
    

    'scope' here was within the dinosaur directive but for some reason, isExtinct was not accessible within the ng-show, I assume because ng-show lived on the Tarpit scope but isExtinct lived on the Dinosaur scope. That's just a guess. It seems that that means we were saying tarpit.ngShow = tarpit.isExtinct. tarpit.isExtinct doesnt exist (but there's no error indicating such) and so it's undefined and ng-show="undefined" is valid, so it doesnt yell at us. This is why creating isExtinct on $parent solved the problem, because now tarpit.isExtinct does exist. It also means that isolating the scope less strictly will allow the normal inheritance to figure this out for us.

    Given that, I found two solutions:

                 scope.$parent.isExtinct = function() { ... }
    

    and

                 scope: true
    

    Since any data I need is passed in as an attribute, I was able to consume everything I needed through attrs instead of a scope value. I wound up just using

                 scope: true