Compiling dynamic content – AngularJS

All we need is an easy explanation of the problem, so here it is.

I’m rewording this question as I think the original wasn’t too clear.

Basically, I have a ‘wrapper’ directive where I am attempting to dynamically add attributes to one of the wrapped (transcluded) elements. I can get this to work, but Angular doesn’t seem to be aware of the new attributes once added.

If I use $compile then Angular does recognise them – but at the expense of double-compiling the transcluded content, and in this case it then doubles the number of options in the select tag.

Here is a plunker that shows (with comments) what I am attempting, and the same code follows below for those who can look at code and suggest an answer just by looking:
(note – my ultimate aim is to check the custom directive valid-form-group for the required attribute, and if found to apply it to the contained select tag)

HTML

<body ng-controller="MainCtrl">

  <form name="validationForm" novalidate>

    <valid-form-group class="form-group" ng-class="{'has-error': validationForm.validInfo.$error.required}" required>

      <select ng-model="data.option" ng-options="option.id as option.message for option in selectOptions" name="validInfo" id="validInfo">
        <option value="">-- Select a Question --</option>
      </select>

    </valid-form-group>

  </form>

</body>

JS

var app = angular.module('plunker', [])
  .controller('MainCtrl', function($scope) {
    $scope.selectOptions = [
      {id: 1, message: 'First option'}, 
      {id: 2, message: 'Second option'}, 
      {id: 3, message: 'Third option'}
    ];
  })
  .directive('validFormGroup', function($compile) {
    return {
      restrict: 'E',
      template: '<div><span ng-transclude></span></div>',
      replace: true,
      transclude: true,
      require: '^form',
      link: function(scope, element, attrs, ctrl) {

        if (attrs.required !== undefined) {

          var selectElement = angular.element(element.find('select'));
          // either of the below produce the same results
          selectElement.attr('ng-required', true);
          //selectElement.attr('required', true);

          // if the below is commented out it wont validate
          // BUT if it is left in it will recompile and add another 3 options
          $compile(selectElement)(scope); 
        }
      }
    };
  });

CSS

.has-error{
  border: solid 1px red;
}

Please note that the sample here is using ‘required‘ (or ng-required) as the added attribute to highlight the fact that Angular does not recognise it unless compiled.

Any help or comments are welcome – a bit disappointed that I can’t get this to work, so perhaps there’s something fundamental I’m missing…

The plunker should help with visualising my problem.

edit – apologies for the delay in responding to the answers and comments. As mentioned in a comment or two below, personal issues have prevented me from being able to find the time to investigate.

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Try this simple directive:

.directive('validFormGroup', function($compile) {
    return {
        restrict: 'A',
        replace: false,
        require: '^form',
        compile: function (element, attr) {
            if (attr.required !== undefined) {
                var selectElement = element.find('select');
                // either of the below produce the same results
                selectElement.attr('ng-required', true);
                //selectElement.attr('required', true);
            }
        }
    };
});

And use it as html attribute:

<div valid-form-group class="form-group" ng-class="{'has-error': validationForm.validInfo.$error.required}" required>

      <select ng-model="data.option" 
      ng-options="option.id as option.message for option in selectOptions"
      name="validInfo" id="validInfo" >
<option value="">-- Select a Question --</option>
</select>
      <br/>
      <br/>Required invalid? {{validationForm.validInfo.$error.required||false}}
      <br/>
      <br/>

</div>

DEMO

Explanation:

  • I don’t use transclude at all in this solution as the purpose of this directive is just to modify the html before compiling with the scope, there is no need for transcluded content which is overly complicated.

  • Here I handle the compile function instead of the link function. compile function is a great place for you to modify html before linking to the scope.

Method 2

I can only guess what you are seeing is the result of a double compile during the directives initialization process, that’s why you are seeing the double sets of options.

You can solve this by wrapping your compile inside $timeout, which will ensure the compilation happens outside the directive initialization. Here’s a working demo and below the directive code:

.directive('validFormGroup', function($compile, $timeout) {
  return {
    restrict: 'E',
    template: '<div><span ng-transclude></span></div>',
    replace: true,
    transclude: true,
    require: '^form',
    link: function(scope, element, attrs, ctrl) {
      if (attrs.required !== undefined) {
        var selectElement = angular.element(element.find('select'));
        $timeout(function(){
          selectElement.attr('ng-required', true);
          $compile(selectElement)(scope);
        });
      }
    }
  };
});      

PS You can achieve similar bootstrap wrapper functionality by using isolated scope on your directive and then checking if your transcluded input/select element has the required attribute set. Define a function on the isolated scope to check for error and tie this function to the form-group ng-class has-error. This way you dont have to use $timeout at all.

Method 3

I would like to suggest here a different approach, which i adapted for having dynamic validation. adding validations dynamically to the field, reduces boilerplate html code for each field, i have written a directive for similar purpose. please see the plunker link for example directive… PLUNKER

i have written such directives for all types of fields: number, text, select, textarea, bool, datepicker, etc… the attached plunker gives you an example for text & number field.

the magic of angular happens using below liner:

  var newElem = angular.element(template);
  element.replaceWith(newElem);
  $compile(newElem)(scope);

all other code is just some logical if else part..

Method 4

You not need to use $compile in this case. I just changed the code to solve your problem. I try to keep as near as possible to your original version to help you understand.

Javascript (added isRequired scope variable based on attrs.required)

  .directive('validFormGroup', function($compile) {
    return {
      restrict: 'E',
      template: '<div><span ng-transclude></span></div>',
      replace: true,
      transclude: true,
      require: '^form',
      link: function(scope, element, attrs, ctrl) {
        if (attrs.required !== undefined) {
          //added isRequired
          scope.isRequired = true;

          var selectElement = angular.element(element.find('select'));
          // either of the below produce the same results
          //selectElement.attr('ng-required', true);
          //selectElement.attr('required', true);

          // if the below is commented out it wont validate
          // BUT if it is left in it will recompile and add another 3 options
          //remove $compile
          //$compile(selectElement)(scope); 
        }
      }
    };
  });

HTML (added ng-required=isRequired)

<select ng-model="data.option" ng-options="option.id as option.message for option in selectOptions" name="validInfo" id="validInfo" ng-required="isRequired">

You can refer to plunkr version at http://plnkr.co/edit/BGQo05mTNr1H1HjFXSpf?p=preview

I think this answer your question. If you got more complex scenario, please share.

Updated: (after read through others comment and answer again)
If you need dynamic HTML content, you can use this solution – Compiling dynamic HTML strings from database

Another way is to remove ng-*attribute to prevent recompile. plunkr version at http://plnkr.co/edit/JpfdvISCZ39heuUfdHt3?p=preview

      selectElement.removeAttr('ng-options');
      selectElement.removeAttr('ng-model');

Method 5

I don’t have a complete answer however if you you give your directive an isolated scope scope: {}, then on the double compile it can’t get the options so fails silently – I tried this in your plunkr and I only got one set of options in the drop down.

I said I don’t have a compete answer – I don’t believe it is – it seems like a hack and I think there are things on the scope that you do need to share with the directive you might be able to inherit these alone via an inherited scope so that you can maintain the functionality but as I said it feels hacky and doesn’t feel right because it doesn’t address the fundamental problem of how best to handle the double compile.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply