37

I am working on a Web application that needs to send XML to a server backend. I'd like to build a XML document in-memory on the client-side, but using XML manipulation routines, instead of appending countless strings together. I'm hoping jQuery can help me out.

Let's say I need to generate this (toy) XML document with JavaScript:

<report>
    <submitter>
        <name>John Doe</name>
    </submitter>
    <students>
        <student>
            <name>Alice</name>
            <grade>80</grade>
        </student>
        <student>
            <name>Bob</name>
            <grade>90</grade>
        </student>
    </students>
</report>

To start, I need to create some kind of an XML document object with the "report" root. I'm assuming one of these should be close, but none of them work quite right, and/or I can't quite figure out how to use the object properly:

function generateDocument1()
{
    var report = $('<report></report>');
    return report;
}

function generateDocument2()
{
    var report = document.implementation.createDocument(null, "report", null);

    return new XMLSerializer().serializeToString(report);   
}

function createXmlDocument(string)
{
    var doc;
    if (window.DOMParser)
    {
        parser = new DOMParser();
        doc = parser.parseFromString(string, "application/xml");
    }
    else // Internet Explorer
    {
        doc = new ActiveXObject("Microsoft.XMLDOM");
        doc.async = "false";
        doc.loadXML(string); 
    }
    return doc;
}

function generateDocument3()
{
    var report = createXmlDocument('<report></report>');

    return report;
}

Now I want to create and append elements. How do I do that? I imagine it's something like this:

function generateReportXml()
{
    // Somehow generate the XML document object with root
    var report = /*???*/;

    // Somehow create the XML nodes
    var submitter = /*???*/;
    var name = /*???*/;

    // Somehow append name to submitter, and submitter to report
    submitter.append(name); /*???*/
    report.append(submitter); /*???*/

    // ... append the rest of the XML

    return report;
}

Any ideas?

tshepang
  • 12,111
  • 21
  • 91
  • 136
Shaggy Frog
  • 27,575
  • 16
  • 91
  • 128
  • Make sure you check out @AlexanderN solution at the end if you want a great plugin js method to create XML structures, including attributes and CDATA. – whyoz May 16 '14 at 18:48

5 Answers5

65

The second approach seems a good way to go. It was designed to work with XML documents. Once you have the document object created, use the standard XML DOM manipulation methods to construct the entire document.

// creates a Document object with root "<report>"
var doc = document.implementation.createDocument(null, "report", null);

// create the <submitter>, <name>, and text node
var submitterElement = doc.createElement("submitter");
var nameElement = doc.createElement("name");
var name = doc.createTextNode("John Doe");

// append nodes to parents
nameElement.appendChild(name);
submitterElement.appendChild(nameElement);

// append to document
doc.documentElement.appendChild(submitterElement);

This may seem a little verbose but is the right way to build the XML document. jQuery does not actually construct any XML document, but just relies on the innerHTML property to parse and reconstruct a DOM given an HTML string. The problem with that approach is that when tag names in your XML collide with tag names in HTML such as <table> or <option>, then the results can be unpredictable. (EDIT: since 1.5 there's jQuery.parseXML() which does actually construct an XML document and thus avoids these problems — for parsing only.)

To cut down on the verboseness, write a small helper library, or maybe a jQuery plugin to construct the document.

Here's a quick and dirty solution to creating a XML document using a recursive approach.

// use this document for creating XML
var doc = document.implementation.createDocument(null, null, null);

// function that creates the XML structure
function Σ() {
    var node = doc.createElement(arguments[0]), text, child;

    for(var i = 1; i < arguments.length; i++) {
        child = arguments[i];
        if(typeof child == 'string') {
            child = doc.createTextNode(child);
        }
        node.appendChild(child);
    }

    return node;
};

// create the XML structure recursively
Σ('report',
    Σ('submitter',
        Σ('name', 'John Doe')
    ),
    Σ('students',
        Σ('student',
            Σ('name', 'Alice'),
            Σ('grade', '80')
        ),
        Σ('student',
            Σ('name', 'Bob'),
            Σ('grade', '90')
        )
    )
);

Returns:

<report>​
    <submitter>​
        <name>​John Doe​</name>​
    </submitter>​
    <students>​
        <student>​
            <name>​Alice​</name>​
            <grade>​80​</grade>​
        </student>​
        <student>​
            <name>​Bob​</name>​
            <grade>​90​</grade>​
        </student>​
    </students>​
</report>​

See example

robert4
  • 1,072
  • 15
  • 20
Anurag
  • 140,337
  • 36
  • 221
  • 257
  • 4
    This may or may not be quick + dirty, but it's definitely very pretty! – Tao Jan 24 '12 at 19:56
  • 8
    Combine with `new XMLSerializer().serializeToString(yourXml)` and it forms a great way to build structured documents to send in AJAX messages. Superb! – Donal Fellows Aug 17 '12 at 13:11
  • This is what I need but somehow I could not make it work, this http://jsfiddle.net/vquyT/1/ is not working either ? Could you update this link or am I missing something? – SpaceDust__ Oct 24 '12 at 17:40
  • @SpaceDust - I was able to run the jsfiddle on Safari 6. What browser are you trying this on, and are there any errors or exceptions you see? – Anurag Oct 24 '12 at 18:26
  • @Anurag is that Σ object is a string or array? I mean I want to create a whole string from that object how can I do that? – SpaceDust__ Oct 25 '12 at 16:40
  • @SpaceDust - The `Σ` is a function and it returns an object of `Node` I believe. It's not a string, but an object representing an XML node. You could convert it to a string yielding something like `"John"` for instance. – Anurag Oct 25 '12 at 16:52
  • Why the first block of code errors with "NotFoundError: Failed to execute 'appendChild' on 'Node': The new child element is null." when tested in the console? – Yasen May 23 '14 at 06:45
  • I find this to be really awesome by the way. Just a suggestion... it looks like `text` var isn't being used anymore in the jsfiddle example. Maybe sync up the code examples from this answer and the jsfiddle. – pjdicke Jun 02 '14 at 17:39
  • I needed to have attributes attached to my nodes, so I [created a fork](http://jsfiddle.net/samwyse/qs7dLxc6/) that uses objects to let you set them. – samwyse Mar 27 '15 at 13:47
  • For namespaces use `createElementNS(ns, 'nameoftag')`. – Christophe Roussy Oct 19 '15 at 08:57
  • For the pretty printing, indentation, see formatXML js https://gist.github.com/kurtsson/3f1c8efc0ccd549c9e31 – Christophe Roussy Oct 19 '15 at 10:28
  • @ChristopheRoussy Is that format function really using regex to do the parsing? Bad idea! – Anurag Oct 19 '15 at 19:25
  • @Anurag This regex parsing is indeed a bit scary but it can be useful for debugging/pretty printing. Can you suggest other ways to pretty print xml using JS ? – Christophe Roussy Oct 20 '15 at 07:42
  • "The problem with that approach is that when tag names in your XML collide with tag names in HTML such as or
    – doliver Jun 23 '16 at 15:51
  • @doliver yes there are adjustments that are done when parsing something as HTML. Like you may find an extra thead or tfoot inserted in a table element. There are quite a few questions on stackoverflow for problems with parsing XML content with jQuery. I'll try to include some if I get time. – Anurag Jun 25 '16 at 02:19
  • @doliver Found this older related [thread](http://stackoverflow.com/questions/2908899/jquery-wont-parse-xml-with-nodes-called-option). – Anurag Jun 29 '16 at 18:36
26

Without addressing whether you should use jQuery to build XML, here are some ideas on how you might do it:

// Simple helper function creates a new element from a name, so you don't have to add the brackets etc.
$.createElement = function(name)
{
    return $('<'+name+' />');
};

// JQ plugin appends a new element created from 'name' to each matched element.
$.fn.appendNewElement = function(name)
{
    this.each(function(i)
    {
        $(this).append('<'+name+' />');
    });
    return this;
}

/* xml root element - because html() does not include the root element and we want to 
 * include <report /> in the output. There may be a better way to do this.
 */
var $root = $('<XMLDocument />');

$root.append
(
    // one method of adding a basic structure
    $('<report />').append
    (
        $('<submitter />').append
        (
            $('<name />').text('John Doe')
        )
    )
    // example of our plugin
    .appendNewElement('students')
);

// get a reference to report
var $report = $root.find('report');

// get a reference to students
var $students = $report.find('students');
// or find students from the $root like this: $root.find('report>students');

// create 'Alice'
var $newStudent = $.createElement('student');
// add 'name' element using standard jQuery
$newStudent.append($('<name />').text('Alice'));
// add 'grade' element using our helper
$newStudent.append($.createElement('grade').text('80'));

// add 'Alice' to <students />
$students.append($newStudent);

// create 'Bob'
$newStudent = $.createElement('student');
$newStudent.append($('<name />').text('Bob'));
$newStudent.append($.createElement('grade').text('90'));

// add 'Bob' to <students />
$students.append($newStudent);

// display the markup as text
alert($root.html());

Output:

<report>
    <submitter>
        <name>John Doe</name>
    </submitter>
    <students>
        <student>
            <name>Alice</name>
            <grade>80</grade>
        </student>
        <student>
            <name>Bob</name>
            <grade>90</grade>
        </student>
    </students>
</report>
rath
  • 427
  • 4
  • 6
  • 6
    A problem with this approach is that in HTML tag names are case insensitive whereas in XML they are case sensitive. Hence jQuery will convert all tags to lowercase, which might not be what you want. – Jeroen Ooms Feb 09 '15 at 01:52
  • 1
    Good warning “Without addressing whether you _should_…”, it made me think over and helped – robert4 Jun 05 '15 at 07:05
2

I've found Ariel Flesler's XMLWriter constructor function to be a good start for creating XML from scratch (in memory), take a look at this

http://flesler.blogspot.com/2008/03/xmlwriter-for-javascript.html

Example

function test(){    
   // XMLWriter will use DOMParser or Microsoft.XMLDOM
   var v = new  XMLWriter();
   v.writeStartDocument(true);
   v.writeElementString('test','Hello World');
   v.writeAttributeString('foo','bar');
   v.writeEndDocument();
   console.log( v.flush() );
}

Result

<?xml version="1.0" encoding="ISO-8859-1" standalone="true" ?>
<test foo="bar">Hello World</test>

A couple of caveats, it doesn't escape strings and the syntax can get coyote++ ugly.

Alex Nolasco
  • 18,750
  • 9
  • 86
  • 81
1

Have you considered JSON? You could save the data using objects. Then you could use JSON.stringify(obj); and send that to the server.

a simple example

var obj = new student('Alice',80);

function student(a,b){
  this.name=a;
  this.grade=b;
}

function sendToServer(){
  var dataString = JSON.stringify(obj);
  //the HTTP request
}
qw3n
  • 6,236
  • 6
  • 33
  • 62
  • JSON is not capable to store lots of data structures, while XML can store anything. – Dima Jul 31 '13 at 10:06
1

If your desired XML structure can be represented in a JavaScript object having the same structure, then you could create such an object and use the following function to convert that object to XML:

/*  Arguments:
      name: name of the root XML-element 
      val: the data to convert to XML
    Returns: XML string 
    Example: toXml("root", { items: { item: [1, 2] } })
      returns: "<root><items><item>1</item><item>2</item></items></root>"
*/
function toXml(name, val) {
    const map = {"<":"&lt;", ">":"&gt;", "&":"&amp;", "'":"&apos", '"':"&quot;"};
    if (Array.isArray(val)) return val.map(elem => toXml(name, elem)).join``;
    const content =  Object(val) === val
        ? Object.keys(val).map(key => toXml(key, val[key])).join``
        : String(val).replace(/[<>&'"]/g, m => map[m]);
    return `<${name}>${content}</${name}>`;
}

// Example:
const report = {
    submitter: { name: "John Doe" },
    students: {
        student: [{ name: "Alice", grade: 80 }, 
                  { name: "Bob",   grade: 90 }]
    }
};

console.log(
    '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' +
    toXml("report", report));
trincot
  • 317,000
  • 35
  • 244
  • 286