javascriptangularjsangularjs-directiveangularjs-ng-repeatisolate-scope

AngularJS: Make isolate scope directive template bind to parent scope


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

Solution

  • 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.