3

I have two elements and will have more looking like these:

<chart type="type-one" legend="true"></chart>
<chart type="type-two" legend="true"></chart>

Each element is supposed to be processed by its own directive put in a separate file. How can I make these directives look for both element name to be chart and type attribute to be present to be processed?

Update: @vittore, thanks for the extended answer! My issue is I want to have different types of chart that are processed by directives from different files, resulting in not having one giant file that processes chart elements with inner functions running for each type of chart, but rather more modularized files that are in charge for processing each chart type.

Now I do it like this:

app.directive('chart', function () {
  return {
    restrict: 'E',
    require: 'ngModel',
    scope: {
      'ngModel': '=',
      'showGrouped': '='
    },

    link: function (scope, element, attrs, ctrl) {
      if (attrs.type != 'type-one') {return}
      render()
    }
  }
});

So I check for the type attribute and if it's not some value, return, otherwise run respective rendering code. I have this piece of code in each directive file that does specific chart type rendering. I am sure there is something wrong in this approach.

Please advise!

Sergei Basharov
  • 51,276
  • 73
  • 200
  • 335
  • So you currently have multiple directives with the same name? – runTarm Jul 17 '14 at 08:26
  • Yes, many directives looking in the top exactly like what I showed, except they check `attrs.type` with different strings then do different code if it's equal to the expected string. – Sergei Basharov Jul 17 '14 at 08:28
  • ok, how I unerstood you want to have seperate view files depends on type value , yes ? – Narek Mamikonyan Jul 17 '14 at 08:29
  • Yes, so that to keep code for each chart type in its own file. – Sergei Basharov Jul 17 '14 at 08:30
  • 4
    Is there any specific reason why you can't just use ``, `` and so on? Is the value in the type attribute static or dynamic or even can be an angular expression? – runTarm Jul 17 '14 at 08:33
  • No too serious reasons, more like cleanness and better understanding of the code by other devs in the teams. – Sergei Basharov Jul 17 '14 at 08:35
  • 1
    I read your question 3 times and cannot still understand what you are going to exactly achieve and why. Could you provide more context? – artur grzesiak Jul 17 '14 at 13:58
  • I will suggest you to use as directive and write all the common code for chart inside that directive. Then, add attribute as additional function. So, your chart directive should looks like or . Then you can use angular restrict on attribute to add different function to directive. – hutingung Jul 18 '14 at 09:11
  • Quote from angularjs "When should I use an attribute versus an element? Use an element when you are creating a component that is in control of the template. The common case for this is when you are creating a Domain-Specific Language for parts of your template. Use an attribute when you are decorating an existing element with new functionality." - https://docs.angularjs.org/guide/directive – hutingung Jul 18 '14 at 09:13
  • Another way is refer to angularjs how to implement ngInput directive. Same html markup but differentiate using type. Just like your requirement. – hutingung Jul 18 '14 at 09:20
  • I posted my answer with sample code that I grab from angularjs source. – hutingung Jul 18 '14 at 09:25
  • @SergeyBasharov I'll update my answer later today, but the way I would do it is update templateUrl property of directive based on provided parameters. I think you can even take it from here. Удачи. – vittore Jul 21 '14 at 10:55
  • Maybe this article could be helpfull http://onehungrymind.com/angularjs-dynamic-templates/ – stanislav.chetvertkov Jul 22 '14 at 11:54

7 Answers7

4

Use scoped parameters

 angular.module('myModule',[])
        .directive('chart', function() {
           return {
             templateUrl:'chart.html',
             replace:true,
             restrict: 'E',
             scope: {
                type:'=', 
                legend:'='
             }                  
           }
        })

and chart.html

  <div> {{type}} {{legend}} </div>

Update:

In order to make attributes truly required, you can throw an exception from either link or compile function of directive when invalid values are provided or some values not provided at all.

Update 2:

There are 3 types of attribute scope binding: =, &, @. And you should use them appropriately.

If you want to just pass strings into directive you might use @ binding:

  .directive('chart', function() {
           return {
             templateUrl:'chart.html',
             replace:true,
             restrict: 'E',
             scope: {
                type:'@', 
                legend:'@'
             }                  
           }
        })

This way your parameters will be treated as a string:

 scope.type='true'
 scope.legend='type-one'

Or you may want to bind them to scope fields:

 <chart type="myModel.type" legend="myModel.legend" />

Having = scope declarations:

 .directive('chart', function() {
           return {
             templateUrl:'chart.html',
             replace:true,
             restrict: 'E',
             scope: {
                type:'=', 
                legend:'='
             }                  
           }
        })

Will create two-way binding between directive's scope properties and parent scope properties:

  scope.type = $parent.myModel.type
  scope.legend = $parent.myModel.legend

After that you can change both properties in the parent scope and in directive scope as well.

Most complicated & binding, which allow you to provide method with parameters on parent scope to be called from directive:

  app.directive("chart", function() {
    return {
      scope: {
        details: "&"
      },
      template: '<input type="text" ng-model="value">' +
        '<div class="button" ng-click="details({legend:value})">Show Details</div>'
    }
  })

and markup

  <chart details='showDetails(legend)'></chart> 

For details on each type of scope binding, please see excellent videos from egghead.io:

vittore
  • 17,449
  • 6
  • 44
  • 82
1

I'd make use of the following:

  1. A constant, to hold which types of charts you support throughout the application.
  2. A named controller for each type of chart.
  3. (Optional) A service to be aware of the currently active charts and what type they are.

Register the app

var app = angular.module('app', []);

Register the constant to control which types we support

app.constant('chartTypes', ['typeOne', 'typeTwo']);

Directive

app.directive('chart', ['$controller', 'chartTypes', function ($controller, chartTypes) {
  return {

    restrict: 'E',

    scope: {
      'showGrouped': '='
    },

    controller: function ($scope, $element, $attrs) {

      if (chartTypes.indexOf($attrs.type) === -1) {

        throw new Error('Chart type ' + $attrs.type + ' is not supported at the moment');

      } else {

        return $controller($attrs.type + 'Controller', {
          $scope: $scope,
          $attrs: $attrs

        });
      }
    },

    link: function (scope, element, attrs, controller) {

      // Rock'n'roll...
      controller.init();

    }
  }
}]);

Note: I stripped away the ngModel requirement from the directive for now to make it clearer how to build something like this. I'm not sure how you would get ngModel to play alongside this solution, but I'm sure you can figure that one out.. If not, I'd be happy to give it a try later on.

Controller(s)

app.controller('typeOneController', ['$scope', '$attrs', function ($scope, $attrs) {

  var ctrl = this;

  ctrl.init = function () {
    console.log('We are initialised and ready to rock!');
  };

  $scope.someFunc = function () {
    console.log($scope, 'is the isolate scope defined in the directive');
  };

}]);

Markup:

<chart type="typeOne" legend="true"></chart>

<chart type="typeTwo" legend="true"></chart>

<chart type="typeThree" legend="true"></chart>

Expected result:

  1. Chart typeOne should be rolling fine at this point, logging out that we are in fact initialised.
  2. Chart typeTwo should throw an error stating that a Controller by that name could not be found (undefined).
  3. Chart typeThree should throw an error stating that the passed in chartType is not currently supported.

In closing:

Now, this is not your conventional directive structure - but it's one I think is highly underused. The benefits of having your linking function be a controller, is that you can completely separate the $scope behaviour from the directive definition.

This in turn allows us to unit test $scope behaviour for the directive itself, without the need to instantiate the directive and its DOM structure in our unit tests. Another added benefit is that you don't have to setup multiple directives for different chart types, we simply call for a controller (or, linking behaviour (if you will)) based on the chart type passed into the directive definition.

This could be built on further to include services and what not, everything can then be injected into your Controllers, on a per-chart basis (or into the directive definition, to have it be there for all the charts), giving you a ton of flexibility and ease of testing to boot.

Another lesson to take home, is that anonymous functions as directive controllers are hard to test, in comparison to named controllers that are defined elsewhere, then injected into the directive. Separation is gold.

Let me know if this does the trick for you, or if you need a more in depth example of how to set it up.

I'll see if I can't get a plunker or something of the sort uploaded throughout the day.

Edit:

Added jsFiddle showcasing the behaviour, albeit a bit simplified (not an isolated scope, no templateUrl): http://jsfiddle.net/ADukg/5416/

Edit 2: Updated the jsFiddle link, this one was with an isolated scope and hence the ng-click definition on the calling element does not fire off the isolated functions.

Edit 3: Added example of some $scope functions.

Edit 4: Added another jsFiddle, showcasing an isolate scope and a templateURL. More in line with what you are working with.

http://jsfiddle.net/ADukg/5417/

0

You should implement a directive with name 'chart'. Inside link function, you can access all atrributes of the directive & implement the functionality accordingly. Accessing attributes from an AngularJS directive

Community
  • 1
  • 1
Nilesh
  • 5,955
  • 3
  • 24
  • 34
0

My suggestion is have a templates object and store the seperated view files and call the view.html depends on type which we getting from attr

var app = angular.module('myApp',[]);


    app.directive('chart', function () {
        var templates = {
            'type-one': 'typeOne.html',
            'type-two': 'typeTwo.html'
        }
        return {
            restrict: 'E',
            scope: {
                'type':'@'
            },
            templateUrl: function(tElement, tAttrs) {
                 return templates[tAttrs.type];
            },
            link: function (scope, element, attrs, ctrl) {


            }
        }
    });
Narek Mamikonyan
  • 4,601
  • 2
  • 24
  • 30
0

This solution based on angularjs source on how they implement directive for <input></input> - ngInput

//function to render text input
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
...
}

//function to render number input
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
...
}

//function to render url input
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
...
}

var inputType = {
  'text': textInputType,
  'number': numberInputType,
  'url': urlInputType,
  ...
}


 //input directive
 var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
  return {
    restrict: 'E',
    require: '?ngModel',
    link: function(scope, element, attr, ctrl) {
      if (ctrl) {
        //logic to use which one to render
        (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
                                                            $browser);
      }
    }
  };
}];
hutingung
  • 1,800
  • 16
  • 25
0

You could have "chart" as a directive, then delegate to other directives based on the type attribute ie "chart1", "chart2" etc by doing something similar to the following: Add directives from directive in AngularJS

Community
  • 1
  • 1
jimbo.io
  • 174
  • 8
0

How can I make these directives look for both element name to be chart and type attribute to be present to be processed?

chart is name of the directive, so if by mistake you use chart2 as name instead of chart, you directive wont itself get processed, so there is no ways to check the typo error in directive names

about checking the type attribute and given that you want it to be mandatory & you have certain predefined types. The technique I follow is define one factory/service per type that to be processed, so if i have typeOne & typeTwo chart, I will create separate factories of both.

app.factory("typeOneChart", function(){
   var renderOne = function(params) {
     // logic for rendering typeOneChart
   };

   return {
      render : renderOne
   }
})

app.factory("typeTwoChart", function(){
   var renderTwo = function(params) {
     // logic for rendering typeTwoChart
   };

   return {
      render : renderTwo
   }
})

also notice that the returning function name is same in all factories, so its kind of implementing an interface.

and then in directive, just write a switch case which does the offloading part to particular factory.

app.directive('chart', function (typeOneChart, typeTwoChart) {

  var linkFunction = function(scope, iElement, iAttrs, ngModelCtrl){
     switch(iAttrs.type) {
        case 'one':
           scope.chartFactory = typeOneChart;
           break;
        case 'two':
           scope.chartFactory = typeTwoChart;
           break;
        default:
           //toastr.error("invalid chart type : " + iAttrs.type);
           throw exception("invalid chart type : " + iAttrs.type);
     }

     scope.chartFactory.render();

  }

  return {
    restrict: 'E',
    require: 'ngModel',
    scope: { 'ngModel': '=', 'showGrouped': '=' },
    link: linkFunction
  }
});

this approach keep your directive code at minimal, plus if someone specifies an invalid chart type, it just throws an exception, or show and error on screen which you can immediately see and fix.

also now all the code for a particular type lies in its factory... so the controller is agnostic of the logic per chart type.

harishr
  • 17,807
  • 9
  • 78
  • 125