I generate the numbering of my headers and figures with CSS's counter and content properties:
img.figure:after {
counter-increment: figure;
content: "Fig. " counter(section) "." counter(figure);
}
This (appropriate browser assumed) gives a nice labelling "Fig. 1.1", "Fig. 1.2" and so on following any image.
Question: How can I access this from Javascript? The question is twofold in that I'd like to access either the current value of a certain counter (at a certain DOM node) or the value of the CSS generated content (at a certain DOM node) or, obviously, both information.
Background: I'd like to append to links back-referencing to figures the appropriate number, like this:
<a href="#fig1">see here</h>
------------------------^ " (Fig 1.1)" inserted via JS
As far as I can see, it boils down to this problem:
I could access content or counter-increment via getComputedStyle:
var fig_content = window.getComputedStyle(
document.getElementById('fig-a'),
':after').content;
However, this is not the live value, but the one declared in the stylesheet. I cannot find any interface to access the real live value. In the case of the counter, there isn't even a real CSS property to query.
Edit: Digging deeper and deeper through the DOM specs, I found the DOM Level 2 Style Counter interface. This seems to a) allow access to the current counter value and b) be implemented in Firefox, at least. However, I have no idea on how to use it. My current approach died tragically after this Firebug output:
// should return a DOM 2 Counter interface implementation...
window.getComputedStyle(fig_a_element, ':after')
.getPropertyCSSValue("counter-increment")[0]
.getCounterValue();
[Exception... "Modifications are not allowed for this document" code: "7"
nsresult: "0x80530007 (NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)"
location: "http://localhost/countertest.html Line: 71"]
Any idea, how this could be brought to life would be highly appreciated.
Edit 2: Apparently I misinterpreted the Counter object of DOM Level 2 Style. It, too, has no property to return the current counter value. This makes the above approach invalid.
New approach: Is there a possibility to read the content of a pseudo-element via the DOM? That is, can I select the pseudo-element (treeWalker comes to mind) and then get its nodeValue? (If you start to type 'jQuery' now, please reconsider to change that term into 'Sizzle'...)
I cannot find any interface to access the real live value. [of the counter]
Yeah. I don't think there is one. Sorry.
The only thing I can think of would be to go through every element (including its :before/:after pseudo-elements) before the element in the document, looking for counters and adding up how many there are.
Obviously that's hideous. If you're going to try to reproduce the browser's own counter mechanism it would probably be easier (and much more compatible, given IE<=7's lack of counter/content support) to just replace it with your own script-based counters. eg. something along the lines of:
this
<div class="counter level=0">...</div>
<img id="prettypicture" class="counter level=1" alt="ooo, pretty"/>
window.onload= function() {
var counters= Node_getElementsByClassName(document.body, 'counter');
var indices= [];
for (var counteri= 0; counteri<counters.length; counteri++) {
var counter= counters[counteri];
var level= Element_getClassArgument(counter, 'level');
while (indices.length<=level)
indices.push(0);
indices[level]++;
indices= indices.slice(level+1);
var text= document.createTextNode('Figure '+indices.join('.'));
counter.parentNode.insertBefore(text, counter.nextSibling);
if (counter.id!=='') {
for (var linki= document.links.length; linki-->0;) {
var link= document.links[i];
if (
link.hostname===location.hostname && link.pathname===location.pathname &&
link.search===location.search && link.hash==='#'+counter.id
) {
var text= document.createTextNode('('+indices.join('.')+')');
link.parentNode.insertBefore(text, link.nextSibling);
}
}
}
}
};
read this:
http://www.w3.org/TR/CSS2/generate.html#propdef-content
Generated content does not alter the
document tree. In particular, it is
not fed back to the document language
processor (e.g., for reparsing).
https://developer.mozilla.org/en/CSS/Getting_Started/Content
Content specified in a stylesheet does not become part of the DOM.
so for this reason the getComputedStyle will not work in this case; i think the only way, is to perform a classic loop through as someone has described below!
I would port your css to Javascript, this enables you to get the figure caption and also you get greater browser coverage. Using jQuery you'd do something like this:
$(function() {
var section = 0;
$(".section").each(function() {
section++;
var figure = 0;
$(this).find("img.figure").each(function() {
figure++;
var s = "Fig. " + section + "." + figure;
$(this).attr({alt:s}).after(s);
});
});
});
Then you could do:
<div class="section">blabla <img id="foo" src="http://www.example.com/foo.jpg"></div>
<p>Lorem ipsum dolor sit amet see here</p>
<script type="text/javascript">
$(function() {
$("a.figurereference").each(function() {
var selector = $(this).attr("href");
var $img = $(selector);
var s = $(this).text() + " (" + $img.attr("alt") + ")";
$(this).text(s);
});
});
</script>
Though I agree that doing this using CSS would be very neat.
I cannot find any interface to access the real live value.
If you can't get the value from window.getComputedStyle, it seems that it would be impossible.
More importantly, while it can do it, I think this might be abusing CSS since you're breaking the barrier between content and presentation at this point.
Check out this related question What are good uses of css “Content” property?
Related
I generate the numbering of my headers and figures with CSS's counter and content properties:
img.figure:after {
counter-increment: figure;
content: "Fig. " counter(section) "." counter(figure);
}
This (appropriate browser assumed) gives a nice labelling "Fig. 1.1", "Fig. 1.2" and so on following any image.
Question: How can I access this from Javascript? The question is twofold in that I'd like to access either the current value of a certain counter (at a certain DOM node) or the value of the CSS generated content (at a certain DOM node) or, obviously, both information.
Background: I'd like to append to links back-referencing to figures the appropriate number, like this:
<a href="#fig1">see here</h>
------------------------^ " (Fig 1.1)" inserted via JS
As far as I can see, it boils down to this problem:
I could access content or counter-increment via getComputedStyle:
var fig_content = window.getComputedStyle(
document.getElementById('fig-a'),
':after').content;
However, this is not the live value, but the one declared in the stylesheet. I cannot find any interface to access the real live value. In the case of the counter, there isn't even a real CSS property to query.
Edit: Digging deeper and deeper through the DOM specs, I found the DOM Level 2 Style Counter interface. This seems to a) allow access to the current counter value and b) be implemented in Firefox, at least. However, I have no idea on how to use it. My current approach died tragically after this Firebug output:
// should return a DOM 2 Counter interface implementation...
window.getComputedStyle(fig_a_element, ':after')
.getPropertyCSSValue("counter-increment")[0]
.getCounterValue();
[Exception... "Modifications are not allowed for this document" code: "7"
nsresult: "0x80530007 (NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)"
location: "http://localhost/countertest.html Line: 71"]
Any idea, how this could be brought to life would be highly appreciated.
Edit 2: Apparently I misinterpreted the Counter object of DOM Level 2 Style. It, too, has no property to return the current counter value. This makes the above approach invalid.
New approach: Is there a possibility to read the content of a pseudo-element via the DOM? That is, can I select the pseudo-element (treeWalker comes to mind) and then get its nodeValue? (If you start to type 'jQuery' now, please reconsider to change that term into 'Sizzle'...)
I cannot find any interface to access the real live value. [of the counter]
Yeah. I don't think there is one. Sorry.
The only thing I can think of would be to go through every element (including its :before/:after pseudo-elements) before the element in the document, looking for counters and adding up how many there are.
Obviously that's hideous. If you're going to try to reproduce the browser's own counter mechanism it would probably be easier (and much more compatible, given IE<=7's lack of counter/content support) to just replace it with your own script-based counters. eg. something along the lines of:
this
<div class="counter level=0">...</div>
<img id="prettypicture" class="counter level=1" alt="ooo, pretty"/>
window.onload= function() {
var counters= Node_getElementsByClassName(document.body, 'counter');
var indices= [];
for (var counteri= 0; counteri<counters.length; counteri++) {
var counter= counters[counteri];
var level= Element_getClassArgument(counter, 'level');
while (indices.length<=level)
indices.push(0);
indices[level]++;
indices= indices.slice(level+1);
var text= document.createTextNode('Figure '+indices.join('.'));
counter.parentNode.insertBefore(text, counter.nextSibling);
if (counter.id!=='') {
for (var linki= document.links.length; linki-->0;) {
var link= document.links[i];
if (
link.hostname===location.hostname && link.pathname===location.pathname &&
link.search===location.search && link.hash==='#'+counter.id
) {
var text= document.createTextNode('('+indices.join('.')+')');
link.parentNode.insertBefore(text, link.nextSibling);
}
}
}
}
};
read this:
http://www.w3.org/TR/CSS2/generate.html#propdef-content
Generated content does not alter the
document tree. In particular, it is
not fed back to the document language
processor (e.g., for reparsing).
https://developer.mozilla.org/en/CSS/Getting_Started/Content
Content specified in a stylesheet does not become part of the DOM.
so for this reason the getComputedStyle will not work in this case; i think the only way, is to perform a classic loop through as someone has described below!
I would port your css to Javascript, this enables you to get the figure caption and also you get greater browser coverage. Using jQuery you'd do something like this:
$(function() {
var section = 0;
$(".section").each(function() {
section++;
var figure = 0;
$(this).find("img.figure").each(function() {
figure++;
var s = "Fig. " + section + "." + figure;
$(this).attr({alt:s}).after(s);
});
});
});
Then you could do:
<div class="section">blabla <img id="foo" src="http://www.example.com/foo.jpg"></div>
<p>Lorem ipsum dolor sit amet see here</p>
<script type="text/javascript">
$(function() {
$("a.figurereference").each(function() {
var selector = $(this).attr("href");
var $img = $(selector);
var s = $(this).text() + " (" + $img.attr("alt") + ")";
$(this).text(s);
});
});
</script>
Though I agree that doing this using CSS would be very neat.
I cannot find any interface to access the real live value.
If you can't get the value from window.getComputedStyle, it seems that it would be impossible.
More importantly, while it can do it, I think this might be abusing CSS since you're breaking the barrier between content and presentation at this point.
Check out this related question What are good uses of css “Content” property?
I am currently creating a desktop app using tide sdk. All of my database information is stored into Parse.com (a serverless database). What I am trying to do is to take the array of the information I queried from Parse (in javascript) and insert it into a table. I am really having a hard time getting used to not using document.write() for my desktop application.
I want the end result to look like:
This is what I started with:
var contactNameArray = [];
var contactNumberArray= [];
var CM = Parse.Object.extend("ContactMenu");
var queryContact = new Parse.Query(CM);
queryContact.ascending("ContactName");
queryContact.find({
success: function(results4) {
alert("Successfully retrieved " + results4.length + " entries.");
// Do something with the returned Parse.Object values
// document.write('<table border="1" cellspacing="1" cellpadding="5">');
for (var i = 0; i < results4.length; i++) {
var object4 = results4[i];
contactNameArray[i] = object4.get('ContactName');
contactNumberArray[i] = object4.get('ContactNumber');
// document.write("<tr><td>Number " + i + " is:</td>");
//document.write("<td>" + contactNameArray[i] + "</td></tr>");
}
//document.write('</table>');
},
error: function(error) {
alert("Error: " + error.code + " " + error.message);
}
});
After doing some research I cam upon this bit of code from http://www.w3schools.com/jsref/dom_obj_table.asp which wrote: the correct response on the bottom of the left handed corner of the screen. (Kind of strange in my opinion). In code how can I better position this table to be in the center for my screen? Is there a way to center this table in javascript?
function generate_table() {
var x = document.createElement("TABLE");
x.setAttribute("id", "myTable");
document.body.appendChild(x);
var y = document.createElement("TR");
y.setAttribute("id", "myTr");
document.getElementById("myTable").appendChild(y);
var z = document.createElement("TD");
for(var i = 0; i< query4.length; i++){
var t = document.createTextNode(contactNameArray[i]);
z.appendChild(t);
var m = document.createTextNode(contactNumberArray[i]);
z.appendChild(m);
}
document.getElementById("myTr").appendChild(z);
}
So I have already figured out how to put the information I want into an array. I am just having a hard time putting this information into a table that is correctly positioned. Thank you in advance. If you need to see any more of my code, then just let me know. If I am unclear, please let me know what I should explain. Thank you!!!
There are several ways to do this. But from what you already have the simplest is to use innerHTML:
queryContact.find({
success: function(results4) {
var html = "";
alert("Successfully retrieved " + results4.length + " entries.");
// Do something with the returned Parse.Object values
html += '<table border="1" cellspacing="1" cellpadding="5">';
for (var i = 0; i < results4.length; i++) {
var object4 = results4[i];
contactNameArray[i] = object4.get('ContactName');
contactNumberArray[i] = object4.get('ContactNumber');
html += "<tr><td>Number " + i + " is:</td>";
html += "<td>" + contactNameArray[i] + "</td></tr>";
}
html += '</table>';
document.body.innerHTML += html;
},
error: function(error) {
alert("Error: " + error.code + " " + error.message);
}
});
As for centering the table on the page the best way is to use CSS. Unfortunately centering anything in CSS is a bit of a hack. There are several ways to do it. See the answers to this question for all the ways of doing it: How to horizontally center a <div> in another <div>?. Note: scroll through the answers, not just read the top one. There really are a lot of ways to do this and some may not work for you.
A few notes about innerHTML:
Although innerHTML looks like a variable it actually behaves more like a function. Specifically it invokes the HTML compiler of the browser. So if you pass it incomplete tags like:
someDiv.innerHTML += '<table>';
it will see that as an incomplete 'table' tag and deals with it the way the browser usually does when it sees an incomplete 'table' tag. For some browsers that means removing the table from the DOM. For others that means immediately inserting a closing </table> tag to make it valid. What this means is that when you later append the closing tag like this:
someDiv.innerHTML += '</table>';
what happens is that the browser will think you did this:
<table></table></table>
^ ^
| |_________ what you're trying to insert
|
auto inserted by the browser earlier
and deal with it the way browsers usually do - consider that tag invalid and discard it.
So you need to pass innerHTML well-formed html which is why I created the table structure in a string then append it to the DOM with innerHTML.
A lot of people consider innerHTML stylistically bad since you're doing DOM manipulation with strings. Also because innerHTML was not originally part of any standard and was a proprietary feature of IE. Since it's not part of any standard there's no real agreement between different browsers for how it should work. Having said that, it's probably the most cross-bowser compatible method of manipulating DOM because it's the most widely implemented (even on really old browsers).
Read the documentation of the DOM API for more info on how to do it "properly": https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
As mentioned by others in the comment to your question, there are also libraries out there that would make your life easier for DOM manipulation. There are procedural libraries like jQuery that wraps the often clunky DOM API and cross-browser issues into a nice to use API. There are also templating library like handlebars that allows you to write the HTML fragments as templates to be processed and inserted into the DOM.
I suggest getting comfortable with how the DOM works by reading the DOM documentation and follow some tutorial and also look at DOM manipulation libraries like jQuery or YUI or underscore to get a better feel of javascript.
Paraphrasing Douglas Crockford, you wouldn't start to program in C or Java without learning the language first. Similarly javascript is a full-featured language. Take some time to learn it and not just assume that "it works like [insert a language you know]". There are many features of javascript like closures and asynchronous functions that will trip you up. Similarly the DOM has many behaviors that may work differently from your assumptions of how HTML works.
But it is a lot to take in. So for now use innerHTML with care being aware of its limitations. Also look at document.getElementById() which is the second most used DOM API for beginners. It allows you to insert your html anywhere in the document using innerHTML. Not just the end of the document.
From reading your questions I deduced the following...
How can I center my table using javascript.
I am having an issue getting my information into a table.
Typically it is not a good idea to do styling within your javascript. While it may seem nice to handle such things conveniently within your jscript, it can end up blowing up your code if not used with moderation. Your best bet would be to write some css, perhaps a generic class that can center an element to a page, and then apply this class to the table element. No Javascript needed, and it makes your code more modular to boot!
Here is a hacky bit of centering code that has worked for me to center a registration form div (Height and Width can be adjusted however you like, use of pixels is not a must.):
body > #register {
margin: auto;
position: absolute;
text-align: center;
height: 156px;
width: 160px;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
For the issues you are experiencing adding information to your table, without knowing what errors or exact output your are seeing, all I can do is go off what I can see in the code. Your generate table function has a few areas I noticed....
The function creates a table, sets an id to it, and appends it to the document, it then creates a new row, setting an id to it as well, it then appends the new row to the table. Then a cell is created.
Here is where I see a problem...
You then jump into a for loop limited by the length of query4 (I'll assume this is the query containing your contact info) and create text nodes, appending them to z (the cell) each iteration, if I am not mistaken that would actually result in the cell in the first(and only) row getting blown up with all your query info. What should be happening is the for loop adds the name and number to its own cells in a NEW row each iteration. This would be your psuedocode...
Create table
Start for loop over contact info for each item...
Create new row
Create new cells
add info to respective cells
append cells to row
append row to table
rinse and repeat
Based on what you have, here is a rough untested representation of what I am suggesting, I built it out of your own code, but it could be done in several ways really...
function generate_table() {
// Create our table
var table = document.createElement("TABLE");
document.body.appendChild(table);
for(var i = 0; i < query4.length; i++) {
// Set up an awesome new row.
var row = document.createElement("TR");
// Set up awesome new cells.
var nameCell = document.createElement("TD");
var numberCell = document.createElement("TD");
// Instantiate variables to hold our data.
var name = document.createTextNode(contactNameArray[i]);
var number = document.createTextNode(contactNumberArray[i]);
// Add values to cells
nameCell.appendChild(name);
numberCell.appendChild(number);
// Add cells to row.
row.appendChild(name);
row.appendChild(number);
// Build out awesome row.
table.appendChild(row);
}
}
I did a couple things here, first off some variable renaming, descriptive variable names do wonders for code readability and maintenance later on. (Check out the book "Clean Code", it talks on this at length, it changed the way I look at code virtually overnight.)
One other thing, I assume the query4 variable is being set up in the global scope, that will work, but it's typically good to try an keep the global space clear if and when you can. (See Douglas Crockfords "Javascript: The Good Parts", another great book on Javascript that really helped me learn the language.) Maybe consider passing the data to the generate table function, and calling the function in the callback of the parse data return?
Anyway, that is my "brief" two cents, hope it helps!
Good luck.
Imagine I have a loaded HTML page which has been already affected by javascript adding/deleting dynamic elements or new classes/attributes/id to elements while initializing(e.g: original source code [html] tag has no classes, after javascript loads [html] tag has class="no-responsive full-with"). Imagine after that I add/amend some id values manually (through my app). And imagine I need to be able to save in database the original source code (without any amends) but with the id attributes I added manually.
Basically I need to add a given id attribute to an element within the source code of an HTML, loaded through PHP.
Do you guys have any idea of how to do such a thing?
There's no simple solution here. The exact nature of the complex solution will be determined by your full set of requirements.
Updated Concept
You've said that in addition to changing things, you'll also be adding elements and removing them. So you can't relate the changed elements to the originals purely structurally (e.g., by child index), since those may change.
So here's how I'd probably approach it:
Immediately after the page is loaded, before any modifications are made, give every element in the a unique identifier. This is really easy with jQuery (and not particularly hard without it):
var uniqueId = 0;
$("*").attr("data-uid", function() {
return ++uniqueId;
});
Now every element on the page has a unique identifier. Next, copy the DOM and get a jQuery wrapper for it:
var clone = $("html").clone();
Now you have a reliable way to relate elements in the DOM with their original versions (our clones), via the unique IDs. Allow the user to make changes.
When you're ready to find out what changes were made, you do this:
// Look for changes
clone.find("*").addBack().each(function() {
// Get this clone's unique identifier
var uid = $(this).attr("data-uid");
// Get the real element corresponding to it, if it's
// still there
var elm = $("[data-uid=" + uid + "]")[0];
// Look for changes
if (!elm) {
// This element was removed
}
else {
if (elm.id !== this.id) {
// This element's id changed
}
if (elm.className !== this.className) {
// This element's className changed
}
// ...and so on...
}
});
That will tell you about removed and changed elements. If you also want to find added elements, just do this:
var added = $(":not([data-uid])");
...since they won't have the attribute.
You can use the information in clone to reconstruct the original DOM's string:
clone.find("[data-uid]").addBack().removeAttr("data-uid");
var stringToSend = clone[0].outerHTML;
(outerHTML is supported by any vaguely modern browser, the latest to add it was Firefox in v11.)
...and of course the information above to record changes.
Live proof of concept
HTML:
<p class="content">Some content</p>
<p class="content">Some further content</p>
<p>Final content</p>
<input type="button" id="makeChange" value="Make Change">
<input type="button" id="seeResults" value="See Results">
JavaScript:
// Probably unnecessary, but I wanted a scoping
// function anyway, so we'll give the parser time
// to completely finish up.
setTimeout(function() {
// Assign unique identifer to every element
var uniqueId = 0;
$("*").attr("data-uid", function() {
return ++uniqueId;
});
// Clone the whole thing, get a jQuery object for it
var clone = $("html").clone();
// Allow changes
$("#makeChange").click(function() {
this.disabled = true;
$("p:eq(1)").attr("id", "p1");
$("p:eq(2)").addClass("foo");
alert("Change made, set an id on one element and added a class to another");
});
// See results
$("#seeResults").click(function() {
this.disabled = true;
// Look for changes
clone.find("*").addBack().each(function() {
// Get this clone's unique identifier
var uid = $(this).attr("data-uid");
// Get the real element corresponding to it, if it's
// still there
var elm = $("[data-uid=" + uid + "]")[0];
// Look for changes
if (!elm) {
display("Element with uid " + uid + ": Was removed");
}
else {
if (elm.id !== this.id) {
display("Element with uid " + uid + ": <code>id</code> changed, now '" + elm.id + "', was '" + this.id + "'");
}
if (elm.className !== this.className) {
display("Element with uid " + uid + ": <code>className</code> changed, now '" + elm.className + "', was '" + this.className + "'");
}
}
});
});
function display(msg) {
$("<p>").html(String(msg)).appendTo(document.body);
}
}, 0);
Earlier Answer
Assuming the server gives you the same text for the page every time it's asked, you can get the unaltered text client-side via ajax. That leaves us with the question of how to apply the id attributes to it.
If you need the original contents but not necessarily identical source (e.g., it's okay if tag names change case [div might become DIV], or attributes gain/lose quotes around them), you could use the source from the server (retrieved via ajax) to populate a document fragment, and apply the id values to the fragment at the same time you apply them to the main document. Then send the source of the fragment to the server.
Populating a fragment with the full HTML from your server is not quite as easy as it should be. Assuming html doesn't have any classes or anything on it, then:
var frag, html, prefix, suffix;
frag = document.createDocumentFragment();
html = document.createElement("html");
frag.appendChild(html);
prefix = stringFromServer..match(/(^.*<html[^>]*>)/);
prefix = prefix ? prefix[1] : "<!doctype html><html>";
suffix = stringFromServer.match(/(<\/html>\s*$)/);
suffix = suffix ? suffix[1] : "</html>";
html.innerHTML = stringFromServer.replace(/^.*<html[^>]*>/, '').replace(/<\/html>\s*$/, '');
There, we take the server's string, grab the outermost HTML parts (or use defaults), and then assign the inner HTML to an html element inside a fragment (although the more I think about it, the less I see the need for a fragment at all — you can probably just drop the fragment part). (Side Note: The part of the regular expressions above that identifies the start tag for the html element, <html[^>]*>, is one of those "good enough" things. It isn't perfect, and in particular will fail if you have a > inside a quoted attribute value, like this: <html data-foo="I have a > in me">, which is perfectly valid. Working around that requires much harder parsing, so I've assumed above that you don't do it, as it's fairly unusual.)
Then you can find elements within it via html.querySelector and html.querySelectorAll in order to apply your id attributes to them. Forming the relevant selectors will be great fun, probably a lot of positional stuff.
When you're done, getting back the HTML to send to the server looks like this:
var stringToSend = prefix + html.innerHTML + suffix;
In practice, what are the advantages of using createElement over innerHTML? I am asking because I'm convinced that using innerHTML is more efficient in terms of performance and code readability/maintainability but my teammates have settled on using createElement as the coding approach. I just wanna understand how createElement can be more efficient.
There are several advantages to using createElement instead of modifying innerHTML (as opposed to just throwing away what's already there and replacing it) besides safety, like Pekka already mentioned:
Preserves existing references to DOM elements when appending elements
When you append to (or otherwise modify) innerHTML, all the DOM nodes inside that element have to be re-parsed and recreated. If you saved any references to nodes, they will be essentially useless, because they aren't the ones that show up anymore.
Preserves event handlers attached to any DOM elements
This is really just a special case (although common) of the last one. Setting innerHTML will not automatically reattach event handlers to the new elements it creates, so you would have to keep track of them yourself and add them manually. Event delegation can eliminate this problem in some cases.
Could be simpler/faster in some cases
If you are doing lots of additions, you definitely don't want to keep resetting innerHTML because, although faster for simple changes, repeatedly re-parsing and creating elements would be slower. The way to get around that is to build up the HTML in a string and set innerHTML once when you are done. Depending on the situation, the string manipulation could be slower than just creating elements and appending them.
Additionally, the string manipulation code may be more complicated (especially if you want it to be safe).
Here's a function I use sometimes that make it more convenient to use createElement.
function isArray(a) {
return Object.prototype.toString.call(a) === "[object Array]";
}
function make(desc) {
if (!isArray(desc)) {
return make.call(this, Array.prototype.slice.call(arguments));
}
var name = desc[0];
var attributes = desc[1];
var el = document.createElement(name);
var start = 1;
if (typeof attributes === "object" && attributes !== null && !isArray(attributes)) {
for (var attr in attributes) {
el[attr] = attributes[attr];
}
start = 2;
}
for (var i = start; i < desc.length; i++) {
if (isArray(desc[i])) {
el.appendChild(make(desc[i]));
}
else {
el.appendChild(document.createTextNode(desc[i]));
}
}
return el;
}
If you call it like this:
make(["p", "Here is a ", ["a", { href:"http://www.google.com/" }, "link"], "."]);
you get the equivalent of this HTML:
<p>Here is a link.</p>
User bobince puts a number of cons very, very well in his critique of jQuery.
... Plus, you can make a div by saying $(''+message+'') instead of having to muck around with document.createElement('div') and text nodes. Hooray! Only... hang on. You've not escaped that HTML, and have probably just created a cross-site-scripting security hole, only on the client side this time. And after you'd spent so long cleaning up your PHP to use htmlspecialchars on the server-side, too. What a shame. Ah well, no-one really cares about correctness or security, do they?
jQuery's not wholly to blame for this. After all, the innerHTML property has been about for years, and already proved more popular than DOM. But the library certainly does encourage that style of coding.
As for performance: InnerHTML is most definitely going to be slower, because it needs to be parsed and internally converted into DOM elements (maybe using the createElement method).
InnerHTML is faster in all browsers according to the quirksmode benchmark provided by #Pointy.
As for readability and ease of use, you will find me choosing innerHTML over createElement any day of the week in most projects. But as you can see, there are many points speaking for createElement.
While innerHTML may be faster, I don't agree that it is better in terms of readability or maintenance. It may be shorter to put everything in one string, but shorter code is not always necessarily more maintainable.
String concatenation just does not scale when dynamic DOM elements need to be created as the plus' and quote openings and closings becomes difficult to track. Consider these examples:
The resulting element is a div with two inner spans whose content is dynamic. One of the class names (warrior) inside the first span is also dynamic.
<div>
<span class="person warrior">John Doe</span>
<span class="time">30th May, 2010</span>
</div>
Assume the following variables are already defined:
var personClass = 'warrior';
var personName = 'John Doe';
var date = '30th May, 2010';
Using just innerHTML and mashing everything into a single string, we get:
someElement.innerHTML = "<div><span class='person " + personClass + "'>" + personName + "</span><span class='time'>" + date + "</span></div>";
The above mess can be cleaned up with using string replacements to avoid opening and closing strings every time. Even for simple text replacements, I prefer using replace instead of string concatenation.
This is a simple function that takes an object of keys and replacement values and replaces them in the string. It assumes the keys are prefixed with $ to denote they are a special value. It does not do any escaping or handle edge cases where $ appears in the replacement value etc.
function replaceAll(string, map) {
for(key in map) {
string = string.replace("$" + key, map[key]);
}
return string;
}
var string = '<div><span class="person $type">$name</span><span class="time">$date</span></div>';
var html = replaceAll(string, {
type: personClass,
name: personName,
date: date
});
someElement.innerHTML = html;
This can be improved by separating the attributes, text, etc. while constructing the object to get more programmatic control over the element construction. For example, with MooTools we can pass object properties as a map. This is certainly more maintainable, and I would argue more readable as well. jQuery 1.4 uses a similar syntax to pass a map for initializing DOM objects.
var div = new Element('div');
var person = new Element('span', {
'class': 'person ' + personClass,
'text': personName
});
var when = new Element('span', {
'class': 'time',
'text': date
});
div.adopt([person, when]);
I wouldn't call the pure DOM approach below to be any more readable than the ones above, but it's certainly more maintainable because we don't have to keep track of opening/closing quotes and numerous plus signs.
var div = document.createElement('div');
var person = document.createElement('span');
person.className = 'person ' + personClass;
person.appendChild(document.createTextNode(personName));
var when = document.createElement('span');
when.className = 'date';
when.appendChild(document.createTextNode(date));
div.appendChild(person);
div.appendChild(when);
The most readable version would most likely result from using some sort of JavaScript templating.
<div id="personTemplate">
<span class="person <%= type %>"><%= name %></span>
<span class="time"><%= date %></span>
</div>
var div = $("#personTemplate").create({
name: personName,
type: personClass,
date: date
});
You should use createElement if you want to keep references in your code. InnerHTML can sometimes create a bug that is hard to spot.
HTML code:
<p id="parent">sample <span id='test'>text</span> about anything</p>
JS code:
var test = document.getElementById("test");
test.style.color = "red"; //1 - it works
document.getElementById("parent").innerHTML += "whatever";
test.style.color = "green"; //2 - oooops
1) you can change the color
2) you can't change color or whatever else anymore, because in the line above you added something to innerHTML and everything is re-created and you have access to something that doesn't exist anymore. In order to change it you have to again getElementById.
You need to remember that it also affects any events. You need to re-apply events.
InnerHTML is great, because it is faster and most time easier to read but you have to be careful and use it with caution. If you know what you are doing you will be OK.
Template literals (Template strings) is another option.
const container = document.getElementById("container");
const item_value = "some Value";
const item = `<div>${item_value}</div>`
container.innerHTML = item;
I'm writing a GreaseMonkey script where I'm iterating through a bunch of elements. For each element, I need a string ID that I can use to reference that element later. The element itself doesn't have an id attribute, and I can't modify the original document to give it one (although I can make DOM changes in my script). I can't store the references in my script because when I need them, the GreaseMonkey script itself will have gone out of scope. Is there some way to get at an "internal" ID that the browser uses, for example? A Firefox-only solution is fine; a cross-browser solution that could be applied in other scenarios would be awesome.
Edit:
If the GreaseMonkey script is out of scope, how are you referencing the elements later? They GreaseMonkey script is adding events to DOM objects. I can't store the references in an array or some other similar mechanism because when the event fires, the array will be gone because the GreaseMonkey script will have gone out of scope. So the event needs some way to know about the element reference that the script had when the event was attached. And the element in question is not the one to which it is attached.
Can't you just use a custom property on the element? Yes, but the problem is on the lookup. I'd have to resort to iterating through all the elements looking for the one that has that custom property set to the desired id. That would work, sure, but in large documents it could be very time consuming. I'm looking for something where the browser can do the lookup grunt work.
Wait, can you or can you not modify the document? I can't modify the source document, but I can make DOM changes in the script. I'll clarify in the question.
Can you not use closures? Closuses did turn out to work, although I initially thought they wouldn't. See my later post.
It sounds like the answer to the question: "Is there some internal browser ID I could use?" is "No."
The answer is no, there isn't an internal id you can access. Opera and IE (maybe Safari?) support .sourceIndex (which changes if DOM does) but Firefox has nothing of this sort.
You can simulate source-index by generating Xpath to a given node or finding the index of the node from document.getElementsByTagName('*') which will always return elements in source order.
All of this requires a completely static file of course. Changes to DOM will break the lookup.
What I don't understand is how you can loose references to nodes but not to (theoretical) internal id's? Either closures and assignments work or they don't. Or am I missing something?
Closure is the way to go. This way you'll have exact reference to the element that even will survive some shuffling of DOM.
Example for those who don't know closures:
var saved_element = findThatDOMNode();
document.body.onclick = function()
{
alert(saved_element); // it's still there!
}
If you had to store it in a cookie, then I recommend computing XPath for it (e.g. walk up the DOM counting previous siblings until you find element with an ID and you'll end up with something like [#id=foo]/div[4]/p[2]/a).
XPointer is W3C's solution to that problem.
A bit confused by the wording of your question - you say that you "need a string ID that [you] can use to reference that element later, " but that you "can't store the references in [your] script because when [you] need them, the GreaseMonkey script itself will have gone out of scope."
If the script will have gone out of scope, then how are you referencing them later?!
I am going to ignore the fact that I am confused by what you are getting at and tell you that I write Greasemonkey scripts quite often and can modify the DOM elements I access to give them an ID property. This is code you can use to get a pseudo-unique value for temporary use:
var PseudoGuid = new (function() {
this.empty = "00000000-0000-0000-0000-000000000000";
this.GetNew = function() {
var fourChars = function() {
return (((1 + Math.random()) * 0x10000)|0).toString(16).substring(1).toUpperCase();
}
return (fourChars() + fourChars() + "-" + fourChars() + "-" + fourChars() + "-" + fourChars() + "-" + fourChars() + fourChars() + fourChars());
};
})();
// usage example:
var tempId = PseudoGuid.GetNew();
someDomElement.id = tempId;
That works for me, I just tested it in a Greasemonkey script myself.
UPDATE: Closures are the way to go - personally, as a hard-core JavaScript developer, I don't know how you didn't think of those immediately. :)
myDomElement; // some DOM element we want later reference to
someOtherDomElement.addEventListener("click", function(e) {
// because of the closure, here we have a reference to myDomElement
doSomething(myDomElement);
}, false);
Now, myDomElement is one of the elements you apparently, from your description, already have around (since you were thinking of adding an ID to it, or whatever).
Maybe if you post an example of what you are trying to do, it would be easier to help you, assuming this doesn't.
UPDATE: Closures are indeed the answer. So after fiddling with it some more, I figured out why closures were initially problematic and how to fix it. The tricky thing with a closure is you have to be careful when iterating through the elements not to end up with all of your closures referencing the same element. For example, this doesn't work:
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var button = document.createElement("button");
button.addEventListener("click", function(ev) {
// do something with element here
}, false)
}
But this does:
var buildListener = function(element) {
return function(ev) {
// do something with event here
};
};
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var button = document.createElement("button");
button.addEventListener("click", buildListener(element), false)
}
Anyway, I decided not to select one answer because the question had two answers: 1) No, there are no internal IDs you can use; 2) you should use closures for this. So I simply upvoted the first people to say whether there were internal IDs or who recommended generating IDs, plus anyone who mentioned closures. Thanks for the help!
If you can write to the DOM (I'm sure you can). I would solve this like this:
Have a function return or generate an ID:
//(function () {
var idCounter = new Date().getTime();
function getId( node ) {
return (node.id) ? node.id : (node.id = 'tempIdPrefix_' + idCounter++ );
}
//})();
Use this to get ID's as needed:
var n = document.getElementById('someid');
getId(n); // returns "someid"
var n = document.getElementsByTagName('div')[1];
getId(n); // returns "tempIdPrefix_1224697942198"
This way you don't need to worry about what the HTML looks like when the server hands it to you.
If you're not modifying the DOM you can get them all by indexed order:
(Prototype example)
myNodes = document.body.descendants()
alert(document.body.descendants()[1].innerHTML)
You could loop through all of the nodes and give them a unique className that you could later select easily.
You can set the id attribute to a computed value. There is a function in the prototype library that can do this for you.
http://www.prototypejs.org/api/element/identify
My favorite javascript library is jQuery. Unfortunately jQuery does not have a function like identify. However, you can still set the id attribute to a value that you generate on your own.
http://docs.jquery.com/Attributes/attr#keyfn
Here is a partial snippet from jQuery docs that sets id for divs based on the position in the page:
$(document).ready(function(){
$("div").attr("id", function (arr) {
return "div-id" + arr;
});
});
You can generate a stable, unique identifier for any given node in a DOM with the following function:
function getUniqueKeyForNode (targetNode) {
const pieces = ['doc'];
let node = targetNode;
while (node && node.parentNode) {
pieces.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
node = node.parentNode
}
return pieces.reverse().join('/');
}
This will create identifiers such as doc/0, doc/0/0, doc/0/1, doc/0/1/0, doc/0/1/1 for a structure like this one:
<div>
<div />
<div>
<div />
<div />
</div>
</div>
There are also a few optimisations and changes you can make, for example:
In the while loop, break when that node has an attribute you know to be unique, for example #id
Not reverse() the pieces, currently it is just there to look more like the DOM structure the ID's are generated from
Not include the first piece doc if you don't need an identifier for the document node
Save the identifier on the node in some way, and reuse that value for child nodes to avoid having to traverse all the way up the tree again.
If you're writing these identifiers back to XML, use another concatenation character if the attribute you're writing is restricted.
Use mouse and/or positional properties of the element to generate a unique ID.
In javascript, you could attach a custom ID field to the node
if(node.id) {
node.myId = node.id;
} else {
node.myId = createId();
}
// store myId
It's a bit of hack, but it'll give each and every node an id you can use. Of course, document.getElementById() won't pay attention to it.
You can also use pguid (page-unique identifier) for unique identifier generation:
pguid = b9j.pguid.next() // A unique id (suitable for a DOM element)
// is generated
// Something like "b9j-pguid-20a9ff-0"
...
pguid = b9j.pguid.next() // Another unique one... "b9j-pguid-20a9ff-1"
// Build a custom generator
var sequence = new b9j.pguid.Sequence({ namespace: "frobozz" })
pguid = sequence.next() "frobozz-c861e1-0"
http://appengine.bravo9.com/b9j/documentation/pguid.html
I 'think' I've just solved a problem similar to this. However, I'm using jQuery in a browser DOM environment.
var objA = $("selector to some dom element");
var objB = $("selector to some other dom element");
if( objA[0] === objB[0]) {
//GREAT! the two objects point to exactly the same dom node
}
OK, there is no ID associated to DOM element automatically.
DOM has a hierarchycal structure of elements which is the main information.
From this perspective, you can associate data to DOM elements with jQuery or jQLite. It can solve some issues when you have to bind custom data to elements.