AngularJS: Make isolate scope directive template bind to parent scope
Asked Answered
H

2

2

I've been struggling with Angular's isolate scope for over 24hrs now. Here's my scenario: I have an ng-repeat iterating over an array of objects from which I want to use a custom directive to either generate a <select> or <input> based on the field_type property of the current object being iterated. This means I'll have to generate the template and $compile in the post-link function of the directive since I have no access to the iterated object in the template function.

Everything works as expected, apart from the actual binding of the generated template to the controller (vm) in my outer scope. I think my approach (adding this in the template string: ng-model="vm.prodAttribs.' + attr.attribute_code +'") may be wrong, and would appreciate pointers in the right direction. Thanks!

See sample code below:

directives:

directives.directive('productAttributeWrapper', ['$compile',  function($compile){
    //this directive exists solely to provide 'productAttribute' directive access to the parent scope
    return {
        restrict: 'A',
        scope: false,
        controller: function($scope, $element, $attrs){
            this.compile = function (element) {
                $compile(element)($scope);
                console.log('$scope.prodAttribs in directive: ', $scope.prodAttribs);
            };
        }
    }
}]);

directives.directive('productAttribute', ['$compile',  function($compile){
    return {
        restrict: 'A',
        require: '^productAttributeWrapper', //use the wrapper's controller
        scope: {
            attribModel: '=',
            prodAttribute: '=productAttribute', //binding to the model being iterated by ng-repeat
        },
        link: function(scope, element, attrs, ctrl){
            var template = '';
            var attr = scope.prodAttribute;
            if(!attr) return;

            switch(attr.attribute_field_type.toLowerCase()){
                case 'textfield':
                    template = 
                        '<input type="text" id="'+attr.attribute_code+'" ng-model="vm.prodAttribs.' + attr.attribute_code +'">';
                    break;
                case 'dropdown':
                    template = [
                        '<select class="cvl" id="'+attr.attribute_code+'" ng-model="vm.prodAttribs.' + attr.attribute_code +'">',
                            '#cvl_option_values',
                        '\n</select>'
                    ].join('');
                    var options = '\n<option value="">Select One</option>';
                    for(var i=0; i<attr.cvl_option_values.length; i++) {
                        var optionVal = attr.cvl_option_values[i].value;
                        options += '\n<option value="'+optionVal+'">' + attr.cvl_option_values[i].value + '</option>';
                    }
                    template = template.replace('#cvl_option_values', options);
                    break;
            }
            element.html(template);
            ctrl.compile(element.html());  //try to bind template to outer scope
        }
    }
}]);

html:

<div ng-controller="ProductController as vm">
    <div product-attribute="attrib" ng-repeat="attrib in vm.all_attribs"></div>
</div>

controller:

app.controller('ProductDetailsController', function(){
    var vm = this;
    //also added the property to $scope to see if i could access it there
    $scope.prodAttribs = vm.prodAttribs = {
            name: '',
            description: '',
            price: [0.0],
            condition: null
    }
    vm.all_attributes = [
        {
          "attribute_id": 1210,
          "attribute_display_name": "Product Type",
          "attribute_code": "product_type",
          "attribute_field_type": "Textfield",
          "cvl_option_values": [],
          "validation_rules": {}
        },
        {
          "attribute_id": 902,
          "attribute_display_name": "VAT",
          "attribute_code": "vat",
          "attribute_field_type": "dropdown",
          "cvl_option_values": [
            {
              "option_id": "5",
              "value": "5%"
            },
            {
              "option_id": "6",
              "value": "Exempt"
            }
          ],
          "validation_rules": {}
    }];
})
Helenhelena answered 12/1, 2016 at 13:35 Comment(0)
P
1

issue is probably here :

element.html(template);
ctrl.compile(element.html());  //try to bind template to outer scope

element.html() returns a html as a string, not the ACTUAL dom content, so what you inserted into your directive's element is never actually compiled by angular, explaining your (absence of) behaviour.

element.append(ctrl.compile(template));

should work way better.

For directive requiring parent controller, I would also change your ctrl.compile method (renamed to insertAndCompile here)

ctrl.insertAndCompile = function(content) {
    $compile(content)($scope, function(clone) {
        $element.append(clone);
    }
}

You would just have to call it this way :

ctrl.insertAndCompile(template);

instead of the 2 lines I gave as first answer.

Paronymous answered 12/1, 2016 at 13:49 Comment(1)
Thanks @Pierre Gayvallet. ctrl.insertAndCompile() was the fix I needed! It's so painful that something so little kept me awake all night. Stackoverflow rocks, and you rock more.Helenhelena
T
1

I would suggest to use templates instead of html compilation manually. The solution is much simpler:

Controller would contain data declaration:

app.controller('ProductDetailsController', function($scope) {
  $scope.prodAttribs = {
    name: '',
    description: '',
    price: [0.0],
    condition: null
  }
  $scope.all_attribs = [{
    "attribute_id": 1210,
    "attribute_display_name": "Product Type",
    "attribute_code": "product_type",
    "attribute_field_type": "Textfield",
    "cvl_option_values": [],
    "validation_rules": {}
  }, {
    "attribute_id": 902,
    "attribute_display_name": "VAT",
    "attribute_code": "vat",
    "attribute_field_type": "dropdown",
    "cvl_option_values": [{
      "option_id": "5",
      "value": "5%"
    }, {
      "option_id": "6",
      "value": "Exempt"
    }],
    "validation_rules": {}
  }];
});

Your directive would be as simple as that:

app.directive('productAttribute', function() {
  return {
    restrict: 'A',
    scope: {
      attribModel: '=',
      prodAttribute: '=productAttribute'
    },
    templateUrl: 'template.html',
    controller: function($scope) {}

  }
});

template.html would be:

<div>
  <select ng-show="prodAttribute.attribute_field_type.toLowerCase() == 'dropdown'" class="cvl" id="" ng-model="prodAttribs.attribute_code">
    <option value="">Select One</option>
    <option ng-repeat="item in prodAttribute.cvl_option_values track by $index"  value="{{item.value}}">{{item.value}}</option>
  </select>
  <input ng-show="prodAttribute.attribute_field_type.toLowerCase() == 'textfield'" type="text" id="{{prodAttribute.attribute_code}}" ng-model="prodAttribute.attribute_code">
</div> 

And your html:

<div ng-controller="ProductController"> 
    <div ng-repeat="attrib in all_attribs" product-attribute="attrib">{{attrib}}</div>
</div>
Tien answered 12/1, 2016 at 14:15 Comment(2)
I didn't want to create bloated html, as there would be a whole lot of these controls on the page, resulting in a lot of unnecessary hidden input elements. reason why I did it in the link function instead. Thanks all the same for the effort put into this answerHelenhelena
Makes sense, however it is way too dirty to create dirrective in such a manner.Tien

© 2022 - 2024 — McMap. All rights reserved.