Simple SVG API

From SVG

This is just a proposal, and is not even in Editor's Draft stage yet. This does not constitute any formal work by the SVG WG, and is merely a data-gathering exercise. Comments and critiques are most welcome, and should be sent to www-svg@w3.org. In particular, the names of the methods should be seen as placeholders. These ideas are drawn from the various members of the SVG WG and collected on the SVG 2 DOM page.

One option for making the SVG DOM faster and more convenient for authors would be element constructors which insert the elements immediately into the DOM. These can already be simulated in JavaScript libraries as convenience methods (see example below), but the chief benefit, making fewer DOM calls and increasing the speed of script, would be lost.

This proposal defines seven new methods on the Element interface, setAttrValues, getAttrValues, insertAtIndex, createElement, createElementNS, createChild, and createChildNS.

As the proposal stands, these methods require ECMAScript object literals to be passed in as arguments, or else return ECMAScript object literals. Although desirable in ECMAScript for the simplicity this brings, it's not clear at this stage whether this is compatible with the existing way of defining DOM interfaces (IDL) or other languages such as Java.

Grouped Attribute Value Setter and Getter

Currently, the syntax to set and get attribute values is both verbose and inefficient, since it has to access the DOM separately for each attribute. This is especially problematic for SVG, which is an attribute-centric language. For example, currently you must use the generic DOM setters, like this:

element.setAttribute("x", "10");
element.setAttribute("y", "20");
element.setAttribute("width", "100%");
element.setAttribute("height", "2em");
element.setAttribute("fill", "lime");

Or use the SVG DOM dot-accessor methods:

element.x.baseVal.value = 10;
element.y.baseVal.value = 20;
element.width.baseVal.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE, 100);
element.height.baseVal.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_EMS, 10);
element.setAttribute("fill", "lime");

Getting attribute values is equally inefficient.

This proposal defines a setter and getter that allow the author to set or get multiple attribute values with one method call.

setAttrValues

This method takes one parameter, an object containing comma-separated attribute-name:attribute-value pairs. Attribute names must be enclosed in quotes if they contain a dash ("-"), but may otherwise be unquoted strings. Attribute names may be unquoted if they are integers, but otherwise must be enclosed in quotes.

This method has no return value.

The syntax is as follows:

element.setAttrValues( object attributes-values-list );

Example usage (equivalent to the five lines found above):

// sets the specified attributes to the specified values
element.setAttrValues( { x:10, y:20, width:"100%", height:"2em", fill:"lime" } );

getAttrValues

This method takes one optional parameter, an array object containing comma-separated attribute-names. Attribute names must be enclosed in quotes. This is the list of attribute names which is requested.

If no object parameter is supplied, then all the attributes on that element are returned.

This method returns an object of attribute-name:attribute-value pairs matching the list of requested attributes, or of all attributes specified on that element if no parameter is supplied. If an attribute on this list is not found on the element, then the return value for that element shall be null. Attribute values which are unitless numbers are returned as numbers. No order is guaranteed for the attributes in the object list.

The syntax is as follows:

element.getAttrValues(  array attributes-list );

Example usage:

// returns an object containing the attribute values for the specified attributes
var attrList = element.setAttrValues( [ "x", "width," "stroke-width" ] );
// for example: attrList == { x:450, width:100, "stroke-width":2 };
// returns an object containing all attribute values specified on the element
var allAttrList = element.setAttrValues();
// for example: attrList == { x:450, y:220, width:100, height:30, fill:"none", stroke:"blue", "stroke-width":2 };
comments:
jwatt: Rather than returning an object literal, it may be better for this method
to return an Array if authors want to get values back in the same order as the order 
of the attribute names they specified in the input Array. Thoughts?

shepazu: I believe that there are more use cases for getting the attributes and 
their values back in a name-value pair format, rather than a particular order, 
especially for passing the object on to another element or to be processed in another 
part of the script, where the context is only preserved by the presence of the 
attribute name.  It is trivial to create an ordered list of raw attribute values from 
an object of name-value pairs, but impossible to do the reverse.

Inserting Child Nodes at a Specific Index

The DOM3 Core specification includes the insertBefore method to allow an author to insert an element before another reference element, but does not also include the ability to insert an element after another reference element.

To address this in a more robust manner, this proposal defines the insertAtIndex method.

insertAtIndex

This method takes two parameters, the element to be inserted, and the index at which the element is to be inserted in the parent element's list of child nodes. If the index is omitted, or if it is larger than the number of child nodes contained by the parent element, the element is simply appended as the last child of the parent element.

The syntax is as follows:

element.insertAtIndex( node element, integer index );

Example usage:

// inserts the new element as the fourth child of the parent element, or as the 
//   last child if there are fewer than 3 other child elements
element.insertAtIndex( myNewEl, 3 );

Constructor Methods

This proposal defines two new "insertion constructors" methods on the Element interface, createChild and createChildNS, and two new non-inserting constructor methods, createElement and createElementNS. As well as eliminating the need to remember and use namespace strings (in the common cases) and vastly simplifying the process of setting attributes on new elements, they also eliminate then need to get to the document object.

The reason for proposing two sets of elements that are identical, except that one set inserts the new element as a child of the element on which it is called, whereas the other does not, is because performance is much better if new subtrees are created outside the document, and then the root of the subtree is inserted once all manipulations of the subtree are done. The idea would be that the root of the subtree is created using the non-inserting method, but then all children are created using the inserting method (thereby saving the author from having to call insertChild for every child created).

jwatt: There has been the question of whether having very similar methods (with the only difference between them being whether they insert or not) would be confusing. By having the definition of one method in the spec point to the definition of the other and just say that it does the same, except that it does/does not insert, I think this would not be the case. Not only is it easy to remember which of createChild vs createElement does which, but by having this and a rational statement in the spec, it would raise awareness of the performance implications of building up a sub-tree in the document.

Generic vs. Element-Specific Constructors

This proposal uses generic constructors.

Having well-defined and specific constructors for individual elements might be better for languages such as Java, and might appeal more to people just learning SVG, since it could give more detailed error messages, but would increase implementation size and time. Authors remembering the specific order for attribute value parameters for each element constructor would also be cumbersome.

A generic constructor offers less specific functionality, but would be more extensible, and easier to remember. It requires authors to use more explicit object notation than the element-specific constructors, but also allows them to order the attributes in whichever manner they prefer, and to omit unwanted values, which default to the lacuna value.

createChild

This method is an "insertion constructor", meaning that it creates the element indicated with the supplied attribute values, and immediately inserts it as a child of the element on which the method was called, at the optional index position of that element's list of child-nodes. The parent element need not be in the DOM for the child to be inserted.

Th optional attribute object works in the same manner and with the same syntax as for the setAttrValues method.

This method returns a reference to the new element, for purposes of easily appending children to it. This is especially needed for container elements and textual elements.

Namespaces

This namespace-insensitive method implicitly creates the new element in the same namespace as the element on which the method was called.

For namespaced attributes, the namespaced attributes should pass an array as the value, with the first item being the attribute namespace, and the second item the attribute value.

The syntax is as follows:

element.createChild( string elementName, object attributes-values-list, integer index );

Example usage:

// create and append a <g> (group) element into the document, with no attributes
var g = document.documentElement.createChild( "g" );

// create and append a green <rect> element into the group
g.createChild( "rect", { x:30, y:30, width:"50", height:"50", rx:5, ry:5, fill:"green" } );

// create and append a red <rect> element into the group, at index 0 (before the green rectangle)
g.createChild( "rect", { x:10, y:10, width:"50", height:"50", rx:5, ry:5, fill:"red" }, 0 );

// create and append a blue <rect> element into the group, at index 2 (after the green rectangle)
g.createChild( "rect", { x:50, y:50, width:"50", height:"50", rx:5, ry:5, fill:"blue" }, 2 );

// create and append a yellow <rect> element into the group, at index 15
//  since this is greater than the number of child elements, the yellow rectangle is simply inserted
//  as the last child, after the blue rectangle, just as if it were appended
g.createChild( "rect", { x:70, y:70, width:"50", height:"50", rx:5, ry:5, fill:"yellow" }, 15 );
// insert text, and set value using textContent
var t = g.createChild( "text", { x:200, y:200, "font-size":"25", "font-weight":"bold", fill:"white",
                                   stroke:"blue", "stroke-width":2, "stroke-linejoin":"round", 
                                   "text-anchor":"middle", "font-family":"Verdana, sans-serif" } );
t.textContent = "This is some SVG text";

createChildNS

This method works identically to createChild, but takes an additional parameter as its first argument, the namespace of the element to be created. The created element does not implicitly use the namespace of its parent. Attribute namespaces work the same as with createChild.

The syntax is as follows:

element.createChild( string namespaceURI, string elementName, object attributes-values-list, integer index );

Example usage:

// insert foreignObject element, and append an HTML p element, then set value using textContent
var fo = root.createChild( "foreignObject", { x:10, y:250, width:"150", height:"150" } );
var p = fo.createChildNS( xhtmlns, "p" );
p.textContent = "This is some HTML text";


createElement

This method works in a similar manner and with the same syntax as the createChild method, with the exception that it does not insert the new element into the DOM, and therefore does not have an index parameter.

This is useful for constructing subtrees and inserting them into the DOM when they are fully constructed.

The syntax is as follows:

element.createElement( string elementName, object attributes-values-list );

Example usage:

// create and append a <g> (group) element into the document, with no attributes
var g = document.documentElement.createElement( "g" );

g.createChild( "title" );
t.textContent = "Mushroom Control";

g.createChild( "desc" );
t.textContent = "This widget lets you control mushrooms in some mysterious manner.  It looks like a yellow mushroom.";

g.createChild( "path", { d:"M300,340 L300,300 270,300 C270,230 350,230 350,300 L320,300 320,340 Z", fill:"yellow" } );

// put some more elements here 

// finally, append the element to the document in the normal way
document.documentElement.appendChild( g );

createElementNS

This method works identically to createElement, but takes an additional parameter as its first argument, the namespace of the element to be created. The created element does not implicitly use the namespace of its parent. Attribute namespaces work the same as with createElement.

The syntax is as follows:

element.createElementNS( string namespaceURI, string elementName, object attributes-values-list );

Example usage:

// insert foreignObject element, and append an HTML p element, then set value using textContent
var fo = root.createElement( "foreignObject", { x:10, y:250, width:"150", height:"150" } );
var d = fo.createElementNS( xhtmlns, "div", { class:"clutter" } );
var p = d.createChildNS( xhtmlns, "p" );
p.textContent = "Here is a list of things I found in my basement:";

// put some more HTML elements, lists and such, into the div

// finally, append the element to the document in the normal way
fo.appendChild( d );
document.documentElement.appendChild( fo );

General Applicability

Obviously, some of these new methods could be used for non-graphical content as well, including other languages like HTML, MathML, etc. If other Working Groups are interested in this idea, it may be advisable to develop this as part of a joint task force with the WebApps WG.

Common Graphical API

This approache is roughly similar to the Canvas API, and could be specified to return either a DOM or draw directly to a buffer and return a a reference to that, depending on whether it was called in a document/vector context ("<svg>", "<g>") or a raster image context ("<canvas>", "<image>"). This way, authors would only have to learn one syntax, and could use the same API for both, modulo differences in underlying rendering models.

(Doug: I call this idea of a single syntax to use create and manipulate both SVG and Canvas COG: Common Open Graphics.)


Backwards Compatibility and Deployment

As with any new feature, there will be a lag between when the specification is finished and when implementations deploy the functionality, and yet another lag between when the functionality is available and when users upgrade their browsers. In some cases, implementations that are not maintained may never change. Therefore, developers may be reluctant to use a new API. In most modern browsers, this risk can be ameliorated by use of a Javascript library to patch in the functionality to work in older browsers.

In most modern browsers, this can be a very short Javascript prototype extension (this will not work in Internet Explorer, but then, neither will SVG... it should work in any modern plug-in's script engine).

ISSUE: how would this work for Java? Could this be done?)


Example Code

You can see an example of this at svg2api.svg, including:

  • createChild / createChildNS
  • setAttrValues
  • getAttrValues

This example includes inserting elements (both shapes and non-rendering elements), inserting text elements, specifying the index of a new element in the childnode list, setting attributes en masse, and getting attributes as either a specific set or all attributes (click on the shapes).

For features of shared graphics methods between SVG and Canvas, see the COG API.