Angular Directive - can I have an isolate scope inherit from parent?

434 Views Asked by At

I am trying to edit an angular directive written by somebody else and I am coming across an issue. It is an attribute directive and I want to watch the value passed in as the attribute's value for changes.

This works when I give it an isolate scope and watch the property using scope.$watch. However using an isolate scope breaks the directive as it had the scope set to true originally which it needs for other things (as I understand it means it inherits the parent scope).

So my question is, how can I inherit the parent scope, but also watch the attribute's value?

To help clarify, here is a basic example of the directive:

return {
            scope: {
                myGridsterItem: "=gridsterItem"
            },
            restrict: 'EA',
            controller: 'GridsterItemCtrl',
            controllerAs: 'gridsterItem',
            require: ['^gridster', 'gridsterItem'],
            link: function(scope, $el, attrs, controllers) {

                .....

                // need to dynamically update draggable/resizable of individuaol gridster item if properties change
                scope.$watch("myGridsterItem.draggable", function(newVal) {
                    draggable.updateDraggable(newVal);
                }, true);
                scope.$watch("myGridsterItem.resizable", function(newVal) {
                    resizable.updateResizable(newVal);
                }, true);

                .....
            }
    }

This throws an error due to it not inheriting the parent scope. And if I set

scope: true

My error is solved, but the watch handlers never run.

A rough outline of the HTML:

<div gridster="gridsterOpts">
    <ul>
        <li gridster-item="getPrefs(portlet.id)", ng-repeat="portlet in getPortlets()">
            .....
        </li>
    </ul>
</div>

Please help me, thanks!

1

There are 1 best solutions below

5
On

Updated answer

I've had more of a play and demonstrated one possible solution in a code snippet below.

Without using an isolated scope (as this caused other issues for you), basically what we need is for getPrefs(portlet.id) to get interpolated or parsed when your directive's link function tries to assign it to scope. Without doing this, if you output the value of attrs.gridsterItem to console you'll see it's getPrefs(portlet.id), and not the result of calling that function.

The cleanest-looking solution I came across was to use $parse. Within your link function, it allows us to evaluate the function contained in the attribute at the time we're assigning it to scope. This way it's the result of that function that gets assigned, which allows the watcher functions to fire correctly.

Note that $parse has to be added as a dependency to the directive declaration and passed as a parameter.

(function() {
  "use strict";

  var myApp = angular.module("myApp", [])
    .directive("gridsterItem", ["$parse", GridsterItemDirective]); // Make sure to DI '$parse'

  function GridsterItemDirective($parse) {
    return {
      restrict: 'EA',
      controller: 'GridsterItemCtrl',
      controllerAs: 'gridsterItem',
      require: ['^gridster', 'gridsterItem'],
      link: function(scope, $el, attrs, controllers) {
        scope.output = ""; //for demo'ing
        scope.myGridsterItem = $parse(attrs.gridsterItem)(scope); //evaluating the value of attrs.gridsterItem in the context of 'scope'

        // need to dynamically update draggable/resizable of individuaol gridster item if properties change
        scope.$watch("myGridsterItem.draggable", function(newVal) {
          console.log("myGridsterItem.draggable: ", newVal);
          scope.output += "Draggable updated (" + newVal + ")... ";
        }, true);
        scope.$watch("myGridsterItem.resizable", function(newVal) {
          console.log("myGridsterItem.resizable: ", newVal);
          scope.output += "Resizable updated (" + newVal + ")... ";
        }, true);
      }
    };
  }

  //-------------------------------
  //Supporting mockups...
  myApp.controller("myController", ["$scope", MyController]) //supporting mocks
    .directive("gridster", [GridsterDirective])
    .controller("GridsterCtrl", ["$scope", GridsterCtrl])
    .controller("GridsterItemCtrl", ["$scope", GridsterItemCtrl]);

  function MyController($scope) {
    var _portlets = [{
      id: "1"
    }, {
      id: "2"
    }, {
      id: "3"
    }];

    $scope.getPortlets = function getPortlets() {
      return _portlets;
    };

    var _prefs = {
      "1": {
        draggable: true,
        resizable: true
      },
      "2": {
        draggable: true,
        resizable: true
      },
      "3": {
        draggable: true,
        resizable: true
      },
    };
    $scope.getPrefs = function getPrefs(id) {
      return _prefs[id];
    }
    $scope.setPrefs = function setPrefs(id, prefName, value) {
      _prefs[id][prefName] = value;
    }
  }

  function GridsterDirective() {
    return {
      restrict: 'EA',
      controller: 'GridsterCtrl',
      controllerAs: 'gridster'
    };
  }

  function GridsterCtrl($scope) {
    //mock
  }

  function GridsterItemCtrl($scope) {
    //mock
  }
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<style type="text/css">
  li {
    border: 1px solid #ccc;
    padding: 10px;
    margin: 10px;
    font: 0.8em Arial, sans-serif;
  }
</style>
<div ng-app="myApp">
  <div ng-controller="myController">
    <div gridster>
      <ul>
        <li gridster-item="getPrefs(portlet.id)" ng-repeat="portlet in getPortlets()">
          <p><strong>portlet.id:</strong> {{portlet.id}}</p>
          <p>{{getPrefs(portlet.id)}}</p>
          <p><strong>Output:</strong> {{output}}</p>
          <button ng-click="setPrefs(portlet.id, 'draggable', !getPrefs(portlet.id).draggable)">Toggle Draggable</button>
          <button ng-click="setPrefs(portlet.id, 'resizable', !getPrefs(portlet.id).resizable)">Toggle Resizable</button>
        </li>
      </ul>
    </div>
  </div>
</div>

Old Answer:

Within your link function (with non-isolated scope), try this:

scope.myGridsterItem = attrs.gridsterItem;

attrs is a param that has been automatically provided to your link function, and should contain within it a collection of the attributes on the element your directive has been used upon. Get the attribute you need, and assign it to a scope variable that matches the relevant ones in the $watch expressions (myGridsterItem in this case, it looks like).

Remember that angular's naming conventions mean that an attribute called this-is-an-attribute in the HTML will be accessible as thisIsAnAttribute (snake- to camel-case conversion) in the javascript.