I have a lot of JavaScript code that creates HTML. For example:
function Reply(postId) {
if (!document.getElementById("reply" + postId)) {
$("#root" + postId).after("<div id='reply" + postId + "'><textarea cols='70' rows='8' style='margin-bottom: 10px'></textarea><br/><span style='margin-bottom: 30px'><input type='button' value='Submit' /><input type='button' value='Cancel' onclick='$(\"#reply" + postId + "\").remove();'/></span></div>");
}
}
How can I use jQuery to make this more readable? What's the best way to go about making new elements in JavaScript? Can I see an example?
The standard way is not to create html and then appending or prepending it but to create dom elements and adding them.
Please read this wonderful article on innerHTML vs DOM. by Tim Scarfe. It's very well written and and points and counter points.
You can read more on DOM here. and a Lot of information at the Gecko DOM Reference..
Sorry for only pasting links but it's a lot to go through.
If you want a quickstart. Look at this part of the Gecko DOM Reference. where they talk about createElement. Which is the magical method you're looking for.
First of all you need to know where you want to append the HTML, here you'll see the documentation: for "manipulation": here.
So lets say you want to create a paragraph inside a DIV with the ID "insideme".
$("#insideme").append("<p>I'm adding this</p>");
That is for creation, now lets say you want to insert an existent DIV:
$("#insideme").append($("div#existent"));
Check the documentation, there you'll find every function that is used to add before, after, inside, etc.
Hope it helped you.
If you want to fully use jquery functions, see if the following works (I have not tested it). Howerver, I would actually advise against this since I understand that direct HTML-string parsing/rendering is optimized a lot faster than a bunch of js function calls.
function Reply(postId) {
var rid = "reply"+postId;
if (!$('#'+rid)) {
$('#root'+postID)
.append("<div/>").attr('id',rid)
.append("<textarea/>").attr({'cols',70,'row',8,'style','margin-bottom: 10px'})
.after("<br/>")
.after("<span />").attr('style','margin-bottom: 30px');
.append("<input type='button' value='Submit' />")
.after("<input type='button' value='Cancel' />")
.onclick(function(){
$("#"+rid).remove();
});
}
}
Two ways:
Improve the formatting of that string
Install event handlers programatically
Eg:
function Reply(postId) {
if (!document.getElementById("reply" + postId)) {
// Generate HTML
var html = "<div id='reply" + postId + "'>";
html += "<textarea cols='70' rows='8' style='margin-bottom: 10px'></textarea><br/>";
html += "<span style='margin-bottom: 30px'>";
html += "<input type='button' value='Submit' />";
html += "<input class='cancelButton' type='button' value='Cancel' />";
html += "</span>";
html += "</div>";
// Insert into DOM
$("#root" + postId).after(html);
// Install event handlers
$("#root .cancelButton").bind("click", function() {
$("#reply" + postId).remove();
});
}
}
Using the DOM is methods directly is specifically discouraged by jQuery since it's so slow.
Update: As Chris says, move those styles to a CSS file to further tidy this up.
For such things I always use EJS.
http://embeddedjs.com/
Remove the angle brackets from your jQuery.
$( ".cancel" ).live( 'click', function() { $( this ).remove(); });
function reply( postId ) {
if ( $( "#reply" + postId ).size() == 0 ) {
var context = { postId: postId };
$( "#root" + postId ).after(new EJS({ url: '/template.ejs' }).render( context ));
}
}
Put them in a template with all their pointy little friends.
<div id="reply<%= postId %>">
<textarea cols="70" rows="8" class="reply_editor"></textarea>
<br>
<span class="ok_cancel">
<input type="button" value="Submit">
<input type="button" value="Cancel" class="cancel">
</span>
</div>
Inline styles are the devil's handiwork.
.reply_editor {
margin-bottom: 10px
}
.ok_cancel {
margin-bottom: 30px;
}
For extra legibility, don't attach handlers in your HTML. Attach them using jQuery.
Move the style stuff to a CSS file.
Remove the onclick event handler, replace it with a jQuery live.
Wrap the elementId in a variable.
$(".cancelButton").live("click", function(){$(this).remove();});
function Reply(postId) {
var elementId = "reply" + postId;
if (!document.getElementById(elementId )) {
var element = $("#" + elementId).after("<div id='" + elementId + "'><textarea cols='70' rows='8' ></textarea><br/><span ><input type='button' value='Submit' /><input type='button' value='Cancel' class='cancelButton'/></span></div>");
}
}
Related
I have a table in HTML where the ID is dynamically generated from a row counter:
$(table).find('tbody').append("<tr>name=\"tableRow\"</tr>"
+ "<td>"
+ "<select id=\"shapeSelect_" + rowCount + "></td>"
+ "<option onclick=\"sphereSelect()\" value=\"sphere\">Sphere</option>"
+ "<option onclick=\"cylinderSelect()\" value=\"cylinder\">Cylinder</option>"
+ "</select>"
+ "</td>"
+ "<td><input type=\"text\" id=\"altitude" + rowCount + "\"</td>"
+ "<td><input type=\"text\" name=\"maxAlt\" id=\"maxAltitude_" + rowCount + "></td>"
+ "</tr>"
I need maxAltitude to become disabled for input when sphere is selected. When cylinder is selected, it should become enabled for input.
Every example I find is pretty simple but requires knowing exactly what the ID is, where in my code it is dynamically generated. This is an example of what I'm finding:
$(#maxAltitude).prop("disabled", true);
How can I do this when maxAltitude will be something more like: maxAltitude_10? There may be 1-n rows in a table, and I need to specifically disable the max altitude in the row where the dropdown select was changed.
I've tried jQuery and javascript but can't seem to find a good way to do this:
<option onclick="shapeSelect()" value="sphere">Sphere</option>
<option onclick="shapeSelect()" value="cylinder">Cylinder</option>
function shapeSelect() {
var shapeSelects = document.getElementsByName("shapeSelect");
var maxAlts = document.getElementsByName("maxAlt");
for(var i = 0; i < shapeSelects.length; i++) {
switch(shapeSelects[i].value) {
case "sphere":
maxAlts[I].disabled = True;
break;
case "cylinder":
maxAlts[i].disabled = False;
}
}
}
With the above code I get: SyntaxError: unexpected token: identifier whenever shapeSelect() is fired.
I've modified the code as follows:
<table class="myTable" id="myTable"></table>
$(table).find('tbody').append("<tr>name=\"tableRow\"</tr>"
+ "<td>"
+ "<select id=\"shapeSelect_" + rowCount + "></td>"
+ "<option value=\"sphere\">Sphere</option>"
+ "<option value=\"cylinder\">Cylinder</option>"
+ "</select>"
+ "</td>"
+ "<td><input type=\"text\" id=\"altitude_" + rowCount + "\"</td>"
+ "<td><input class=\"maxAltitudeInput\" type=\"text\" id=\"maxAltitude_" + rowCount + "\" disabled></td>"
+ "</tr>"
$('#myTable').on('change','.shapeSelector',function(){
var shouldDisableInput = $(this).val() === 'sphere';
$(this).closest('tr').find('.maxAltitudeInput').attr('disabled',shouldDisableInput);
}
And still nothing happens when I change the shape selector dropdown.
EDIT:
Apologies on the naming mismatches. My dev machine is on an airgapped network and I was hand jamming the post here on Stack Overflow. The rowCount variable was being created and incremented in another function. I was trying to only put relevant code in the post for brevity.
I was missing a class from shapeSelector. That was the missing link. It works now!
jQuery actually makes this really easy by binding this to whichever element triggered an event.
For instance, instead of writing a generic function for when that value changes, you could use jQuery to bind an event listener to them:
$('#myTable').on('change','.shapeSelector',function(){
var shouldDisableInput = $(this).val() === 'sphere';
$(this).closest('tr').find('.maxAltitudeInput').attr('disabled',shouldDisableInput);
}
You'll notice a few things in this snippet:
The element we are binding the listener to is the table, not the individual row. That's because the row is dynamic, and we don't want to have to keep adding listeners every time we add a row. Instead we add it to the parent which is stable, but then we specify that we are interested in its children that match ".shapeSelector"
The listener relies on class names, not IDs, since we want to match multiple copies of them, not just a specific one. So you'd need to add those class names or a similar way of matching more than one item
Inside the callback function that runs, you'll notice a couple uses of this. jQuery has bound that to the element that triggered the event listener, in this case, the <select> control. So when we use this, we have to think of it from that perspective. We can get its value by $(this).val(), we can find its parentt with $(this).parent(), etc. In this case, I'm travelling up to the nearest tr, then from there down to that tr's input that I want to disable. You'd need to adjust a little depending on your dom.
Also note that this is a DOM element, not a jQuery result. That's why when we want to run more jQuery commands on it, we have to put it in $() again.
That's how I'd approach it. We don't have your entire code here, so you'll have to adjust a bit, but hopefully that pushes you off in the right direction.
EDIT
To be honest, there were a lot of naming mismatches and things that didn't line up. For instance, you were attempting to append onto a tbody tag, but that tag didn't exist. You were using a rowCount variable, but didn' ever set that up or increment it. The select tag sill didn't have the class name you were trying to use.
I suggest you look at your code piece by piece, ask yourself what you're telling the browser to do, and then do that instruction in your mind to make sure the computer can do it.
HTML:
<table class="myTable" id="myTable"><tbody></tbody></table>
JavaScript:
var rowCount = 0;
function addRow(){
$('.myTable tbody').append(`<tr name="tableRow">
<td>
<select class="shapeSelector" id="shapeSelect_${rowCount}">
<option value="sphere">Sphere</option>
<option value="cylinder">Cylinder</option>
</select>
</td>
<td><input type="text" id="altitude_${rowCount}" /></td>
<td><input class="maxAltitudeInput" type="text" id="maxAltitude_${rowCount}" disabled></td>"
</tr>`);
rowCount++;
}
$('.myTable').on('change','.shapeSelector',function(){
var shouldDisableInput = $(this).val() === 'sphere';
$(this).closest('tr').find('.maxAltitudeInput').attr('disabled',shouldDisableInput);
});
addRow();
addRow();
addRow();
https://jsfiddle.net/32vnjq81/
I am wanting to try and pass record.ItemID to my onclick = buy() function. But I am getting errors like "Uncaught SyntaxError: Unexpected end of input"
I have tried \"record.ItemID \" but that of course just passes the literal string of result.name
I have also tried (\'' + record.ItemID + '\') but get the same Syntax error
function showShop(items) {
let tableContent = "<tr class='orderTitle'><td =imgTable></td><td id = contentTable ></td></tr>\n";
let odd = true;
const addRecord = (record) => {
tableContent += odd ? "<tr class='orderOdd'>" : "<tr class='orderEven'>";
odd = !odd;
tableContent += "<td>" + "<img id = image src="+ "http://redsox.uoa.auckland.ac.nz/ms/MuseumService.svc/shopimg?id=" + record.ItemId + " />" + "</td><td id = content>" + record.Description + "<td><button onclick='buy("+ record.ItemId +")'/> Buy </button></td>";
}
items.forEach(addRecord)
document.getElementById("shop").innerHTML = tableContent;
}
function buy(item){
window.open('http://redsox.uoa.auckland.ac.nz/mss/Service.svc/buy?id='+ item,'_self');
}
I'm not sure if this will solve your problem but it looks like you're mixing up 's and "s.
onclick='buy('record.ItemId')'
You are terminating the onclick attribute right after buy(.
You may need to do something like:
onclick='buy(" + record.ItemId + ")'
Generally speaking though, if you have to build up HTML in strings, you are better off string interpolation. It makes it easier to read and is less prone to these types of issues.
Example:
const html = `<button onclick="buy(${record.ItemId})">Click</button>`;
It looks like you're trying to build up some HTML content to put into a table, and you want some behaviour attached to a button inside the table so that it opens a new window when you click on it.
There are a number of different approaches to what you're trying to do which would be safer in production code, so while you've got some answers to your specific question, please consider these alternative approaches which are more idiomatic:
You could use a link (<a>) instead of a button, and use CSS to make the link look like a button. This avoids needing a click handler at all.
You could use data attributes to store the record in a safe way, then access it from the click event, e.g. e.target.dataset.recordId.
You could use jQuery or a similar toolkit to create the button, then attach a function to the button as a click handler.
When you create HTML directly like you are doing in your question, you're opening your code up to code injection, where someone malicious could craft data that could steal private information from users of your site. It's much safer to use a library to construct your HTML directly rather than building it up in strings.
Really you're much better off separating out your inline JS and using event listeners to target classes on your elements.
Here's a quick example to show you how you might achieve that:
const records = [{ itemId: 1 }, { itemId: 2 }, { itemId: 3 }];
const imgRoot = 'https://dummyimage.com/30x30/676767/fff.png?id=';
// `map` iterates over the array and produces one element of HTML per record
// We use a class on the button to identify it, and a data attribute
// button to hold the itemId
const html = records.map(({ itemId }) => {
return `
<div class="itemWrapper">
<img class="item" src="${imgRoot}${itemId}" />
<button data-itemid="${itemId}" class="buyRecord">Buy record</button>
</div>
`;
});
document.querySelector('.root').innerHTML = html.join('');
// We grab the buttons and iterate over them attaching
// event listeners that call `handleBuy` when the button is clicked
const buyButtons = document.querySelectorAll('.buyRecord');
buyButtons.forEach(button => button.addEventListener('click', handleBuy, false));
function handleBuy(e) {
// Destructure the itemid from the dataset of the button
// click event
const { target: { dataset: { itemid } } } = e;
console.log(itemid);
}
<div class="root" />
Documentation
map
Data attributes
Template literals
Destructuring assignment
The General format of onclick is
onclick="function_name(variable)"
For this case you can do something like this:
tableContent += '<td>' + '<img id = image src="http://redsox.uoa.auckland.ac.nz/ms/MuseumService.svc/shopimg?id=' + record.ItemId + '" /></td><td id="content">' + record.Description + '<td><button onclick="buy('+record.ItemId+')"> Buy </button></td>';
I have the following xml file with the controls information to render into HTML page.
The contents are like:
<control type="panel">
<panel id="p1">
<button id="b1">
<value>TEST</value>
</button>
<textbox id="t1">
<text>HELLO</text>
</textbox>
</panel>
<control>
This has to rendered on the fly into a div with a panel containing one button and one textbox.The contents of xml are known only at runtime.It can be anything like only a button or a dropdown list information.How would one go about approaching this problem.A generic algorithm(probably using jquery) would be really helpful.
For something generic, your XML would need to be generic or conformant to a convention along the lines of "XML to HTML form convention". I know of no such thing. :)
It seems easy enough to handle. Here's an example of how I'm handling your XML example.
$(function() {
// Data setup (I assume you'd be getting this from AJAX)
var xml = '<control type="panel">' +
'<panel id="p1">' +
'<button id="b1">' +
'<value>TEST</value>' +
'</button>' +
'<textbox id="t1">' +
'<text>HELLO</text>' +
'</textbox>' +
'</panel>' +
'</control>';
// Convert to jQuery XML object
var xml = $($.parseXML(xml));
// Set parent
var parent = $('#control');
// Handle elements
xml.find('panel').children().each(function() {
var tag = $(this)[0].tagName;
switch (tag) {
case 'button':
parent.append('<button id="' +
$(this).attr('id') + '">' +
$(this).find('value').text() +
'</button>');
break;
case 'textbox':
parent.append('<input type="text" id="' +
$(this).attr('id') +
'" value="' +
$(this).find('text').text() +
'" />');
break;
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="control"></div>
Of course, you could modify this as necessary, add new element handling, change the way it's added to the parent, etc. etc. I hope it helps.
<div id="tagTree1" class="span-6 border" style='width:280px;height:400px;overflow:auto;float:left;margin:10px; '>
<a class="tabheader" style="font-size:large">Data Type</a><br />
<div class="pane">Refine search by Data Type</div>
</div>
Above div(tagTree1) is present in a division ad.
<div id="newtagTree1" class="span-6 border" style='width:200px;height:400px;overflow:auto;float:left'>
<a class="tabheader"><strong>Geographic Location</strong></a><br />
<div class="pane"><strong>Refine search by geographic location</strong></div>
</div>
newTagTree1 division is present in another division search. But both have the same functionality to generate children divisions within them, which is written in a js file. All the children division generated dynamically in js file. Both of them uses same function to generate children divs. I am facing problem when i am using them in same page. If one works fine then the other doesn't. Can any one say me about the mistake i am doing in this?
Thanks in advance.
$.getJSON('/api/TagsApi/Children?id=800002', function (data) {
//$(tagDiv).empty();
$.each(data, function (i, item) {
$("#tagTree1").append(tagTabBuilder(item));
});
$("#tagTree1").tabs("#tagTree1 div.pane", { api: true, tabs: 'a', effect: 'slide', onClick: buildChildren, initialIndex: 0 });
});
function tagTabBuilder(tagData) {
var str = "<input type='checkbox' name='tagchkbox[]' value='" + tagData.ID + "' onClick='startQuery()' /><a class='tabheader '>" + tagData.NAME;
if (tagData.count == 0) {
str += " (<span class='el_count' id='t" + tagData.ID + "'>" + tagData.count + "</span>)" + "</a><br/>";
} else {
str += " (<span class='el_count' id='t" + tagData.ID + "'><strong>" + tagData.count + "</strong></span>)" + "</a><br/>";
}
str += "<div id='tid-" + tagData.ID + "' class='pane tag'><!--Loading subtags. . .<img src='/assets/modules/gaiaModule/shared/images/load-small.gif' />--></div>";
return str;
}
My guess would be that when they generate child divs, they're generating them with the same ID scheme. Thus, either of them can generate child divs just fine by itself, but when both of them are included, there is ID collision. The answer is to modify the child generation code to, for example, include the id of the parent div as the first portion of the id of the child div.
Alternately, if you dont' need them for other portions of the javascript, leave the child div ids out entirely. In general, I find that it's better to avoid the id attribute in generated nodes, and instead use classes or the like.
I'm building a small app with a few modal dialog windows. The windows require a tiny bit of HTML. I've hard coded the window HTML in the javascript library but am not thrilled with this solution. Is there a more elegant way to do this? It seems that JavaScript doesn't have multi line strings/heredoc syntax.
var html = "<div id='email_window'><h2>Email Share</h2><div>";
html = html + "<form action='javascript:emailDone();' method='post'>";
html = html + "<div><label for='to'>To</label><input id='to' type='text'></div>";
html = html + "<div><label for='from'>From</label><input id='from' type='text' value='" + email + "'></div>";
html = html + "<div><label for='subject'>Subject</label><input id='subject' type='text' disabled='disabled' value='" + subject + "'></div>";
html = html + "<div><label for='body'>Body</label><input id='body' type='text' disabled='disabled' value='" + body + "'></div>";
html = html + "<div><input type='submit' value='Send'><input type='button' value='Cancel' onClick='javascript:$.fancybox.close();'></div>";
html = html + "</form></div>";
$("#data").html(html);
Added to clarify the original message-
Any solution can't use Ajax/XHR to pull in the template file because the javascript library will be on a different domain that the html file it's included in
It's a little like ShareThis. The library will be included on a number of different sites and attached to the onClick event of any anchor tag inside divs with attribute sharetool="true".
For example:
http://www.bar.com - index.html
<html>
...
<script type="text/javascript" src="http://www.foo.com/sharetool.js"></script>
...
<body>
<div sharetool="true">
</div>
...
</html>
You can include the HTML as regular markup at the end of the page, inside an invisible div. Then you're able to reference it with jQuery.
You then need to programmatically set your variable fields (email, subject, body)
<div id='container' style='display: none;'>
<div id='your-dialog-box-contents'>
...
...
</div>
</div>
<script type='text/javascript'>
$("#from").val(from);
$("#subject").val(subject);
$("#body").val(body);
$("#data").html($("#your-dialog-box-contents"));
</script>
Templates. Pick your poison
EJS
jQuery templates (nb: development discontinued)
underscore templates
mustache
jResig micro templates
Either inline them as script blocks or load them using ajax as external resources.
I personally use EJS as external template files and just get EJS to load them and inject them into a container with json data bound to the template.
new EJS({
url: "url/to/view"
}).update('html_container_name', {
"foobar": "Suprise"
});
And then view files use generic view logic.
// url/to/view
<p> <%=foobar %></p>
For multiline strings (no frameworks, just javascript) there are several solutions. See my answer to this SO Question. You could combine that with some simple templating:
String.prototype.template = String.prototype.template ||
function (){
var args = Array.prototype.slice.call(arguments)
,str = this
,i=0
;
function replacer(a){
var aa = parseInt(a.substr(1),10)-1;
return args[aa];
}
return str.replace(/(\$\d+)/gm,replacer)
};
//basic usage:
'some #1'.template('string'); //=> some string
//your 'html' could look like:
var html =
[ '<form action="javascript:emailDone();" method="post">',
' <div><label for="to">To</label>',
' <input id="to" type="text"></div>',
' <div><label for="from">From</label>',
' <input id="from" type="text" ',
' value="$0"></div>',
' <div><label for="subject">Subject</label>',
' <input id="subject" type="text" disabled="disabled" ',
' value="$1"></div>',
' <div><label for="body">Body</label>',
' <input id="body" type="text" disabled="disabled" ',
' value="$2"></div>',
' <div><input type="submit" value="Send"><input type="button" ',
' value="Cancel" ',
' onClick="javascript:$.fancybox.close();"></div>',
'</form>'
] .join('').template(email, subject, body);
Personally I like building DOM trees like this:
$('#data').html(
$('<div/>', {
id: 'email_window',
html: $('<h2/>', {
html: 'Email Share'
})
}).after(
$('<form/>', {
action: 'javascript:emailDone();',
method: 'post',
html: $('<div/>', {
html: $('<label/>', {
for: 'to',
html: 'To'
}).after($('<input/>', {
id: 'to',
type: 'text'
}))
}).after(
... etc
)
})
)
);
There is 2 solutions tto your problem:
- An alternative to the heredoc Syntax in javascript is to escape the newline char with \ :
var tpl = "hello\
stackoverflow\
World !";
The char is escaped so ignored, and it wont take place in the resulting string.
You can also create a plain html file with your template, and in your js script you create a hidden iframe and load the crossdomain html template. You can now access the document object of the iframe and retreive body.innerHTML. In theory! I Didn't tested this solution yet....
You're right, JS doesn't have heredocs or multi-line strings. That said, the usual approach to this is to have the HTML in...the HTML, and show or hide it as appropriate. You're already using jQuery, so you're most of the way there:
<div style="display:none;">
<form method='post' class="email">
<input id='from' type='text'> <!-- other form fields omitted for brevity -->
</form>
<form method='post' class="facebook"></form> <!-- again, omitted for brevity -->
</div>
Then, you can populate the form and toss it in the right spot:
$('#data').html($('form.email').find('input#from').val(email).end().html());
Cook.js
div([
button({click:[firstEvent, secondEvent]},
'You can bind (attach) events on the fly.'),
p('Here are some popular search engines'),
ul([
li([
a('Google', {href:'http://www.google.com'})
]),
li([
a('Bing', {href:'http://www.bing.com'})
]),
li([
a('Yahoo', {href:'http://www.yahoo.com'})
])
])
]);
how it works
Objects = Attribute & Events
-> {href:'facebook.com', src:'static/cat.gif', ng-bind:'blah'}
String = Text or Html
-> 'hello world'
Array = Elements
-> [a('hello world', {href:'facebook.com'}), img({src:'static/cat.gif'})]
more on cook.js!