Search code examples
javascriptangularjssvgangularjs-directiveangularjs-scope

Accessing $scope From a Directive in AngularJS


Due to our lack of expertise in developing with AngularJS, we've come to another roadblock in our development process.

We are developing a Angular/Web API application where our page only consists of an interactive SVG diagram that displays data when a user hovers over a particular SVG tag in an Angular directive.

There are currently two custom directives in the application.

  1. Directive One - Loads SVG file into web page
  2. Directive Two - Adds SVG element hover event/data filter

enter image description here

DIRECTIVE ONE:

//directive loads SVG into DOM
angular.module('FFPA').directive('svgFloorplan', ['$compile', function  ($compile) {
return {
    restrict: 'A',

    templateUrl: 'test.svg',
    link: function (scope, element, attrs) {

        var groups = element[0].querySelectorAll("g[id^='f3']")
        angular.forEach(groups, function (g,key) {
            var cubeElement = angular.element(g);
            //Wrap the cube DOM element as an Angular jqLite element.
            cubeElement.attr("cubehvr", "");
            $compile(cubeElement)(scope);
        })
    }
}
}]);

The SVG diagram contains tags with unique identifiers, ie:

<g id="f3s362c12"></g>

Directive Two loads JSON data from an injected service that corresponds to each of the SVG tag id's.

 //filters json based on hover item
 dataService.getData().then(function(data) {
    thisData = data.filter(function (d) {
    return d.seatId.trim() === groupId
 });

As shown above, Directive Two also adds a hover event function that filters the JSON data based on the tag that was hovered over.

IE: If a user hovers over , a filter in the directive would return this JSON record:

{"Id":1,
 "empNum":null,
 "fName":" Bun E.",
 "lName":"Carlos",
  ...
 "seatId":"f3s362c12 ",
 "floor":3,
 "section":"313 ",
 "seat":"12 "}

DIRECTIVE TWO:

//SVG hover directive/filter match json to svg
angular.module("FFPA").directive('cubehvr', ['$compile', 'dataService',     function ($compile, dataService) {
return {
    restrict: 'A',
    scope: true,
    link: function (scope, element, attrs) {

        //id of group 
        scope.elementId = element.attr("id");
        //alert(scope.elementId);
        var  thisData;
        //function call
        scope.cubeHover = function () {

            //groupId is the id of the element hovered over.
            var groupId = scope.elementId;

            //filters json based on hover item
            dataService.getData().then(function(data) {
            thisData = data.filter(function (d) {
                return d.seatId.trim() === groupId
            });
              //return data.seatId === groupId
              scope.gData = thisData[0];
              alert(thisData[0].fName + " " + thisData[0].lName + " " +   thisData[0].deptId);  
            });
            //after we get a match, we need to display a tooltip with   save/cancel buttons.
            $scope.empData = $scope.gData;
        };
        element.attr("ng-mouseover", "cubeHover()");
        element.removeAttr("cubehvr");
        $compile(element)(scope);
    }
    //,
    //controller: function($scope, $element){
    // $scope.empData = $scope.gData;
    //}
  }
}]);

The problem we now have is now (besides having minimal Angular experience and facing a unique and difficult implementation problem) is that we're trying to implement a way to create a tooltop using a div tag and an angular scope variable that we can display when a user hovers over the SVG tag element (instead of a Javascript alert which is demonstrated in the Plunker POC link below).

Since the data is being driven by the directive and the directive is already taking "cubehvr" as a parameter:

angular.module("FFPA").directive('*cubehvr*', ['$compile', 'dataService', function ($compile, dataService)

We're stuck since we don't know how to set an HTML page scope directive or variable, say like this from our second directive:

<div uib-popover="Last Name: {{empData.lName}}" 
    popover-trigger="'mouseenter'" 
    type="div" 
    class="btn btn-default">Tooltip
</div>

Or as simple as say, this:

  <div emp-info></div>

The div tooltips will have html buttons that call Web API Update functionality.

We have a scaled down POC Plunk here:

POC Plunk

Also were thinking about using the Angular Bootstrap UI for the toolips:

Bootstrap UI Plunk

Hope that makes sense.


Solution

  • //Edit. I read your question once again and go thru my answer. I didn't fully answer your question as it's multi-layered. Now I'll go thru all your concerns and try to answer them:

    1. Passing $scope to the other directives.

    $scope is Model-View in MVVM design pattern, it glues your Template (View) and your Model together. In theory you could probably pass the $scope to the other directive, but I think it's an anti-pattern.

    1. Communication between directives.There are at least 4 methods which you can use to communicate your directives:

      • Share the same scope, what you almost did in your plunker, just don't define any 'scope' in your directives' spec. I'm not sure if it's the best way to go with as any of your directives can malform your scope's data.
      • Create isolated scopes and use ng-model or $watch, it's safer method, but it needs more overhead. In such case you pass the variable down, which you scope.$watch. It's two-way-binding. You can push and pull the value. $watch
      • Create a service, where you keep something like an event-bus or a storage for your variables
      • You can communicate your directives with events: $on $emit That works well with hierarchical directives (so effectively, you would have to create isolated child scopes)
    2. Add popover to a SVG's child. Bootstrap has an ability to add popover to body instead of to parent element. It's useful for SVGs: https://angular-ui.github.io/bootstrap/#!#popover

    I refactored your code to use two directives, and the data is loaded in controller. One directive wraps the popover and the second one passes the data, also the popover uses template now, so it's being compiled with angular:

      var app = angular.module('FFPA',  ['ngAnimate', 'ngSanitize', 'ui.bootstrap']);
    
      //controller
      app.controller('myCtrl', function ($scope, dataService) {
        $scope.test = 'test';
        dataService.getData().then(function(data) {
          $scope.dataset = data.reduce(function (obj, item) {
            obj[item.seatId.trim()] = item;
            item.fullName = item.fName + ' ' + item.lName;
            return obj;
          }, {});
        });
      });
    
      angular.module('FFPA').service('dataService', function($http){
        this.getData = function(){
          return $http.get("data.json").then(
            function(response){
              return response.data;
            }, function() {
              return {err:"could not get data"};
            }
          );
        }
      });
    
      //directive loads SVG into DOM
      angular.module('FFPA').directive('svgFloorplan', ['$compile', function ($compile) {
          return {
            restrict: 'A',
            templateUrl: 'test.svg',
            scope: {
              'dataset': '=svgFloorplan'
            },
            link: {
              pre: function (scope, element, attrs) {
                var groups = element[0].querySelectorAll("g[id^='f3']");
                scope.changeName = function (groupId) {
                  if (scope.dataset[groupId] && scope.dataset[groupId].lastName.indexOf('changed') === -1) {
                    scope.dataset[groupId].lastName += ' changed';
                  }
                }
    
                groups.forEach(function(group) {
                  var groupId = group.getAttribute('id');
                  if (groupId) {
                    var datasetBinding = "dataset['" + groupId + "']";
                    group.setAttribute('svg-floorplan-popover', datasetBinding);
    
                    $compile(group)(scope);
                  }
                });
              }
            }
          }
      }]);
    
      angular.module('FFPA').directive('svgFloorplanPopover', ['$compile', function ($compile) {
          return {
            restrict: 'A',
            scope: {
              'person': '=svgFloorplanPopover'
            },
            link: function (scope, element, attrs) {
              scope.changeName = function () {
                if (scope.person && scope.person.fullName.indexOf('changed') === -1) {
                  scope.person.fullName += ' changed';
                }
              }
              scope.htmlPopover = 'popoverTemplate.html';
              element[0].setAttribute('uib-popover-template', "htmlPopover");
              element[0].setAttribute('popover-append-to-body', 'true');
              element[0].setAttribute('popover-trigger', "'outsideClick'");
              element[0].querySelector('text').textContent += '{{ person.fullName }}';
    
              element[0].removeAttribute('svg-floorplan-popover');
    
              $compile(element)(scope);
    
            }
          }
      }]);
    

    And your HTML body now looks like:

      <body style="background-color:#5A8BC8;">
        <div ng-app="FFPA" ng-controller="myCtrl">
          <div svg-floorplan="dataset"></div>
        </div>
      </body>
    

    HTML for popover:

      <div><button type="button" class="btn btn-default" ng-click="changeName()">{{ person.fullName }}</button></div>
    

    Here is working plunker: http://plnkr.co/edit/uHgnZ1ZprZRDvL0uIkcH?p=preview