AngularJS: Make isolate scope directive template b

2019-05-25 08:50发布

问题:

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": {}
    }];
})

回答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.



回答2:

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>