Contidional template - Controller 'mdRadioGroup', required by directive 'mdRadioButton', can't be found

I'm trying to build custom directive that will allow me to display questions in survey. Because I have multiple types of questions I thought about creating single directive and change it's template based on question type.

my directive:

directive('question', function($compile) {
  var combo = '<div>COMBO - {{content.text}}</div>';
  var radio = [
    '<div>RADIO - {{content.text}}<br/>',
    '<md-radio-group layout="row" ng-model="content.answer">',
    '<md-radio-button ng-repeat="a in content.answers track by $index" ng-value="a.text" class="md-primary">{{a.text}}</md-radio-button>',
    '</md-radio-group>',
    '</div>'
  ].join('');
  var input = [
    '<div>INPUT - {{content.text}}<br/>',
    '<md-input-container>',
    '<input type="text" ng-model="content.answer" aria-label="{{content.text}}" required md-maxlength="10">',
    '</md-input-container>',
    '</div>'
  ].join('');

  var getTemplate = function(contentType) {
    var template = '';

    switch (contentType) {
      case 'combo':
        template = combo;
        break;
      case 'radio':
        template = radio;
        break;
      case 'input':
        template = input;
        break;
    }

    return template;
  }

  var linker = function(scope, element, attrs) {

    scope.$watch('content', function() {
      element.html(getTemplate(scope.content.type))
      $compile(element.contents())(scope);

    });
  }

  return {
    //require: ['^^?mdRadioGroup','^^?mdRadioButton'],
    restrict: "E",
    link: linker,
    scope: {
      content: '='
    }
  };
})

Inside my main controller I have list of questions and after clicking button I'm setting current question that is assign to my directive.

Everything works fine for first questions, but after I set current question to radio type I get this error:

Error: [$compile:ctreq] Controller 'mdRadioGroup', required by directive 'mdRadioButton', can't be found!

I've tried adding required to my directive as below, but it didn't helped.

require: ['^mdRadioGroup'],

I can't figure out whats going on, because I'm still new to angular.

I've created Plunker to show my issue: http://plnkr.co/edit/t0HJY51Mxg3wvvWrBQgv?p=preview

Steps to reproduce this error:

  1. Open Plunker
  2. Click Next button two times (to navigate to question 3)
  3. See error in console

EDIT:
I've edited my Plunker so my questions model is visible. I'm able to select answers, even in questions that throw error-questions model is updating. But still I get error when going to question 3.

Answers:

Answer

I'd just simply extend a base directive, and then have a specialized ones with different directive names too.

// <div b></div>
ui.directive('a', ... )
myApp.directive('b', function(aDirective){
   return angular.extend({}, aDirective[0], { templateUrl: 'newTemplate.html' });
});

Code taken from https://github.com/angular/angular.js/wiki/Understanding-Directives#specialized-the-directive-configuration

Answer

Working Demo

There is no need to create and use a directive for your requirement.

You can just use angular templates and ng-include with condition.

You can just create three templates (each for combo, radio and input) on your page like this,

<script type="text/ng-template" id="combo">
    <div>COMBO - {{content.text}}</div>
</script>

And include these templates in a div using ng-include.

<!-- Include question template based on the question -->
<div ng-include="getQuestionTemplate(question)">

Here, getQuestionTemplate() will return the id of the template which should be included in this div.

// return id of the template to be included on the html
$scope.getQuestionTemplate = function(content){
    if(content.type == "combo"){
      return 'combo';
    }
    else if (content.type == "radio"){
      return 'radio';
    }
    else{
      return 'input';
    }
}

That's all. You are done.

Please feel free to ask me any doubt on this.

Answer

I played a little with your code and find that, the reason why the error occurred is because the 3rd question got more answers than the 2nd, so when you create the mdRadioGroup the first time it defines 4 $index answers and later for question 3 it go out of bound with 6 answers... So a non elegant solution is to create as many $index as the max answers to any question, the first time, show only the ones with text...

.directive('question', function($compile) {
var combo = '<div>COMBO - {{content.text}}</div>';
var radio = [
'<div>RADIO - {{content.text}}<br/>',
'<md-radio-group layout="row">',
'<md-radio-button ng-repeat="a in content.answers track by $index" ng-show={{a.text!=""}} value="{{a.text}}" class="md-primary">{{a.text}}</md-radio-button>',
'</md-radio-group>',
'</div>'
].join('');
var input = [
'<div>INPUT - {{content.text}}<br/>',
'<md-input-container>',
'<input type="text" ng-model="color" aria-label="{{content.text}}" required md-maxlength="10">',
'</md-input-container>',
'</div>'
].join('');

var getTemplate = function(contentType) {
var template = '';

switch (contentType) {
  case 'combo':
    template = combo;
    break;
  case 'radio':
    template = radio;
    break;
  case 'input':
    template = input;
    break;
}

return template;
}

then change questions to have the max amount of answers every time in all questions:

$scope.questions = [{
type: 'radio',
text: 'Question 1',
answers: [{
  text: '1A'
}, {
  text: '1B'
}, {
  text: '1C'
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}]
}, {
type: 'input',
text: 'Question 2',
answers: [{
  text: '2A'
}, {
  text: '2B'
}, {
  text: '2C'
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}]
}, {
type: 'radio',
text: 'Question 3',
answers: [{
  text: '3A'
}, {
  text: '3B'
}, {
  text: '3C'
}, {
  text: '3D'
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}]
}, {
type: 'combo',
text: 'Question 4',
answers: [{
  text: '4A'
}, {
  text: '4B'
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}, {
  text: ''
}]
}];

The rest of the code is the same. As I say before, no elegant and for sure there are better options, but could be a solution for now...

Answer

In case anyone is wondering, the problem is that the parent component's scope is used to compile each new element. Even when the element is removed, bindings on that scope still remain (unless overwritten), which may cause the errors OP saw (or even worse memory leaks).

This is why one should take care of cleaning up when manipulating an element's HTML content imperatively, like this. And because this is tricky to get right, it is generally discouraged to do it. Most usecases should be covered by the built-in directives (e.g. ngSwitch for OP's case), which take care of cleaning up after themselves.


But you can get away with manually cleaning up in a simplified scenario (like the one here). In its simplest form, it involves creating a new child scope for each compiled content and destroying it once that content is removed.

Here is what it took to fix OP's plunker:

before

scope.$watch('content', function() {
  element.html(getTemplate(scope.content.type))
  $compile(element.contents())(scope);
});

after

var childScope;
scope.$watch('content', function() {
  if (childScope) childScope.$destroy();
  childScope = scope.$new();
  element.html(getTemplate(scope.content.type))
  $compile(element.contents())(childScope);
});

Here is the fixed version.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.