Nested ng-repeat with ng-bind-html - javascript

I want to render a list of widgets on a page from my controller. Each widget has its own render function that returns a safe HTML string.
My first part of my ng-repeat (outside layer) looks like this:
$scope.renderWidgets = function() {
var html = "";
html += "<div ng-repeat='widget in widgets'>";
html += "<div ng-bind-html='widget.render()'></div>"
html += "</div>";
return $sce.trustAsHtml(html);
}
As you can see I have ng-bind-html inside my ng-repeat which is calling a function named .render() located inside widget:
this.render = function() {
var html = "";
html += "<div ng-repeat='choice in widget.choices'>";
html += "{{choice.name}}";
html += "</div>";
return $sce.trustAsHtml(html);
}
I also have a directive that I use to $compile the above HTML string into AngularJS.
If I run the above code the widget.render() function gets called just fine but the output on the page will look like this: {{choice.name}} and not the value inside choice.name.
As you can see I am trying to do another ng-repeat inside ng-bind-html with the widget object from my first ng-repeat.
Is this even possible what I am trying to do here (I am new to AngularJS)? If yes then what am I doing wrong? Or is there another way to resolve my problem?
I am using the latest AngularJS (v. 1.7.9).
UPDATE
I think I know the issue. When I call my first function $scope.renderWidgets() the returned HTML will be automatically compiled(the first ng-repeat) at which point the ng-bind-html gets triggered returning a non-compiled HTML string. Now I have to figure out a way to $compile the returned HTML from my ng-bind-html directive.
Am I on the right track?

The ng-bind-html directive only binds HTML. It does not compile HTML. This was a deliberate decision by the AngularJS team to avoid security problems.
You state that you have a directive that compiles HTML. Let's assume the directive is:1
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);
});
}
};
});
Then use it in your nested code:
$scope.renderWidgets = function() {
var html = "";
html += "<div ng-repeat='widget in widgets'>";
̶h̶t̶m̶l̶ ̶+̶=̶ ̶"̶<̶d̶i̶v̶ ̶n̶g̶-̶b̶i̶n̶d̶-̶h̶t̶m̶l̶=̶'̶w̶i̶d̶g̶e̶t̶.̶r̶e̶n̶d̶e̶r̶(̶)̶'̶>̶<̶/̶d̶i̶v̶>̶"̶
html += "<div dynamic='widget.render()'></div>"
html += "</div>";
return $sce.trustAsHtml(html);
}
For more information, see
Compiling dynamic HTML strings from database — this answer

Related

Dynamically Added ng-model in custom directive does not work

I have searched for this question on Stackoverflow, but all are different from this one. Although the question might be the same the implementation is different.
This directive accepts an array of JSON called config. Which contains properties of an input or a dropdown. I also receive the variable that has to be binded within ngModel. Everything works fine like ng-maxlength ng-minlength but ng-model does not. I cannot fetch the values or in simple words there is no two way binding. Also to get all the values within a single object I append a string called pgCard within the name attribute in the custom directive. To make it look like pgCard.* to use it directly as a json object. But all in vain. I even tried removing the pgCard but of no use.
Below is my custom directive Element markup
<program-gateway config="configArrayCard" name="pgCard"></program-gateway>
This is my custom directive.
app.directive('programGateway',['$compile',
function($compile){
return {
restrict : 'E',
replace :true,
template:"<div class='row'></div>",
scope:{
config:"=",
name:"#"
},
link: function(scope,e,attr){
var modelPrefix=scope.name+"." || "";
var field="",model="";
var max="",min="",maxlen="",req="",options="";
var leftHTML="",rightHTML="",innerHTML="";
scope.config=sortByKey(scope.config,"propertyKey");
angular.forEach(scope.config,function(value,key){
model=modelPrefix+value.propertyKey;
var midLen=(Math.floor(scope.config.length/2))+(Math.floor(scope.config.length%2));
if(!value.propertyInfo.selection){
if(value.propertyInfo.hasOwnProperty("max")){
max="data-ng-maxlength='"+value.propertyInfo.max+"'";
maxlen="maxlength='"+value.propertyInfo.max+"'"
} if(value.propertyInfo.hasOwnProperty("min")){
min="data-ng-minlength='"+value.propertyInfo.min+"'";
}
req=(value.propertyInfo.optional)?"":"required";
field="<input "+req+" class='form-control' type='text' id='"+model+"' data-ng-model='"+model+"' "+max+" "+min+" "+maxlen+">";
}else{
var optArr="[\""+(value.propertyInfo.selectionOptions).replace(/,/g, '","')+"\"]";
field="<select data-ng-options='opt for opt in "+optArr+"' "+req+" data-ng-model='"+model+"' class='form-control' id='"+model+"'>"+
"<option value=''>Select </option>"+
"</select>";
}
if(key<midLen){
leftHTML+="<div class='form-group'>"+
"<label for='"+value.propertyInfo.propertyLabel+"' class='col-sm-5 control-label'><span data-ng-hide='"+value.propertyInfo.optional+"' class='red'>*</span>"+value.propertyInfo.propertyLabel+":</label>"+
"<div class='col-sm-7'>"
+field+
"</div>"+
"</div>";
}else{
rightHTML+="<div class='form-group'>"+
"<label for='"+value.propertyInfo.propertyLabel+"' class='col-sm-5 control-label'><span data-ng-hide='"+value.propertyInfo.optional+"' class='red'>*</span>"+value.propertyInfo.propertyLabel+":</label>"+
"<div class='col-sm-7'>"
+field+
"</div>"+
"</div>";
}//if closed
});// for closed
innerHTML="<div class='col-sm-6'>"+leftHTML+"</div>"+
"<div class='col-sm-6'>"+rightHTML+"</div>";
e.html(innerHTML);
scope.$watch(function() {
return scope.pgCard = scope.pgCard;
});
$compile(e.contents())(scope);
}
}
}]);
This is the snap of the json that I receive
"config":[{"propertyKey":"reportGroup","editable":true,"propertyInfo"
:{"max":3,"optional":true,"propertyLabel":"Report Group","selection":false}},{"propertyKey":"sendStatementToCompany"
,"propertyValue":"Yes","editable":true,"propertyInfo":{"optional":false,"propertyLabel":"Send Statement
to Company","selection":true,"selectionOptions":"Yes,No","defaultValue":"Yes"}}]
Please help am I doing something wrong.

Angular Material Components in Ng-Sweet-Alert-2

I try to put an HTML code including Angular material input component (which is included in my project) in ng-sweet-alert dialog:
var strVar="";
strVar += "<div> <md-input-container style=\"padding-left:0px;\" flex=\"\">
<label>Titel<\/label> <input ng-model=\"WBQuery.Titel\">
<\/md-input-container> <\/div>";
swal({
title: 'HTML example',
html: strVar });
}
But what is shown is just the standard input field of HTML. Is there any (hacking) way to use angular material components in that dialog?
I've not tried it but you can try this. After you have invoked the swal function, try the following:
(Assuming you are doing it in a directive or a controller)
var element = angular.element(document.querySelector(".sweet-alert"));
$compile(element.contents())($scope);
If that doesn't work, try to put the above code in a $timeout service:
$timeout(function() {
var element = angular.element(document.querySelector(".sweet-alert"));
$compile(element.contents())($scope);
}, 2000);

Creating names for elements created by an Angular directive

For a project I'm working on, I created a simplified version of the UI Bootstrap Calendar widget.
Plunker of my Simplified Calendar, and how I'm using it, is here.
One interesting aspect of the UI Bootstrap calendar, is that even though it goes onto a input[text], it still produces a date validation in the $error dictionary for a form controller, just as if I had specified an input[date] element in my DOM.
However, there's a catch with numerous sub-catches. One thing you'll notice right away in my plunker's DOM is that I've specified error spans for times when the given date fields are not actually dates (try entering something ridiculous like 'cat' for a value!) If you enter something that isn't a date, those should appear, but they don't.
I've tried a few things to expose the markup being created to the name field of the parent:
$transclude set to false, such that the <calendar></calendar> tags get replaced with the contents of the calendar directive's template, with a name attribute specified. This "works", except that said input is wrapped in a span that has a class necessary to look correct using the Bootstrap styling framework.
Directly creating a name attribute in the calendar directive's input field with a binding, like so*:
app.directive('mustPrecedeDate', [
function () {
return {
restrict: 'E',
template: '<input type="text" name="{{ someName }}" />',
scope: {},
controller: 'calendarCtrl',
link: function () {}
};
}
};
Writing link code to explicitly find the input that is a child of the calendar generated markup, and assign it a name attribute. Both 2 and 3 failed, because apparently that's not really something that can be done (I can't find the SO question that was the source of that discovery.)
This leads to my Question: in what way can I get a name down to the input element, such that validation results can be reported to the $error dictionary, so that I can give my users helpful validation messages?
*: Apparently, code blocks with the 'four spaces from the left' formatting don't behave well with numbered lists, so I had to use back-quote code notation to get the text to format halfway correctly. Please feel free to correct my formatting, if I haven't found a bug in the markdown setup SO uses.
The #3 thing needed to be tried a bit harder!
I was able to get a name on the input by adding the following code into my link function:
var inputElement = elem.find('input');
inputElement.attr('name', inputName);
...Where inputName is scraped from the attributes list. I was able to get the inputName down to the generated input[text] field by using a compile function as below.
app.directive('calendar', [
function() {
return {
restrict: 'E',
transclude: false,
scope: {},
template:
'<span class="input-group">'
+ '<input class="form-control" required '
+ 'type="text" placeholder="MM/dd/yyyy" '
+ 'data-ng-model="dt" data-ng-click="toggle($event)" '
+ 'data-ng-change="updateParentProperty()" '
+ 'datepicker-popup="MM/dd/yyyy" is-open="isOpen" />'
+ '<span class="input-group-btn">'
+ '<button type="button" class="btn btn-default" data-ng-click="toggle($event)">'
+ '<i class="fa fa-calendar"></i>'
+ '</button>'
+ '</span>'
+ '</span>',
controller: 'calendarCtrl',
compile: function(elem, attrs) {
var inputName = attrs.inputName;
var inputElement = elem.find('input');
inputElement.attr('name', inputName);
// Compile returns a Link function!
return function(scope, elem, attrs, ctrl) {
var modelName = attrs.ngModel;
scope.parentProperty = modelName;
scope.dt = scope.$parent[modelName];
};
}
};
}
]);

AngularJS generate HTML template with ng-click="function(arg)"

I am trying to generate an HTML templaye from AngularJS directive but I'm not able to pass any object into a function within one of the generated elements. This is my directive:
app.directive('listObject', function($compile){
return {
scope : {
obj : "="
},
controller: "httpPostController",
link : function(scope, element) {
scope.elementTree = null;
//recursivly generate the object output
scope.printObject = function (field, content) {
if (field){
content = "<div>"
if (field.isRequired){
content += "<p>" + field.name + "*:</p>";
} else {
content += "<p>" + field.name + ":</p>";
}
if (field.isEnum){
content += '<select ng-model="createEntityResource[field.name]" ng-change="getCreateEntityAsText()" class="form-control">' +
'<option></option>' +
'<option ng-repeat="enumValue in field.enumValues" label={{enumValue.name}}>{{enumValue.ordinal}}</option>' +
'</select>';
} else if (field.restResourceName) {
content += '<button type="button" ng-click="loadResourceFieldsForField(field)">Create new</button>';
// content += "<p>"+i+""+scope.printObject(field)+"</p>";
} else {
content += '<input type="text" ng-model="createEntityResource[' + field.name + ']" ng-change="getCreateEntityAsText()"' +
'class="form-control" placeholder="{{parseClassName(field.type)}}">';
}
content+="</div>";
return content;
}
};
scope.refresh = function (){
if(scope.elementTree){
scope.elementTree.remove();
}
//generate the html into a string
var content = scope.printObject(scope.obj, content);
//make elements out of a string
scope.elementTree = angular.element(content);
compiled = $compile(scope.elementTree);
element.append(scope.elementTree);
compiled(scope);
};
scope.refresh();
}
};
});
When I create a <button> element - I give it a ng-click function. The function is called and works fine, except that the param it is passed (field) is always undefined.
Does anyone know how to pass object into function within an AngularJS directive?
Every useful answer is highly appreciated and evaluated.
Thank you.
P.S. I tried to split the definition of the button to :
'<button type="button" ng-click="loadResourceFieldsForField(' + field + ')">Create new</button>';
but this does not seem work, AngularJS argues about [Object object] being passed to a function.
P.P.S I also tried to make it according to documentation which says:
Often it's desirable to pass data from the isolated scope via an
expression and to the parent scope, this can be done by passing a map
of local variable names and values into the expression wrapper fn. For
example, if the expression is increment(amount) then we can specify
Blockquote
the amount value by calling the localFn as localFn({amount: 22}).
So it looks like this, but field is still undefined:
'<button type="button" ng-click="loadResourceFieldsForField({field: field})">Create new</button>';
So my printObject algorithm worked great :)
`<button type="button" ng-click="loadResourceFieldsForField(' + field + ')">Create new</button>`
The problem here is: You are generating a string.
And you print your object into the string. This will call toString within the object and won't do what you want to do. It will just print "loadResourceFieldsForField([Object])"
`<button type="button" ng-click="loadResourceFieldsForField({field: field})">Create new</button>`
The field property in the string doesn't have any reference to your param field in the method.
Within $compile: angularjs will search for a variable field within the scope and won't find any.
You have to place this field in the scope, to make this work as aspected. But it seems not like a easy job there.
I would try this:
$compile in each printObject with an own $scope.$new containing the field variable.

Angularjs Directive

This is my scenario. I am adding a block of code dynamically using javascript. Since its dynamic I have to bind it to my angularjs scope, which is done. But I have one problem here. One of the text box has a directive for it which works. But on change of any text box other than the directive text box for first time the scope.$watch triggers, later on it does not. Here is my code
$('.addNew').click(function(){
var uniqid = Date.now();
var html= '';
html += '<section class="newItem" id="'+uniqid+'">';
html += '<h4 style="margin: 10px 22px 8px 22px;color:#FF9900;border-bottom:1px dotted black;padding:1%;" > Grocery: <em>{{gName}}</em></h4>';
html += '<div class="grosinput" style="width:0%;">-</div>';
html += '<div class="grosinput" style="width:50%;">';
html += '<lable style="color:#6699CC;font-size: 15px;">Name:</lable><input type="text" placeholder="Enter grocery item" name="name" ng-model="gName"/>';
html += '</div>';
html += '<div class="grosinput">';
html += '<lable style="color:#6699CC;font-size: 15px;">Cost:</lable><input type="text" placeholder="Enter Cost" value="30" name="cost" cost-check ng-model="cost"/></div></section>';
var $injector = angular.injector(['ng', 'grocery']);
$injector.invoke(function($rootScope, $compile) {alert('t');
$('.grocadd').hide().after($compile(html)($rootScope)).fadeIn(1500);
});
});
Here is the directive
app.directive('costCheck',function($compile,$rootScope){
$rootScope.gName= "What did i buy?";
return{
restrict: 'A',
link: function(scope,element,attrs){
scope.$watch('cost',function(oldval,newval){alert(attrs.name);
if(attrs.name === 'cost'){
alert(oldval+'--'+newval);
}
});
}
}
});
why is it triggering for other text box also
Please note Angular relies on dirty checking which means it triggers all the watches to check if anything is dirty in order to update the View.
Either you should put a if condition to check newVal and oldVal are different to go ahead as:
scope.$watch('cost',function(newVal, oldVal){
if (newVal !== oldVal){
alert(oldval+'--'+newval);
}
});
Or you can use attrs.$observe to observe the attributes as:
I changed the directive in the DOM from cost-check to cost-check="{{cost}}" and replace $watch with $observe as:
attrs.$observe('costCheck', function(val) {
console.log(scope.$eval(val));
});
Demo: http://jsfiddle.net/HB7LU/3018/

Categories

Resources