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

4

9

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.

Bindle answered 19/2, 2016 at 11:47 Comment(1)
The related issue can be found here: github.com/angular/material/issues/7275Rudiger
C
3

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

Chiu answered 19/2, 2016 at 11:57 Comment(5)
Thank You for this proposal, but could You also help me with removing error I'm getting right now?Bindle
That's a good question. The first that I noticed is that you don't have an ng-model on your md-radio-groupChiu
I've added ng-model but it didn't help.Bindle
I've tried Your approach but it didn't help. I need a way to conditionally replace template of my directive, and those templates contains other directives. Only when adding ngMaterial I'm getting an error.Bindle
any ideas why I get that error? I've tried Your approach but without luck. I get this error only when I use angular material inside my directiveBindle
V
3

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.

Velours answered 1/3, 2016 at 4:52 Comment(3)
Thank You so much for reply. It is more complicated. I'd like to use sub-directives in my directive, because I need different logic for each type of question, for example validation. For simple text input I'd like to check if entered text is equal to correct answer I'd like to get, for checkboxes I must check if only correct are selected. I think this kind of modularity will help me in future. Here is Plunker showing my second approach: plnkr.co/edit/fq6nTXGYBT8oJSkvOFIE?p=previewBindle
@Misiu, Sounds good. Sorry to misleads you. Go with your current approach. All the best !!! :)Velours
Your solution works fine without error, but I really would like to use that inside directive. Any ideas why I'm getting that error?Bindle
A
3

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.

Arbe answered 13/9, 2018 at 9:47 Comment(2)
Thanks for this deeply buried nugget of gold. I've encountered the same mdRadioGroup required error, but it's on a component rather than a directive, so there isn't an explicit link function that can be changed like this. Is there a corresponding change that can be made to a component, or will the non-destroying scope problem essentially be within the angular js library code and unpatchable?Jackstraw
Components are just directives in AngularJS (.component() is just syntactic suger over .directive() with some default values), so this isn't much different. For example, you can use the component controller's $postLink lifecycle hook instead of a directive's post-link callback: docs.angularjs.org/api/ng/service/$compile#life-cycle-hooksArbe
F
2

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

Fuzzy answered 19/2, 2016 at 15:43 Comment(1)
Thank You for help. I think it's bit weird behavior, because I'm compiling template every time I change question. I'll leave this question open and start bounty as fast as I can.Bindle

© 2022 - 2024 — McMap. All rights reserved.