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.
Related
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...
I need to render dynamic template from database and also need to bind the variables to expressions.
My response JSON will look like this,
[{
"htmlTemplate": "<div>{{name}}</div><div>{{age}}</div>",
"bindData": {
"name": "safeer",
"age" : "25"
}
}, {
"htmlTemplate": "<span>{{name}}</span><div>{{address}}</div>",
"bindData": {
"name": "john",
"address":"qwerty"
}
}, {
"htmlTemplate": "<h4>{{name}}</h4><h2>{{country}}</h2>",
"bindData": {
"name": "james",
"country":"India",
"state" : "Kerala"
}
}]
I have created a directive as per the answer to the question Compiling dynamic HTML strings from database
In html, demo.html
<div dynamic="html"></div>
In directive, directive.js
var app = angular.module('app', []);
app.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamic, function(html) {
ele.html(html);
$compile(ele.contents())(scope);
});
}
};
});
It will render html template and replace variable with $scope variable name finely.
But I need to render each htmlTemplate with its corresponding bindData. That is render each template with isolated scope data. Need to generalize directive.
Any help appreciated! Thanks in advance.
When you receive the JSON data you can use angular.fromJSON to decode the json string into the array (unless you are using the $http.get() which already does that for you)...
//request the JSON from server
jsonString = SomeFactory.fetchDataFromServer();
$scope.dataArray = angular.fromJson(jsonString);
...and then use the ngRepeat to create multiple elements:
<div ng-repeat="element in dataArray" dynamic="element"></div>
Modify your directive like this:
app.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.bindData = {};
scope.$watch(attrs.dynamic, function(dynamic) {
console.log('Watch called');
ele.html(dynamic.htmlTemplate); //here's your htmlTemplate being compiled
$compile(ele.contents())(scope);
scope.bindData = dynamic.bindData; //here's your bound data ifyou need the reference
scope.name = dynamic.bindData.name; //bound data property name
}, true); //deep watch
}
};
});
Edit: Or you could simply pass element.htmlTemplate and element.bindData separately to the directive through two separate attributes as mentioned in an answer by user Vineet, which makes more sense.
Edit2: Fixed some bugs.
Here's the fiddle: Fiddle
With generalising I assume you want to make it like a component and resuse it whenever you want.
I would suggest to separate the scope of this directive. Bind your "html" and "name" both of them from parent.
AFTER EDITING:
app.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
scope : {
html : "=dynamic"
},
link: function (scope, ele, attrs) {
ele.html(scope.html);
$compile(ele.contents())(scope);
var unbindWatcher = $scope.$watch(
attrs.bindData,
function(binddata) {
if ( binddata ) {
angular.extend(scope,binddata);
// Once the data has been binded to scope,
// there's no more need to watch the change
// in the model value.
unbindWatcher();
}
}
);
}
};
});
And your html as:
<div dynamic="html" bindData="bindData"></div>
In this edit, what I have done is following 3 things:
1) Watching your attribute bindData - which will contain your db stored bind data. I have not included this in scope of directive because I want to include its properties in the scope so that you can bind your 'name', etc. from bindData in db to the templates.
2)Extending the bindData object into scope of your directive. Now your scope will have 'name', etc.
3)Destroying the watcher as soon as bindData is read for first time. This ensures that any change in bindData variable inside parent will not be conveyed to directive after first bind.
But still keep the scope separated to make it work properly.
Hope this will solve your problem
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.
Supposedly, I have a directive with scope
scope: {
columns: '=',
}
How can I achieve this?
<my-directive columns="[{ field:'id', displayName: 'ID' },
{ field:'title', displayName: 'Title' },
{ field:'address', displayName: 'Address' },
{ field:'city', displayName: 'City' }]" />
Apparently Angular's compiler have problems with figuring out that it's an array, although it doesn't have a problem with standard JS objects passed this way - {}. Is there any nifty way to do this? Does the fact that it works with objects is just a coincidence?
Please keep in mind that I know, that I can set this as $scope parameter in Controller and pass just the parameter name from $scope. But I would really like to learn if it's possible to do it straight from HTML.
--
Update with full code:
This is how it is used in template
<es-paged-data-grid columns="[
{ field:'id', displayName: 'ID' },
{ field:'title', displayName: 'Title' },
{ field:'address', displayName: 'Address' },
{ field:'city', displayName: 'City' }
]">
</es-paged-data-grid>
This is the directive:
app.directive('esPagedDataGrid', function () {
var definition = {
restrict: "E",
replace: false,
transclude: true,
scope: {
columns: '=',
},
templateUrl: 'application/directive/pagedDataGrid/PagedDataGrid.html',
controller: ['$scope', '$element', '$attrs', '$transclude', function($scope, $element, $attrs, $transclude) {
var dataGridOptions = {};
if ($scope.columns) {
dataGridOptions.columnDefs = $scope.columns;
}
$scope.dataGridOptions = dataGridOptions;
}]
};
return definition;
});
This is the directive's template:
<div ng-grid="dataGridOptions">
</div>
You have mentioned in one of your comments that your directive throws a nonassign error. A nonassign error occurs when a directive attempts to modify an isolate scope defined using the = notation wherein the assigned attribute is an expression that is not two-way data bound(not a scope variable).
Probably somewhere in your directive, you may have attempted to change scope.columns directly such as, scope.columns = []; or any other scope property that is assigned via the = notation. Try removing that and it might solve your problem.
UPDATE:
trying changing this:
dataGridOptions.columnDefs = $scope.columns;
to this:
dataGridOptions.columnDefs = angular.copy($scope.columns);
I suspect the ng-grid directive is probably doing some manipulation on the columnDefs options, since columnDefs has a direct reference towards $scope.columns property then any manipulation performed in the columnDefs options would likely affect $scope.columns.
Well AFAIK i guess a way to initialize data in DOM is using the ng-init directive.
So the directive can look like,
app.directive('testd', function() {
return {
scope: {
options: "=ngInit"
},
link: function(scope, e, a) {
console.log('test', scope.options);
},
template: 'test'
};
});
And you can supply the array via ng-init,
<div testd ng-init="[{ field:'id', displayName: 'ID' },
{ field:'title', displayName: 'Title' },
{ field:'address', displayName: 'Address' },
{ field:'city', displayName: 'City' }]"></div>
Sample Demo: http://plnkr.co/edit/MwOPLm16KTOr2Q3rY6LK?p=preview
Well one more way would be to pass it via an attribute. Though it will be taken as string, you can use eval() to convert it to object and assign it to a scope variable. Plnkr is also updated for the same. Included columns:"#" and use eval(attrs.columns) to convert it to array
I've created a directive with a binding using "scope". In some cases, I want to bind a constant object. For instance, with HTML:
<div ng-controller="Ctrl">
<greeting person="{firstName: 'Bob', lastName: 'Jones'}"></greeting>
</div>
and JavaScript:
var app = angular.module('myApp', []);
app.controller("Ctrl", function($scope) {
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: {
person: "="
},
template:
'<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
Although this works, it also causes a JavaScript error:
Error: 10 $digest() iterations reached. Aborting!
(Fiddle demonstrating the problem)
What's the correct way to bind a constant object without causing the error?
Here's the solution I came up with, based on #sh0ber's answer:
Implement a custom link function. If the attribute is valid JSON, then it's a constant value, so we only evaluate it once. Otherwise, watch and update the value as normal (in other words, try to behave as a = binding). scope needs to be set to true to make sure that the assigned value only affects this instance of the directive.
(Example on jsFiddle)
HTML:
<div ng-controller="Ctrl">
<greeting person='{"firstName": "Bob", "lastName": "Jones"}'></greeting>
<greeting person="jim"></greeting>
</div>
JavaScript:
var app = angular.module('myApp', []);
app.controller("Ctrl", function($scope) {
$scope.jim = {firstName: 'Jim', lastName: "Bloggs"};
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: true,
link: function(scope, elements, attrs) {
try {
scope.person = JSON.parse(attrs.person);
} catch (e) {
scope.$watch(function() {
return scope.$parent.$eval(attrs.person);
}, function(newValue, oldValue) {
scope.person = newValue;
});
}
},
template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
You are getting that error because Angular is evaluating the expression every time. '=' is for variable names.
Here are two alternative ways to achieve the same think without the error.
First Solution:
app.controller("Ctrl", function($scope) {
$scope.person = {firstName: 'Bob', lastName: 'Jones'};
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: {
person: "="
},
template:
'<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
<greeting person="person"></greeting>
Second Solution:
app.directive("greeting2", function () {
return {
restrict: "E",
replace: true,
scope: {
firstName: "#",
lastName: "#"
},
template:
'<p>Hello {{firstName}} {{lastName}}</p>'
};
});
<greeting2 first-name="Bob" last-Name="Jones"></greeting2>
http://jsfiddle.net/7bNAd/82/
Another option:
app.directive("greeting", function () {
return {
restrict: "E",
link: function(scope,element,attrs){
scope.person = scope.$eval(attrs.person);
},
template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
This is because if you use the = type of scope field link, the attribute value is being observed for changes, but tested for reference equality (with !==) rather than tested deeply for equality. Specifying object literal in-line will cause angular to create the new object whenever the atribute is accessed for getting its value — thus when angular does dirty-checking, comparing the old value to the current one always signals the change.
One way to overcome that would be to modify angular's source as described here:
https://github.com/mgonto/angular.js/commit/09d19353a2ba0de8edcf625aa7a21464be830f02.
Otherwise, you could create your object in the controller and reference it by name in the element's attribute:
HTML
<div ng-controller="Ctrl">
<greeting person="personObj"></greeting>
</div>
JS
app.controller("Ctrl", function($scope)
{
$scope.personObj = { firstName : 'Bob', lastName : 'Jones' };
});
Yet another way is to create the object in the parent element's ng-init directive and later reference it by name (but this one is less readable):
<div ng-controller="Ctrl" ng-init="personObj = { firstName : 'Bob', lastName : 'Jones' }">
<greeting person="personObj"></greeting>
</div>
I don't particularly like using eval(), but if you really want to get this to work with the HTML you provided:
app.directive("greeting", function() {
return {
restrict: "E",
compile: function(element, attrs) {
eval("var person = " + attrs.person);
var htmlText = '<p>Hello ' + person.firstName + ' ' + person.lastName + '</p>';
element.replaceWith(htmlText);
}
};
});
I had the same problem, I solved it by parsing the json in the compile step:
angular.module('foo', []).
directive('myDirective', function () {
return {
scope: {
myData: '#'
},
controller: function ($scope, $timeout) {
$timeout(function () {
console.log($scope.myData);
});
},
template: "{{myData | json}} a is {{myData.a}} b is {{myData.b}}",
compile: function (element, attrs) {
attrs['myData'] = angular.fromJson(attrs['myData']);
}
};
});
The one drawback is that the $scope isn't initially populated when the controller first runs.
Here's a JSFiddle with this code.