Search code examples
angularjsng-class

Sharing controller for multiple directives not working


Plunker Code

I am having an issue getting the ng-class to work in the flipCard directive's templates. Technically it does work, if I set the property immediate in the controller it will add the css class.

What it looks like to me in that children directives may be getting an independent copy of the controller instead of sharing the base flipCard directive's controller. So when I call flipCtrl.move it is calling the flipCardOver directive's instance of the controller as setting the property on the controller in the flipCtrl.move function is not updating the base parents flipped property, though console logging it says it is set to true.

Goal: Have a controller on flipCard that is shared with flipCardBack, flipCardFront, flipCardOver, flipCardReset While also allowing multiple flipCard directives on a page and not having conflicts.

What am I missing?

angular.module('cardFlip', [])
  .controller('flipCardController', ['$scope', '$element', '$timeout', '$window',
    function($scope, $element, $timeout, $window) {
      var vm = this;

      vm.flipped = false;
      vm.moved = false;

      vm.originalTop = -1;
      vm.originalLeft = -1;


      vm.move = function(e) {

        vm.flipped = true;
        console.log(vm.flipped);
      };


      vm.reset = function() {
        vm.flipped = false;
      };
    }
  ])
  .directive('flipCard', function() {

    return {
      restrict: 'AE',
      controller: 'flipCardController',
      controllerAs: 'flipCtrl',
      scope: true,
      transclude: true,

      // LOOK: this is the template that I am expecting to change
      template: '<div class="container"><div class="panel" ng-class="{ flip: flipCtrl.flipped, slide: flipCtrl.moved }" ng-transclude></div></div>'
    }

  })
  .directive('flipCardFront', function() {
    return {
      restrict: 'AE',
      require: '^flipCard',
      transclude: true,
      template: '<div class="front" ng-transclude></div>'
    }
  })
  .directive('flipCardBack', function() {
    return {
      restrict: 'AE',
      require: '^flipCard',
      transclude: true,
      template: '<div class="back" ng-transclude></div>'
    }
  })
  .directive('flipCardOver', function() {

    return {
      restrict: 'AE',
      require: '^flipCard',
      link: function(scope, element, attribs, flipCtrl) {


        // LOOK: this is the event I am expecting to call move that sets flipped to true
        element.on('click', flipCtrl.move);
      }
    }
  })
  .directive('flipCardReset', function() {
    return {
      restrict: 'AE',
      require: '^flipCard',
      link: function(scope, element, attribs, flipCtrl) {

        element.on('click', flipCtrl.reset);

      }
    }
  });
/* Styles go here */

[ng-click] {
  cursor: pointer;
}
.panel .pad {
  padding: 0 15px;
}
.panel.flip .action {
  display: none;
}
.panel {
  float: left;
  width: 200px;
  height: 200px;
  margin: 20px;
  position: relative;
  font-size: .8em;
  -webkit-perspective: 600px;
  perspective: 600px;
}
/* -- make sure to declare a default for every property that you want animated -- */

/* -- general styles, including Y axis rotation -- */

.panel .front {
  float: none;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 900;
  width: inherit;
  height: inherit;
  background: #6b7077;
  -webkit-transform: rotateX(0) rotateY(0);
  transform: rotateX(0) rotateY(0);
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* -- transition is the magic sauce for animation -- */
  -webkit-transition: all .4s ease-in-out;
  transition: all .4s ease-in-out;
}
.panel.flip .front {
  z-index: 900;
  -webkit-transform: rotateY(179deg);
  transform: rotateY(179deg);
}
.panel.slide {
  top: 15%;
  left: 15%;
  -webkit-transition: all 5s ease-in-out;
  transition: all 5s ease-in-out;
}
.panel .back {
  float: none;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 800;
  width: inherit;
  height: inherit;
  box-shadow: 0 2px 15px rgba(0, 0, 0, 0.2);
  -webkit-transform: rotateY(-179deg);
  transform: rotateY(-179deg);
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* -- transition is the magic sauce for animation -- */
  -webkit-transition: all .4s ease-in-out;
  transition: all .4s ease-in-out;
}
.panel.flip .back {
  z-index: 1000;
  -webkit-transform: rotateX(0) rotateY(0);
  transform: rotateX(0) rotateY(0);
}
<!DOCTYPE html>
<html>

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.js"></script>
</head>

<body>
  <div ng-app="cardFlip">
    <div ui-view="main">
      <flip-card>
        <flip-card-front>
          <div style="width:50px;height:50px;" flip-card-over="function(){return true;}">
            FRONT1
          </div>
        </flip-card-front>
        <flip-card-back>
          <div style="width:100px;height:100px;" flip-card-reset="function(){return true;}">
            BACK1
          </div>
        </flip-card-back>
      </flip-card>
    </div>
  </div>
</body>

</html>


Solution

  • At first I was stumped too, but then I realised - you haven't told Angular about the change using scope.$evalAsync() (safer than doing $apply() or $digest() in case there is already a digest happening). This is because you are handling the click yourself, rather than using ng-click (which triggers a digest, so doesn't have this problem).

    Here is a forked Plunkr with it working.

    angular.module('cardFlip', [])
      .controller('flipCardController', ['$scope', '$element', '$timeout', '$window',
        function($scope, $element, $timeout, $window) {
          var vm = this;
    
          vm.flipped = false;
          vm.moved = false;
    
          vm.originalTop = -1;
          vm.originalLeft = -1;
    
    
          vm.move = function(e) {
    
            vm.flipped = true;
            console.log(vm.flipped);
          };
    
    
          vm.reset = function() {
            vm.flipped = false;
          };
        }
      ])
      .directive('flipCard', function() {
    
        return {
          restrict: 'AE',
          controller: 'flipCardController',
          controllerAs: 'flipCtrl',
          scope: true,
          transclude: true,
    
          // LOOK: this is the template that I am expecting to change
          template: '<div class="container"><div class="panel" ng-class="{ flip: flipCtrl.flipped, slide: flipCtrl.moved }" ng-transclude></div></div>'
        }
    
      })
      .directive('flipCardFront', function() {
        return {
          restrict: 'AE',
          require: '^flipCard',
          transclude: true,
          template: '<div class="front" ng-transclude></div>'
        }
      })
      .directive('flipCardBack', function() {
        return {
          restrict: 'AE',
          require: '^flipCard',
          transclude: true,
          template: '<div class="back" ng-transclude></div>'
        }
      })
      .directive('flipCardOver', function() {
    
        return {
          restrict: 'AE',
          require: '^flipCard',
          link: function(scope, element, attribs, flipCtrl) {
    
    
            // LOOK: this is the event I am expecting to call move that sets flipped to true
            element.on('click', function () {
                flipCtrl.move();
                scope.$evalAsync(); // tell Angular we did something
            });
          }
        }
      })
      .directive('flipCardReset', function() {
        return {
          restrict: 'AE',
          require: '^flipCard',
          link: function(scope, element, attribs, flipCtrl) {
    
            element.on('click', function () {
                flipCtrl.reset();
                scope.$evalAsync(); // tell Angular
            });
          }
        }
      });
    /* Styles go here */
    
    [ng-click] {
      cursor: pointer;
    }
    .panel .pad {
      padding: 0 15px;
    }
    .panel.flip .action {
      display: none;
    }
    .panel {
      float: left;
      width: 200px;
      height: 200px;
      margin: 20px;
      position: relative;
      font-size: .8em;
      -webkit-perspective: 600px;
      perspective: 600px;
    }
    /* -- make sure to declare a default for every property that you want animated -- */
    
    /* -- general styles, including Y axis rotation -- */
    
    .panel .front {
      float: none;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 900;
      width: inherit;
      height: inherit;
      background: #6b7077;
      -webkit-transform: rotateX(0) rotateY(0);
      transform: rotateX(0) rotateY(0);
      -webkit-transform-style: preserve-3d;
      transform-style: preserve-3d;
      -webkit-backface-visibility: hidden;
      backface-visibility: hidden;
      /* -- transition is the magic sauce for animation -- */
      -webkit-transition: all .4s ease-in-out;
      transition: all .4s ease-in-out;
    }
    .panel.flip .front {
      z-index: 900;
      -webkit-transform: rotateY(179deg);
      transform: rotateY(179deg);
    }
    .panel.slide {
      top: 15%;
      left: 15%;
      -webkit-transition: all 5s ease-in-out;
      transition: all 5s ease-in-out;
    }
    .panel .back {
      float: none;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 800;
      width: inherit;
      height: inherit;
      box-shadow: 0 2px 15px rgba(0, 0, 0, 0.2);
      -webkit-transform: rotateY(-179deg);
      transform: rotateY(-179deg);
      -webkit-transform-style: preserve-3d;
      transform-style: preserve-3d;
      -webkit-backface-visibility: hidden;
      backface-visibility: hidden;
      /* -- transition is the magic sauce for animation -- */
      -webkit-transition: all .4s ease-in-out;
      transition: all .4s ease-in-out;
    }
    .panel.flip .back {
      z-index: 1000;
      -webkit-transform: rotateX(0) rotateY(0);
      transform: rotateX(0) rotateY(0);
    }
    <!DOCTYPE html>
    <html>
    
    <head>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.js"></script>
    </head>
    
    <body>
      <div ng-app="cardFlip">
        <div ui-view="main">
          <flip-card>
            <flip-card-front>
              <div style="width:50px;height:50px;" flip-card-over="function(){return true;}">
                FRONT1
              </div>
            </flip-card-front>
            <flip-card-back>
              <div style="width:100px;height:100px;" flip-card-reset="function(){return true;}">
                BACK1
              </div>
            </flip-card-back>
          </flip-card>
        </div>
      </div>
    </body>
    
    </html>