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

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.

Related

addeventlistener to indexed element

I have a list of elements. However, the length of this list varies between trials. For example, sometimes there are 6 elements and sometimes there are 8. The exact number is detailed in an external metadata.
To display this variable list, I've written:
var html = '';
html += '<div id="button' + ind + '" class="buttons">';
html += '<p>' + name + '</p></div>';
display_element.innerHTML = html;
If I were to 'inspect' the elements in my browser, they would appear to have IDs of button0.buttons, button1.buttons, etc.
Now I am trying to attach event listeners to each element but my code is not working so far. Different forms of broken code below:
document.getElementById("button' + ind + '").addEventListener("click", foo);
$("#button' + ind + '").click(foo);
document.getElementById("button").addEventListener("click", foo);
$("#button").click(foo);
Any help would be very appreciated! Thanks.
You wrong at concat string update it as
document.getElementById("button" + ind).addEventListener("click", foo);
var html = '';
var ind = 1;
var display_element = document.getElementById("test");
html += '<div id="button' + ind + '" class="buttons">';
html += '<p>' + name + '</p></div>';
display_element.innerHTML = html;
document.getElementById("button" + ind).addEventListener("click", foo);
function foo(){
alert('click');
}
<div id="test"></div>
Use "document.getElementsByClassName" get all botton elements then foreach to add click function.
document.getElementsByClassName('buttons').map( element => { element.addEventListener("click", foo) })
To answer the question of why neither of those uses of document.getElementById() are working for you, you are mixing your quotes incorrectly. "button' + ind '" evaluates to exactly that, rather than evaluating to "button0", "button1", etc. To make your code more readable, and to avoid similar quote mixing issues, I would recommend looking into template literals https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
With modern JS if you want to execute the same function you won't require to add an id to each button.
Just use the class added to the buttons like this:
document.querySelectorAll('.buttons').forEach(button => {
button.addEventListener('click',foo);
});
Then use the event parameter in that function to get the target node & execute whatever you want. You can also add data attributes in those buttons to use while executing that function.

How to pass a value from a Javascript generated button to a controller?

My code generates a table with a button at the end of each row. When the user clicks a button how can I pass a property u.userEmail to the controller via the button? Will the value being sent to the controller be a string?
My (non-working) attempt:
<script>
$(document.body).append("waiting on async table to load<br>");
$(document).ready(function () {
$.getJSON("/Account/LoadClaimsTable", function (crewResponse) {
//returns a List<UserClaims>
$(document.body).append("<table>")
crewResponse.forEach(function (u) {
var s = "";
s+="<tr><td>" + u.userEmail + "</td>";
u.userClaims.forEach(function (k) {
console.log("added claim"+k.value);
s += ("<td>" + k.type + "</td><td>" + k.value + "</td><td>" +
"<input type=\"hidden\" name=\"userEmail\" value=\"`${u.userEmail}`\" />"+
"<input type=\"button\" value=\"Create\" onclick=\"location.href='#Url.Action("EditClaims", "Account")'" />
+"</td>");
});
s += "</tr>";
$(document.body).append(s);
s = "";
});
$(document.body).append("</table>")
});
});
</script>
AccountController.cs contains:
public ActionResult EditClaims(string userEmail)
{
return View("StringView", userEmail);
}
You have to pass it on the url of the action. Not sure if you want to pass u.userEmail, but it could looks like this:
crewResponse.forEach(function (u) {
var s = "<tr><td>" + u.userEmail + "</td>";
u.userClaims.forEach(function (k) {
console.log("added claim"+k.value);
s += ("<td>" + k.type + "</td><td>" + k.value + "</td><td>" +
"<input type=\"hidden\" name=\"userEmail\" value=\"`${u.userEmail}`\" />"+
"<input type=\"button\" value=\"Create\" onclick=\"location.href='#Url.Action("EditClaims", "Account")?userEmail=" + u.userEmail + "'\"/></td>");
});
s += "</tr>";
$(document.body).append(s);
});
There are multiple ways to do it. One is mentioned in the answer above by Felipe. Here is another alternate approach using unobtrusive js
Add the email as html5 data attributes to the button along with another attribute which we will use bind the click behavior.
u.userClaims.forEach(function (k) {
// Add quotes as needed if you want multiline ( i just removed those)
s += "<td>" + k.type + "</td><td>" + k.value + "</td><td>
<input type='button'
clickablebutton data-email='" + u.email + "' value='Create'/></td>";
});
Now, in your document ready, bind a click event handler to those elements (with our custom attribute) and read the data attribute and build the url you need.
$(document).on("click", "input[clickablebutton]", function (e){
var url = '#Url.Action("EditClaims", "Accounts")?useremail=' + $(this).data("email");
window.location.href = url;
});
Some other suggestions
Use the appropriate element. Button is better than input (Consider accessibility)
If it is for navigation, Use an anchor tag instead of a button.
Inline javascript is not great. Let the browser parses your markup without any interruptions and you can add the behavior scripts later (that is the whole point of uobutrisive js approach)
The approach you appear to be taking would be Ajax, response, render a template. With that being said, you may want to rethink your approach.
Step 1.
Build a template
<template id="...">
<button type="button" value="[action]" onclick="[url]">[text]</button>
</template>
Step 2.
Create your request.
axios.get('...').then((response) => {
// Retrieve template.
// Replace bracket with response object model data.
html += template.replace('[action]', response.action);
});
Step 3.
Have the JavaScript render your template.
The above can create a clear concise codebase that is easier to maintain and scale as the scope changes, rather than an individual request performing a change with embedded markup. This approach has worked quite well for me, also I feel it'll make you troubleshooting and definition easier, as the controller is handing an object back to your JavaScript instead of a markup / view data. Which will be a better finite control for the frontend and clear modifications in future.

Access ng-model created via an isolate scope in Angular

I have a simple directive using isolate scope which passes data to a scoped method from within the template:
app.directive("phone", function () {
return {
scope: {
dial: "&"
},
template: '<input type="text" ng-model="value">' +
'<br>' +
'<div class="button" ng-click="dial({message:value})">' +
'Call home!</div>',
controller: function($scope) {
console.log($scope);
}
};
});
Works fine. But I'd like to clear the input field after the alert has been completed. I'm having a hard time figuring out how I can access ng-model="value" on that input that is generated from within the directive. Any help?
Here's a plunk for you
change the template to this
template: '<input type="text" ng-model="value">' +
'<br>' +
'<div class="button" ng-click="dial({message:value}); value = \'\' ">' +
'Call home!</div>',
note that ng-click is changed to ng-click="dial({message:value}); value = \'\' "
this will sets the value to empty string after calling the dial() function.
here is the demo
or you can try something this, but seems like first one is the better one.

Getting data that is stored in several HTML elements

Let's say I have like 10 spans at the beginning of page's body with a structure like this:
(Just to reach it easier with Jquery, didn't know how to pass an array with JSON)
<span class="nike" data-img="logos/nike_logo.png" data-id="10"></span>
EDIT!: The spans can be changed into anything else, inputs etc. and the structure can be changed, only need to reach the img url and the id
Now, there's a Javascript function that goes through an array of words, and prints them out (search/suggestion/typeahead kind of thing). Example of the function:
function suggestionTemplate(context) {
switch(displayFn(context)) {
case "nike":
return "<p style='background-image:url(logos/nike_logo.png);'>"
+ displayFn(context) + "</p>";
break;
case "spalding":
return "<p style='background-image:url(logos/spalding_logo.png);'>"
+ displayFn(context) + "</p>";
break;
default:
return "<p>" + displayFn(context) + "</p>";
}
}
So the question, how can I make this function get all the span class names (or it can be data-name etc.), and then when displayFn(context) == one of the span's class names return the <p> element with that span's data-img ?
Please tell me if it's too hard to understand. I'm just making kind of an experiment and would like to know if this is possible to do.
If you change the structure like this, it would be much easier:
<span class="js-app-data" data-name="nike" data-img="logos/nike_logo.png" data-id="10"></span>
Then you can get all the data you defined like this:
data = Array.prototype.reduce.call(document.querySelectorAll('.js-app-data'), function(mem, e){
mem[e.dataset.name] = {
img: e.dataset.img,
id: e.dataset.id
}
return mem
}, {})
This will make data an object with structure like this:
{nike: {img:'logos/nike_logo.png', id:'10'}, ...}
From that, it should be easy to get the data from javascript.
function suggestionTemplate(context) {
if (context.name in data) {
return ("<p style='background-image:url("+ data[context.name].img +");'>"
+ context.name + "</p>");
}
}

calling javascript function with an argument from html

I am creating a html element using javascript:
html_info += '<div class="row"><h4>'+ i +'. Patient: '+ key +'</h4>Date of birth: '+info[0]+'<br> Occupation: '+info[1]+'<br> Date of Test: ' + info[2]+ '<script type="text/javascript"> draw_chart(' + patient_test_info[key].right_eye +' ); </script></div>';
document.getElementById('patient_display').innerHTML += html_info;
It creates the element properly and displays the information stored in info array, but call to the draw_chart() function fails. I printed the contents of patient_test_info[key].right_eye using console.log before passing it as an argument and the contents of the variable are fine. But checking the html elements displays that patient_test_info[key].right_eye is not passed correctly and are just empty objects.
<script type="text/javascript"> draw_chart([object Object],[object Object],[object Object],[object Object] ); </script>
My draw_chart() function must draw the chart in the same div as the one used to display info content. On calling it independently, it does not recognise the newly created div and thus does not display anything.
function draw_chart(data) {
var chart = d3.cloudshapes.barChart()
.width(800).height(800);
for(var i=0; i<data.length; i++) {
var temp_value = data[i];
d3.select("#row")
.datum(temp_value)
.call(chart);
}
}
What is the right way of calling the function using an argument ?
What adeneo is saying is if you call the function directly it should work, like this:
html_info += '<div class="row"><h4>'+ i +'. Patient: '+ key +'</h4>Date of birth: '+info[0]+'<br> Occupation: '+info[1]+'<br> Date of Test: ' + info[2] + draw_chart(patient_test_info[key].right_eye) + '</div>';
In response to your comment, I don't know how the chart code you're using works, but if d3.select("#row") is supposed to be targeting the div you've created, then you have 3 problems:
1) You're calling the function before the div has actually been added to the DOM. You need to do your html_info += '<div class="row">...' and ....innerHTML += html_info; first, then call draw_chart() afterwards.
2) You're targetting the <div class="row"> with select("#row") - assuming standard syntax, # denotes an ID, not a class. You either need to make it id="row" or select(".row") (a dot denotes a class). However, if you're planning to be able to call this function multiple times to add multiple charts, you'll need them to have unique names, otherwise you'll have trouble specifying that you want to use the latest row. I suggest using the ID and adding your "key" to it, assuming that's valid.
3) Again, I'm not sure how the chart code works, but I'd guess that it's going to replace the contents of the targetted div, not append to it, which means you'll lose the title from your row. If that's the case, I'd suggest putting another div inside your row div and giving it the name instead so you can target it, e.g. '<div><h4>'+ i +'. Patient: '+ key +'</h4>Date of birth: '+info[0]+'<br> Occupation: '+info[1]+'<br> Date of Test: ' + info[2] + '<div id="chart-' + key + '"></div></div>'. Once you've added that to the innerHTML, you can then call draw_chart(patient_test_info[key].right_eye, key) and modify your draw_chart method to use the key for the name, e.g. d3.select("#chart-" + key).

Categories

Resources