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

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:
Open Plunker
Click Next button two times (to navigate to question 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.

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

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.

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.

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

Related

Why aren't the inputs changing color?

I'm on a angularjs application with jquery and can't seem to change the background color of some inputs. In short, when the user clicks "hide stage" text or "unhide stage" text, the input boxes background should change to white/lightgrey respectively. But the only thing that changes is the text between "hide stage" to "unhide stage" which is fine. Here is the angularjs file:
function stages($scope,$rootScope,$apiSrvc,$compile){
// defintions
// setting up stages statuses to angular array.
$scope.stages_visibilities = ['unhidden','unhidden','unhidden','unhidden','unhidden','unhidden','unhidden','unhidden'];
// changes the stage's status and it's visibility settings
$scope.setStageStatus = function(stageN,status){
$scope.stages_visibilities[stageN] = 'hidden';
if(status === 'unhide'){
$scope.stages_visibilities[stageN] = 'unhidden';
}
$scope.showStages();
};
// shows the stage rows.
$scope.showStages = function() {
// update(populate) the caption/value rows
$('#stages_rows').empty();
$nStages = parseInt($scope.nStages);
var stage_row = "";
for(var i=0; i < $nStages; i++){
stage_row += '<div class="stage_row">'+
'<input type="text" id="stage_caption_'+i+'" class="stg_caption" />'+
'<input type="number" id="stage_value_'+i+'" class="stg_val" />';
// show hide/unhide for this stage.
if($scope.stages_visibilities[i] === 'unhidden'){
stage_row += '<span id="stage_hide_unhide_'+i+'" class="hide_unhide_stg" ng-click="setStageStatus('+i+',\'hide\')">hide stage</span>';
// set row color to white.
$("#stage_caption_"+i).css("background-color","white");
$("#stage_value_"+i).css("background-color","white");
$("#stage_value_"+i).prop('disabled', false);
$("#stage_caption_"+i).prop('disabled', false);
}
else {
stage_row += '<span id="stage_hide_unhide_'+i+'" class="hide_unhide_stg" ng-click="setStageStatus('+i+',\'unhide\')">un-hide stage</span>';
// set row color to lightgrey.
$("#stage_caption_"+i).css("background-color","lightgrey");
$("#stage_value_"+i).css("background-color","lightgrey");
$("#stage_value_"+i).prop('disabled', true);
$("#stage_caption_"+i).prop('disabled', true);
}
stage_row += '</div>';
}
$("#stages_rows").append(stage_row);
// register new directives to angularjs
$compile($("#stages_rows").contents())($scope);
}
}
All the angular js pre-liminary stuff work fine, it's just that section of the code where it doesn't change the background colors of the input(#stage_caption_i and #stage_value_i) which is in the $scope.showStages function. I have no idea what is wrong.
Here is a more "Angular" approach to what you're trying to do. I would recommend setting aside your jQuery knowledge as it really should only be used in a very limited fashion in directives. As georgeawg mentioned you shouldn't do DOM manipulation in the controller. Rather you should modify the values of your model and then put all the view-specific stuff in the HTML.
A preferred method would be to make the hidden/visible status a property on the model, rather than maintaining a separate array with that data. Of course, you can still use an array, but then you have to write a method to check the value of the visibility in the corresponding array when you want to change a class or toggle the disabled status. Using a method like that will cause a lot of traffic between the controller and the view as the view calls back for each item. Probably not noticeable on a small sample like this, but could lead to performance issues on a larger view. Even if you are getting this data back from a database or some other storage on the back end you can still extend the model to include a visibility property on the client.
The id values aren't needed, but I've included them to show how to use $index in case you decide to go the route of storing visibility in a separate array. You could use the $index value from the ng-repeat to reference the proper element in the visibility array.
This separation of controller and view will help with long term maintainability and keep Angular and jQuery from stepping on each other as they both manipulate elements of the DOM.
angular.module('app', [])
.controller('ctrl', ($scope) => {
$scope.stages = [{
caption: 'Stage 0',
value: 0,
hidden: false
}, {
caption: 'Stage 1',
value: 1,
hidden: false
}, {
caption: 'Stage 2',
value: 2,
hidden: false
}, {
caption: 'Stage 3',
value: 3,
hidden: false
}, {
caption: 'Stage 4',
value: 4,
hidden: false
}, {
caption: 'Stage 5',
value: 5,
hidden: false
}, {
caption: 'Stage 6',
value: 6,
hidden: false
}, {
caption: 'Stage 7',
value: 7,
hidden: false
}];
// changes the stage's status and its visibility settings
$scope.setStageStatus = (stage) => {
stage.hidden = !stage.hidden;
};
});
.hidden {
background-color: lightgray;
}
.unhidden {
background-color: white;
}
.hide_unhide_stg {
cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.2/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div class="stage_row" ng-repeat="stage in stages">
<input type="text"
id="stage_caption_{{$index}}"
class="stg_caption"
ng-model="stage.caption"
ng-disabled="stage.hidden"
ng-class="{hidden: stage.hidden, unhidden: !stage.hidden}" />
<input type="number"
id="stage_value_{{$index}}"
class="stg_val"
ng-model="stage.value"
ng-disabled="stage.hidden"
ng-class="{hidden: stage.hidden, unhidden: !stage.hidden}" />
<span class="hide_unhide_stg"
ng-click="setStageStatus(stage)"><span ng-if="stage.hidden">un-</span>hide stage</span>
</div>
</div>

How to apply show less and show more on cells of a reactive table in meteor

document_table_Settings : function ()
{
return{
rowsPerPage: 5,
showNavigation: 'auto',
showColumnToggles: false,
fields: [
{key: 'para',label: 'Para',sortable: false},
{key: 'desc', label: 'Description',sortable: false},
{
key: 'rowId', label: 'Delete',sortable: false, fn: function (rowId, object) {
var html = "<button name='Del' id=" + rowId + " class='btn btn-danger'>Delete</button>"
return new Spacebars.SafeString(html);
}
},
{
key: 'rowId', label: 'Edit',sortable: false, fn: function (rowId, object) {
var html = "<button name='edit' id=" + rowId + " class='btn btn-warning'>Edit</button>"
return new Spacebars.SafeString(html);
}
}
]
};
}
I want to show description entries having show more and show less feature .As the description is long enough. so after 100 character it shows button to toggle.
If I understand you correctly, you are trying to only show the first 100 characters of the 'Description' column in the Reactive Table and then add some mechanism so that the user can click or rollover to see the entire 'Description' text.
There are a few ways to achieve this and I have provided two options below (in order of simplicity).
For a low tech rollover option, truncate the text to only show the first 100 characters, add an ellipsis (...) to the end of your text, then use the title property in a span element to show the full text on rollover.
First you will need to define a 'truncate' Template helper (I would make this a global helper so that you can use anywhere in your app).
Template.registerHelper('truncate', function(strValue, length) {
var len = DEFAULT_TRUNCATE_LENGTH;
var truncatedString = strValue;
if (length && length instanceof Number) {
len = length;
}
if (strValue.length > len) {
truncatedString = strValue.substr(1, len) + "...";
}
return truncatedString;
});
Then create a new Template for the column.
<template name="field_description">
<span title="{{data.description}}">{{truncate data.description}}</span>
</template>
And finally, change your Reactive Table configuration to use a Template.
fields: [
...,
{ key: 'desc', label: 'Description', tmpl: Template.field_description }
...,
];
For a slightly more complicated option, you can take a similar approach but add a clickable link that would show more or less detail. To get it to work you have to define a few Reactive Vars, define an event handler, and change your 'Description' Template accordingly. Here is a rough solution that should work.
Change your template like so.
<template name="field_description">
<span>{{truncatedDescription}}
{{#if showLink}}
{{linkState}}
{{/if}}
</span>
</template>
Then add the necessary logic to your field_description template (including an event handler).
import { Template } from 'meteor/templating';
import './field-description.html';
Template.field_descirption.onCreated(function() {
const MAX_LENGTH = 100;
this.description = new ReactiveVar(Template.currentData().description);
this.showMore = new ReactiveVar(true);
if (this.description.get().length > MAX_LENGTH) {
this.description.set(Template.currentData().description.substr(1, MAX_LENGTH));
}
this.showLink = () => {
return Template.currentData().description.length > MAX_LENGTH;
};
this.toggleTruncate = () => {
if (this.showMore.get()) {
this.description.set(Template.currentData().description);
this.showMore.set(false);
} else {
this.description.set(Template.currentData().description.substr(1, MAX_LENGTH));
this.showMore.set(true);
}
};
});
Template.field_descirption.helpers({
truncatedDescription: function() {
return Template.instance().description.get();
},
showLink: function() {
return Template.instance().showLink();
},
linkState: function() {
if (Template.instance().showMore.get()) {
return 'show more';
} else {
return 'show less';
}
};
});
Template.field_descirption.events({
'click .js-more-less'(event, instance) {
instance.toggleTruncate();
},
});
Lastly, make sure your Reactive Table config is still setup to use a Template for the field.
fields: [
...,
{ key: 'desc', label: 'Description', tmpl: Template.field_description }
...,
];
Note that the second option makes use of Meteor's Reactivity to solve the problem. Let me know if you need additional explanation on how the 2nd solution works.
That should do it!

dynamic grid as custom directive

I am relatively new to Angular and got stuck on a custom directive.
I am trying to create a dynamic grid as a custom directive.
I already got that part working as in this example:
working grid as custom directive
There are certain scenarios where I need to set attributes on some of the elements of the grid.
This part has got me stumped.
I am planning on including the attributes as an array inside the object and then just putting it in the html tag of the associated entry.
This part is demonstrated here:
broken grid as custom directive with dynamic attributes
If you look at the "entries" array in the controller, I have now changed it to include an "attributes" array which will contain objects specifying the attribute name and property. These attributes should then be applied to the associated column.
e.g.
(First entry of the array)
col1: {
text: 'Obj1.col1',
attributes: [{
attr: 'ng-class',
attrVal: 'propVal == "" ? "someClass" : "someOtherClass"'
}, {
attr: 'id',
attrVal: '{{propName}}{{$index}}'
}]
},
...Truncated for brevity
This array entry should then be translated to:
<td ng-class="propVal == '' ? 'someClass' : 'someOtherClass'" id="col11">Obj1.col1</td>
I have read a couple of articles about the execution order of compile, controller, pre-link and post-link functions and have played around with different orders and trying to invoke compiling myself, but it all has failed.
Probably because I lack a deeper understanding of how it all ties together.
If someone can help me out or point me in the right direction if I'm heading down the wrong path, I would greatly appreciate that.
Okay, I finally figured out how to generate the grid dynamically using embedded custom directives inside a parent custom directive.
Here is a plunker showing how I did it:
Plunker with working dynamic grid
I have the Html templates defined as:
<div ng-grid ng-collection="entries" ng-collection-headings="headings" ng-button-click="theAction(inp)">
<div ng-checkbox-column></div>
</div>
and then the ng-grid directive as:
.directive("ngGrid", function () {
return {
restrict: "A",
scope: {
ngCollectionHeadings: "=",
ngCollection: "=",
ngButtonClick: "&"
},
template: function (element, attrs) {
var children = element.html();
children = children.trim().replace(/div/g, "td");
var htmlText = "<input type='button' ng-click='buttonClicked()' value='From the grid directive' /><table class='table table-bordered'><thead><tr><th ng-repeat='heading in ngCollectionHeadings'>{{heading}}</th></tr></thead><tbody><tr id='item{{$index}}' ng-repeat='item in ngCollection'>" + children + "</tr></tbody></table>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.buttonClicked = function (inp) {
if (typeof inp != 'undefined')
inp = inp + ", through the grid directive.";
else
inp = "From the grid directive.";
$scope.ngButtonClick({ inp: inp });
};
}
};
})
and finally the ng-checkbox-column directive:
.directive("ngCheckboxColumn", function () {
return {
restrict: "A",
template: function (element, attributes) {
var htmlText = "<td><label><input type='checkbox' ng-model='item.checked' ng-click='tempButtonClicked()' /> From the checkbox directive.</label></td>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.tempButtonClicked = function () {
var val = "From the checkbox directive";
$scope.buttonClicked(val);
};
}
};
})
My data collections are pretty straight forward:
$scope.headings = {
head1: 'Heading 1',
head2: 'Heading 2',
head3: 'Heading 3'
};
$scope.entries = [{
col1: 'Obj1.col1',
col2: 'Obj1.col2',
col3: 'Obj1.col3',
checked: false
}, {
col1: 'Obj2.col1',
col2: 'Obj2.col2',
col3: 'Obj2.col3',
checked: false
}, {
col1: 'Obj3.col1',
col2: 'Obj3.col2',
col3: 'Obj3.col3',
checked: false
}, {
col1: 'Obj4.col1',
col2: 'Obj4.col2',
col3: 'Obj4.col3',
checked: false
}];
This is still not entirely completed, but you should get the basic idea.

How to programatically add an option to an optiongroup in selectize.js

I want to dynamically add an option to an optiongroup in Selectize.js. The API only has
addOption(data)
updateOption(value, data)
addOptionGroup(id, data)
without much help on what "data" is. I've seen the examples for adding an option but no mention of using optionGroups
$('#button-addoption').on('click', function() {
control.addOption({
id: 4,
title: 'Something New',
url: 'http://google.com'
});
Thanks
Data is the object passed to the optgroup rendering method.
And so, you can put anything in it.
$('#selectize').selectize({
...
optgroupField: 'mygroup',
render: {
optgroup_header: function(data, escape) {
return '<div class="optgroup-header">' + escape(data.a) + escape(data.b) '</div>';
}
},
...
});
And then, whenever you want, you can add groups and options in the selectize:
//add group
var optGroup = { a: 'fruit', b: ... };
$('#selectize')[0].selectize.addOptionGroup('0', optGroup);
//add option
var option = { value: 'abc', text: 'banana', mygroup: '1'};
$('#selectize')[0].selectize.addOption(option);
Of course, if you only want a label for the group, you can do this:
//code
...
render: {
optgroup_header: function(data, escape) {
return '<div class="optgroup-header">' + escape(data) + '</div>';
}
...
//code
$('#selectize')[0].selectize.addOptionGroup('1', 'meat');
You can see the API demo (search for 'Optgroups (programmatic)' in page).

Angular: compiling template from string to string

See the fiddle
test.directive('testMe', ['$compile', function($compile) {
return {
restrict: 'EA',
transcluded: true,
link: function(scope, element, attrs) {
scope.state = 'compiled';
//a = $(element.html()); //this gives: Error: Syntax error, unrecognized expression: Something I actually want to save {{state}}
a = $('<div>' + element.html() + '</div>');
var el = $compile(a)(scope);
scope.compiled = element.html();
},
}
}]);
For some reason I want to compile template with a given scope to a string, and after asking Uncle Google and doing some experiments I gave up.
Does annyone knows how to do this? Maybe my approach is wrong at all?
I want to notice that as a result I need template compiled to string, saved in a variable.
EDIT
To be more specific, here's what i need:
var template = "<p>{{variable}}</p>";
$scope.variable = "test";
var compiled = //magic goes here
alert(compiled); // gives <p>test</p>
I recently stumbled onto a similar problem and after several hours i was able to solve it with a little help from this post:
Blog post from Ben Lesh
I wanted to create a ComboBox to select an Image for another Entity to save in a relational Database. Of course my Entity had other relations too and so I described them in a JSON-File
//image
{ id: 4, path: 'http://www.example.com/myimage.png', name: 'Picture of my cat' }
//entity
{ id: 7, foo: 'bar', imageId: 4, anotherEntityId: 12}
//anotherEntity
{ id: 12, name: 'My Entity'}
I now wanted to create a Formular for entering new entities and for the foreign keys I wanted a combobox
I then declared another JSON-Object, containing every column of entity and also how i wanted them rendered
{cols:
[
{
name: 'imageId',
displayName: 'Image',
type: Image,
template: '<img src="{{e.path}}" />{{e.name}}'
},
...
]}
To do so i created a new directive, called nComboBoxRenderer
<ng-combo-box-renderer data="image", template="column.template"></ng-combo-box-renderer>
-
directives.directive('ngComboBoxRenderer', ['$compile', function($compile) {
return {
restrict: "E",
scope: {
e: '=data', // in my case this is the image
template: '=template' // the template
},
link: function($scope, element, attributes) {
var tl = angular.element("<span>" + $scope.template + "</span>");
compiled = $compile(tl);
element.append(tl);
compiled($scope);
}
}
}]);
While this is not the exact same use case as you have, the process involved appears to be the same.

Categories