Section 15.8. Adding Content to a Document


15.8. Adding Content to a Document

The methods Document.createElement() and Document.createTextNode() create new Element and Text nodes, and the methods Node.appendChild(), Node.insertBefore(), and Node.replaceChild() can be used to add them to a document. With these methods, you can build up a DOM tree of arbitrary document content.

Example 15-9 is an extended example that defines a log() function for message and object logging. The example also includes a log.debug() utility function that can be a helpful alternative to inserting alert() calls when debugging JavaScript code. The "message" passed to the log() function can be either a string of plain text or a JavaScript object. When a string is logged, it is simply displayed as it is. When an object is logged, it is displayed as a table of property names and property values. In either case, the createElement() and createTextNode() functions create the new content.

With an appropriate CSS stylesheet (which is shown in the example), the log() function of Example 15-9 generates output like that shown in Figure 15-4.

Figure 15-4. Output of the log() function


Example 15-9 is a long one, but it is well commented and worth studying carefully. Pay attention, in particular, to the calls to createElement(), createTextNode(), and appendChild(). The private function log.makeTable() shows how these functions are used to create the relatively complex structure of an HTML table.

Example 15-9. A logging facility for client-side JavaScript

 /*  * Log.js: Unobtrusive logging facility  *  * This module defines a single global symbol: a function named log().  * Log a message by calling this function with 2 or 3 arguments:  *  *   category: the type of the message. This is required so that messages  *     of different types can be selectively enabled or disabled and so  *     that they can be styled independently. See below.  *  *   message: the text to be logged. May be empty if an object is supplied  *  *   object: an object to be logged. This argument is optional.  If passed,  *     the properties of the object will be logged in the form of a table.  *     Any property whose value is itself an object may be logged recursively.  *  * Utility Functions:  *  *   The log.debug() and log.warn() functions are utilities that simply  *   call the log() function with hardcoded categories of "debug" and  *   "warning".  It is trivial to define a utility that replaces the built-in  *   alert() method with one that calls log().  *  * Enabling Logging  *  *   Log messages are *not* displayed by default.  You can enable the  *   display of messages in a given category in one of two ways. The  *   first is to create a <div> or other container element with an id  *   of "<category>_log". For messages whose category is "debug", you might  *   place the following in the containing document:  *  *      <div ></div>  *  *   In this case, all messages of the specified category are appended  *   to this container, which can be styled however you like.  *  *   The second way to enable messages for a given category is to  *   set an appropriate logging option. To enable the category  *   "debug", you'd set log.options.debugEnabled = true. When you  *   do this, a <div > is created for the logging messages.  *   If you want to disable the display of log messages, even if a container  *   with a suitable id exists, set another option:  *   log.options.debugDisabled=true. Set this option back to false to  *   re-enable log messages of that category.  *  * Styling Log Messages  *  *   In addition to styling the log container, you can use CSS to  *   style the display of individual log messages. Each log message  *   is placed in a <div> tag, and given a CSS class of  *   <category>_message. Debugging messages would have a class "debug_message"  *  * Log Options  *  *   Logging behavior can be altered by setting properties of the log.options  *   object, such as the options described earlier to enable or disable logging  *   for given categories. A few other options are available:  *  *     log.options.timestamp: If this property is true, each log message  *       will have the date and time added to it.  *  *     log.options.maxRecursion: An integer that specifies the maximum number  *       of nested tables to display when logging objects. Set this to 0 if  *       you never want a table within a table.  *  *     log.options.filter: A function that filters properties out when logging  *       an object. A filter function is passed the name and value of  *       a property and returns true if the property should appear in the  *       object table or false otherwise.  */ function log(category, message, object) {     // If this category is explicitly disabled, do nothing     if (log.options[category + "Disabled"]) return;     // Find the container     var id = category + "_log";     var c = document.getElementById(id);     // If there is no container, but logging in this category is enabled,     // create the container.     if (!c && log.options[category + "Enabled"]) {         c = document.createElement("div");         c.id = id;         c.className = "log";         document.body.appendChild(c);     }     // If still no container, we ignore the message     if (!c) return;     // If timestamping is enabled, add the timestamp     if (log.options.timestamp)         message = new Date() + ": " + (message?message:"");     // Create a <div> element to hold the log entry     var entry = document.createElement("div");     entry.className = category + "_message";     if (message) {         // Add the message to it         entry.appendChild(document.createTextNode(message));     }     if (object && typeof object == "object") {         entry.appendChild(log.makeTable(object, 0));     }     // Finally, add the entry to the logging container     c.appendChild(entry); } // Create a table to display the properties of the specified object log.makeTable = function(object, level) {     // If we've reached maximum recursion, return a Text node instead.     if (level > log.options.maxRecursion)         return document.createTextNode(object.toString());     // Create the table we'll be returning     var table = document.createElement("table");     table.border = 1;     // Add a Name|Type|Value header to the table     var header = document.createElement("tr");     var headerName = document.createElement("th");     var headerType = document.createElement("th");     var headerValue = document.createElement("th");     headerName.appendChild(document.createTextNode("Name"));     headerType.appendChild(document.createTextNode("Type"));     headerValue.appendChild(document.createTextNode("Value"));     header.appendChild(headerName);     header.appendChild(headerType);     header.appendChild(headerValue);     table.appendChild(header);     // Get property names of the object and sort them alphabetically     var names = [];     for(var name in object) names.push(name);     names.sort();     // Now loop through those properties     for(var i = 0; i < names.length; i++) {         var name, value, type;         name = names[i];         try {             value = object[name];             type = typeof value;         }         catch(e) { // This should not happen, but it can in Firefox             value = "<unknown value>";             type = "unknown";         };         // Skip this property if it is rejected by a filter         if (log.options.filter && !log.options.filter(name, value)) continue;         // Never display function source code: it takes up too much room         if (type == "function") value = "{/*source code suppressed*/}";         // Create a table row to display property name, type and value         var row = document.createElement("tr");         row.vAlign = "top";         var rowName = document.createElement("td");         var rowType = document.createElement("td");         var rowValue = document.createElement("td");         rowName.appendChild(document.createTextNode(name));         rowType.appendChild(document.createTextNode(type));         // For objects, recurse to display them as tables         if (type == "object")             rowValue.appendChild(log.makeTable(value, level+1));         else             rowValue.appendChild(document.createTextNode(value));         // Add the cells to the row, and add the row to the table         row.appendChild(rowName);         row.appendChild(rowType);         row.appendChild(rowValue);         table.appendChild(row);     }     // Finally, return the table.     return table; } // Create an empty options object log.options = {}; // Utility versions of the function with hardcoded categories log.debug = function(message, object) { log("debug", message, object); }; log.warn = function(message, object) { log("warning", message, object); }; // Uncomment the following line to convert alert() dialogs to log messages // function alert(msg) { log("alert", msg); } 

The debugging messages that appear in Figure 15-4 were created with the following simple code:

 <head> <script src="/books/2/427/1/html/2/Log.js"></script>                         <!-- include log() --> <link rel="stylesheet" type="text/css" href="log.css"> <!-- include styles --> </head> <body> <script> function makeRectangle(x, y, w, h) { // This is the function we want to debug     log.debug("entering makeRectangle");    // Log a message     var r = {x:x, y:y, size: { w:w, h:h }};     log.debug("New rectangle", r);          // Log an object     log.debug("exiting makeRectangle");     // Log another message     return r; } </script> <!-- this button invokes the function we want to debug --> <button onclick="makeRectangle(1,2,3,4);">Make Rectangle</button> <!-- This is where our logging messages will be placed --> <!-- We enable logging by putting this <div> in the document --> <div  ></div> 

The appearance of the log messages in Figure 15-4 is created using CSS styles, imported into the code with a <link> tag. The styles used to create the figure are the following:

 #debug_log { /* Styles for our debug message container */     background-color: #aaa;    /* gray background */     border: solid black 2px;   /* black border */     overflow: auto;            /* scrollbars */     width: 75%;                /* not as wide as full window */     height: 300px;             /* don't take up too much vertical space */ } #debug_log:before { /* Give our logging area a title */     content: "Debugging Messages";     display: block;     text-align: center;     font: bold 18pt sans-serif ; } .debug_message { /* Place a thin black line between debug messages */     border-bottom: solid black 1px; } 

You'll learn more about CSS in Chapter 16. It is not important that you understand the CSS details here. The CSS is included here so you can see how styles are associated with the dynamically generated content created by the log() function.

15.8.1. Convenience Methods for Creating Nodes

In reading Example 15-9, you may have noticed that the API creating document content is verbose: first you create an Element, then you set its attributes, and then you create a Text node and add it to the Element. Next, you add the Element to its parent Element. And so on. Simply creating a <table> element, setting one attribute, and adding a header row to it took 13 lines of code in Example 15-9. Example 15-10 defines an Element-creation utility function that simplifies this kind of repetitive DOM programming.

Example 15-10 defines a single function named make(). make() creates an Element with the specified tag name, sets attributes on it, and adds children to it. Attributes are specified as properties of an object, and children are passed in an array. The elements of this array may be strings, which are converted to Text nodes, or they may be other Element objects, which are typically created with nested invocations of make().

make() has a flexible invocation syntax that allows two shortcuts. First, if no attributes are specified, the attributes argument can be omitted, and the children argument is passed in its place. Second, if there is only a single child, it can be passed directly instead of placing it in a single-element array. The only caveat is that these two shortcuts cannot be combined, unless the single child is a text node passed as a string.

Using make(), the 13 lines of code for creating a <table> and its header row in Example 15-9 can be shortened to the following:

 var table = make("table", {border:1}, make("tr", [make("th", "Name"),                                                   make("th", "Type"),                                                   make("th", "Value")])); 

But you can do even better. Example 15-10 follows the make() function with another function called maker(). Pass a tag name to maker(), and it returns a nested function that calls make() with the tag name you specified hardcoded. If you want to create a lot of tables, you can define creation functions for common table tags like this:

 var table = maker("table"), tr = maker("tr"), th = maker("th"); 

Then, with these maker functions defined, the table-creation and header-creation code shrinks down to a single line:

 var mytable = table({border:1}, tr([th("Name"), th("Type"), th("Value")])); 

Example 15-10. Element-creation utility functions

 /**  * make(tagname, attributes, children):  *   create an HTML element with specified tagname, attributes, and children.  *  * The attributes argument is a JavaScript object: the names and values of its  * properties are taken as the names and values of the attributes to set.  * If attributes is null, and children is an array or a string, the attributes  * can be omitted altogether and the children passed as the second argument.  *  * The children argument is normally an array of children to be added to  * the created element. If there are no children, this argument can be  * omitted. If there is only a single child, it can be passed directly  * instead of being enclosed in an array. (But if the child is not a string  * and no attributes are specified, an array must be used.)  *  * Example: make("p", ["This is a ", make("b", "bold"), " word."]);  *  * Inspired by the MochiKit library (http://mochikit.com) by Bob Ippolito  */ function make(tagname, attributes, children) {     // If we were invoked with two arguments, the attributes argument is     // an array or string; it should really be the children arguments.     if (arguments.length == 2 &&         (attributes instanceof Array || typeof attributes == "string")) {         children = attributes;         attributes = null;     }     // Create the element     var e = document.createElement(tagname);     // Set attributes     if (attributes) {         for(var name in attributes) e.setAttribute(name, attributes[name]);     }     // Add children, if any were specified.     if (children != null) {         if (children instanceof Array) {  // If it really is an array             for(var i = 0; i < children.length; i++) { // Loop through kids                 var child = children[i];                 if (typeof child == "string")          // Handle text nodes                     child = document.createTextNode(child);                 e.appendChild(child);  // Assume anything else is a Node             }         }         else if (typeof children == "string") // Handle single text child             e.appendChild(document.createTextNode(children));         else e.appendChild(children);         // Handle any other single child     }     // Finally, return the element.     return e; } /**  * maker(tagname): return a function that calls make() for the specified tag.  * Example: var table = maker("table"), tr = maker("tr"), td = maker("td");  */ function maker(tag) {     return function(attrs, kids) {         if (arguments.length == 1) return make(tag, attrs);         else return make(tag, attrs, kids);     } } 

15.8.2. The innerHTML Property

Although it has never been sanctioned by the W3C as an official part of the DOM, the innerHTML property of HTMLElement nodes is an important and powerful property that is supported by all modern browsers. When you query the value of this property for an HTML element, what you get is a string of HTML text that represents the children of the element. If you set this property, the browser invokes its HTML parser to parse your string and replaces the children of the element with whatever the parser returns.

Describing an HTML document as a string of HTML text is usually more convenient and compact than describing it as a sequence of calls to createElement() and appendChild(). Consider again the code from Example 15-9 that created a new <table> element and added a header row to it. You can rewrite that relatively lengthy code using innerHTML as follows:

 var table = document.createElement("table");  // Create the <table> element table.border = 1;                             // Set an attribute // Add a Name|Type|Value header to the table table.innerHTML = "<tr><th>Name</th><th>Type</th><th>Value</th></tr>"; 

Web browsers are, almost by definition, very good at parsing HTML. It turns out that using innerHTML is a reasonably efficient thing to do, especially when working with large chunks of HTML text to be parsed. Note, however that appending bits of text to the innerHTML property with the += operator is usually not efficient because it requires both a serialization step and a parsing step.

innerHTML was introduced by Microsoft in IE 4. It is the most important and frequently used member of a quartet of related properties. The other three properties, outerHTML, innerText, and outerText are not supported by Firefox and related browsers. They are described at the end of this chapter in Section 15.11.




JavaScript. The Definitive Guide
JavaScript: The Definitive Guide
ISBN: 0596101996
EAN: 2147483647
Year: 2004
Pages: 767

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net