LOGO
EDITORIAL
JAVASCRIPT DOM Helper
SPOTS ON D3.js
ARTIST'S CORNER Sensorial Expressions and Emotion: Part II

JAVASCRIPT

DOM Helper
Author: DS
Published: July 2011

In this article we are going to see how to make a progressive and efficient DOM helper. The term is commonly used to designate a function or class designed to mechanize the creation of DOM nodes. We are actually going to write an XML namespace version suitable for SVG, and then two derivatives, one for plain HTML and the other for HTML in an XML namespace context.

The DOM helper is designed to serve both as core for conceptually modern libraries and as a user tool for building elements, with a learning curve of length zero.

The basic concept behind its design is that of exposing the SVG library (the implementation) directly, using the library's standard language, with no need for cumbersome pseudo-languages and redundant classes or utility functions. Starting then from the principle that an SVG implementation is a library, we can consider that when accessing it through its JavaScript DOM interfaces to create instances of elements, the most straightforward method would be to use an OOD model where during instantiation we can set or override the intrinsic properties of the class (whether those properties for a particular element are designated as properties or attributes in SVG grammar), remembering that some properties have initial values and that some are required.

The function expects exactly 1 object, and returns a reference to the newly created SVG element in the DOM.

The object literal or reference passed as argument contains the definition of any attributes that need to be set, including the style attribute, without any artifact or appendices, in one pass. The object is precisely what is expected by the SVG library, an element name and a collection of attribute/value pairs.

The names of the object's properties will be equal to the attribute names that they represent. For those SVG attribute names containing illegal characters such as - (hyphen) or : (colon) we use the string notation – otherwise known as array notation. We could actually use it for all the properties but this is not necessary, property names in JavaScript always resolve to strings!

As new elements and attributes are added to specifications we will be able to use them directly. The DOM helper, and any library that uses it, will not need to be updated.

The function is designed to evaluate three properties whose names are not in the SVG namespace:

element. Required. It gets a string specifying the name of the element to create. This must be the first property defined in the object.

textNode. Optional. It gets a string specifying the text. It can be used while creating a <text> or <tspan> element. The text node is automatically created and appended.

appendTo. Optional. Specifies the parent node to which the element is to be appended. The element can be appended manually or programmatically at a later stage in your program. A generic value for the appendTo property can be document.documentElement. If we set this property its value must be an existing and valid container, whether referenced or defined on the fly (in this case the new call will be executed in its own scope).

All the other properties representing SVG attribute names are not evaluated, and it's our responsibility to see that they are coherent with the specified element and conformant, just like when coding SVG.

The SVG DOM Helper Function


var createSVGElement = function(o) {
  for (var p in o) {
    var value = o[p];
    switch(p) {
      case "element" : var element = document.createElementNS(svgNS, o.element);
      break;
      case "textNode" : element.appendChild(document.createTextNode(value));
      break;
      case "appendTo" : value.appendChild(element);
      break;
      default : element.setAttributeNS((p == "xlink:href") ? 
                xlinkNS : null, p, value);
    }
  }
  return element;
};

Of course svgNS and xlinkNS will have been defined first:


var svgNS = "http://www.w3.org/2000/svg",
    xlinkNS = "http://www.w3.org/1999/xlink";

That's all there is to it. The function creates any SVG element, with any attributes set, and if the textNode property is set a text node will be created and appended (only!) to <text> and <tspan> elements.

Note that, contrary to a certain mythology, the order of the enumeration of array elements or object properties in the for in loop IS guaranteed as long as the object is not manipulated by array methods or the delete operator in the case of objects. It is a mistake to interpret the JavaScript specification warning about the order not being guaranteed, as meaning that the array indices or object properties (as labeled indices) are not stored sequentially and contiguous in memory. The confusion may come from the fact that the indices are not required to be ordinally contiguous. The table has a base address (the address of the first index) and the address of the indices is calculated on that. The addresses where the values of the elements are actually stored, and to which the indices point can be anywhere in memory. For further and probably better explanations you can consult literature on processors or assembler.

Back to our case. When dealing with a literal passed as parameter, alteration is impossible of course, and in the case of a reference you must make sure that the referenced object does not get altered. Nevertheless, changing the values of the properties programmatically is possible, as the order of the properties will not be affected. The for in loop is perfectly safe and a good habit as long as it is handled with care.

Why not make the appendTo property be a class method, or prototype method, or a property of the function, or similar technique? It would be counterproductive for at least one reason, that it's not uncommon to find situations where we need to preserve the possibility of appending the element or group of elements at a later stage. For example, a candidate parent node may not exist yet, or may exist but not yet be known.

Similarly, insertBefore and insertAfter are not implemented because the benefit would be practically inexistent. Experience proves that those methods are primarily used in layers reorganization or similar processes, and only occasionally when elements are created, making that the supposed benefit is not worth the cost of overhead processing, however small that may be.

The call can be referenced or not. The function returns the element itself, meaning that whenever the call is assigned to a variable or property, this is then a pointer to the element in the DOM tree, extremely useful for DOM nodes that are intended or likely to be manipulated.

The object literal passed in the call will look like this:

createSVGElement ({
  element : element, 
  ... (attribute name/value pair),
  ... (attribute name/value pair),
  appendTo : node
});

The value node will be any suitable existing parent container, whether appended to the document or not.

We can thus set any attribute for the element. As we have already seen, it's our responsibility to see that attributes are coherent with the requested element.

Examples

A <g> element (assigned):


var g = createSVGElement({
  element : "g",
  id : id,
  transform : "translate(value value)",
  appendTo : node
});

A <rect> element:


createSVGElement({
  element : "rect", 
  width : value, 
  height : value, 
  fill : value, 
  stroke : value,
  "stroke-width" : value,
  "fill-opacity" : value,
  rx : value,
  ...
  appendTo : g
});

Note the string notation for property names with illegal characters. Note also how, for the attributes, the syntax compares to SVG markup.

In the example above the style (or class) attribute could be used instead:


createSVGElement({
  element : "rect",
  style : "width: value; height: value; fill: value; stroke: value; ...", 
  appendTo : g
});

Nevertheless, the string quickly becomes difficult to read and maintain if we include expressions.

Note: At writing time Safari does not allow to set the class attribute in the SVG namespace.

A <text> element:


createSVGElement({
  element : 'text', 
  x : 40, 
  y : 40, 
  "font-size" : "18pt", 
  fill : "red", 
  "text-anchor" : "middle", 
  "pointer-events" : "none",
  transform : "rotate(-45) translate(100 100)",
  textNode : "rotX", 
  appendTo : node
});

Elements containing descendant elements, like filters for example, will be created first, then the descendants created and appended to them. We are going to see a different technique for appending descendant elements to an unreferenced element (the marker example), in which case the process cannot be deferred of course.

A <linearGradient> element:


var gradient = createSVGElement({
  element: "linearGradient",
  id : "...",
  x1 : "0%",
  y1 : "0%",
  x2 : "0%",
  y2 : "100%",
  gradientUnits : "objectBoundingBox",
  appendTo : defs
});
createSVGElement({
  element : "stop",
  offset : "...",
  "stop-color" : "...",
  appendTo : gradient
});
createSVGElement({
  element : "stop",
  offset : "...",
  "stop-color" : "...",
  appendTo : gradient
});

A <marker> element:


createSVGElement({
  element : "marker",
  id : "arrow",
  viewBox : "0 0 10 10",
  refX : 0,
  refY : 5,
  orient : "auto",
  appendTo : defs
})
.appendChild(createSVGElement({
  element : "path",
  d : "M0,0L10,5L0,10z",
  fill : "#A00000",
  stroke : "gray",
  "stroke-width" : .1
}));

What is the difference between the two techniques? Apart from the one already described, in the first case (the gradient) the descendants can be assigned for future reference, allowing for easier dynamic transformations of the document.

For some situations it will be useful and easy to define sidekick functions for serialized production.

We are now going to see two derivatives, for creating HTML nodes in an XML document (for example SVG + <foreignObject> + HTML), and for plain HTML where we simply drop the namespace:

The HTML DOM Helper Function for Use with foreignObject

Use this code to produce html code in <foreignObject> (first create a <foreignObject> element with the SVG DOM Helper).


var xhtmlNS = "http://www.w3.org/1999/xhtml";

var createHTMLElement = function(o) {
  for (var p in o) {
    var value = o[p];
    switch(p) {
      case "element" : var element = document.createElementNS(xhtmlNS, value);
      break;
      case "textNode" : element.appendChild(document.createTextNode(value));
      break;
      case "appendTo" : value.appendChild(element);
      break;
      default : element.setAttribute(p, value);
    }
  }
  return element;
};

The HTML DOM Helper Function


var createHTMLElement = function(o) {
  for (var p in o) {
    var value = o[p];
    switch(p) {
      case "element" : var element = document.createElement(o.element);
      break;
      case "textNode" : element.appendChild(document.createTextNode(value));
      break;
      case "appendTo" : value.appendChild(element);
      break;
      default : element.setAttribute(p, value);
    }
  }
  return element;
};

Note: This HTML DOM helper is not backward compatible with old browsers prior to DOM Level 1.

While SVG elements have "paint" attributes that are exposed by the SVG DOM giving us the possibility of setting them individually or through the style or class attribute, in an HTML context we typically prefer to use the style or class attribute for maximum compatibility.

Example with the style attribute:

createHTMLElement({
  element : "p",
  textNode : "A paragraph",
  style : "font-size: 16pt; font-weight: bold; color: brown;",
  appendTo : document.body
});

Example with the class attribute:

createHTMLElement({
  element : "p",
  textNode : "A paragraph",
  class : "myClass",
  appendTo : document.body
});

Note that at writing time Safari does not allow to set the class attribute, with or without namespace. The cross-compatible alternative is to set the className JavaScript property:

createHTMLElement({
  element : "p",
  textNode : "A paragraph",
  appendTo : document.body
})
.className = "myClass";

Unlike the SVG DOM Helper, where we can use the textNode property only when creating a <text> or <tspan> element, with the HTML DOM Helper we can set it for all the relevant elements.

A modern library is one that takes full advantage of the DOM exposure, where the objective is not that of defining a proprietary representation of it, but that of providing well designed classes specializing in well defined modular categories of objects and, or user functions and Object extensions. The user should be able to include osmotic libraries in her work and be able to use them in a consistent manner. We may consider that a library that doesn't adopt this core model, constraining the user to learn an intermediate proprietary representation, is old legacy.