Wednesday, December 28, 2011

Simplify with an XML data model - Part 1

Part 1: Binding from XML to the HTML form.


When doing data binding, you typically need to map a field in the UI with some part of the data model. XPath is a terrific solution to this. All you need is an XPath expression that evaluates to a single element or attribute node. This allows for bidirectional binding between HTML <form> elements and XML nodes. Binding to read-only HTML elements can be done with more complex XPath expressions and involve several nodes and functions, e.g. concatenating several fields together. It might make sense to allow for one input to map to many nodes in the future.


Interactive Example:

Binding concept:
Once the binding Javascript is loaded, All we should need to add to our HTML forms is a little attribute that contains an XPath expression which is linked to one node:


<input data-dxb-bind="/root/address[1]/street/@number" />
(The data... attribute is a new feature in HTML 5 that allows for user defined attributes that do not affect layout)

All major browsers support XML DOM XPath evaluation which makes things easy. In the spirit of making things easy, we will also use a library called Sarissa to get around some browser differences with XML manipulation. Sarissa is an “ECMAScript library acting as a cross-browser wrapper for native XML APIs”.

In this example we will load XML into the browser, bind an input field to the XML and display the updated XML to the user. There are some utility functions used here that are included in the example download.

The first part of our Javascript will create an XML DOM data model from a string.
     <script type="text/javascript" src="sarissa.js"></script>
     ...
     // Use Sarissa to create an XML DOM document 
     var xmlDataModel = Sarissa.getDomDocument();
     var xmlString = "<root><primary>yes</primary><sex/><address><street number=\"\"></street><state></state></address></root>";
     xmlDataModel = (new DOMParser()).parseFromString(xmlString, "text/xml");
Sarissa makes it easy to deal with XML DOM in Javascript across browsers.

Next, we need a function that will take in an XPath expression and an HTML element and update the data model.


    // Updates the XML DOM from the element
    function setXmlValue(element, xpath, xmlDom) {
        //Get the node from the model
        var node = getNode(xpath, xmlDom);

        if(element.type == 'checkbox' || element.type == 'radio'){
            if(element.checked){
                node.textContent = element.value;
            }else{
                node.textContent = null;                
            }
        }else{
            // Set the node's content to the value of the HTML element
            node.textContent = element.value;
        }
    }
This function hides some of the logic we will need for different types of HTML form input.
The real magic happens in the call to getNode.
    function getNode(xpath, xmlDom) {
        try {
            // Evaluate the XPath and return the node
            var xpathResult = xmlDom.evaluate(xpath, xmlDom, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
            return xpathResult.singleNodeValue;
        } catch (e) {
            outputError(e);
        }
    }
The key here is the evaluate function. It will evaluate the XPath against our data model to get a node set. Once we have that node, we can use the element’s value to update our data model.

After that we will need a function that binds changes in the form to changes in the model.


    //Binds an element to the corresponding XPath expression
    function bind(element, xpath, xmlDom) {

        // Method to update and pretty print the model
        var setModelValue = function() {
            setXmlValue(element, xpath, xmlDom);
            document.getElementById("dataModelPrettyPrint").innerHTML = printXml(xmlDom.documentElement, 0);
        };

        // Add form event handlers to update the model
        element.onchange = setModelValue;
        if (element.type == 'text') {
            element.onkeyup = setModelValue;
        }
    }
We will use the DOM events onchange and onkeyup and set them to our setModelValue function.

The final step is to parse the document after it is done loading, and call the bind function for each form input that defines a binding path:


    // Parses html for dxb attributes and binds form input elements
    function init() {
        // Loop through all form inputs and bind if they contain "data-dxb-bind" attributes
        var inputTags = document.getElementsByTagName("input");
        for ( var i = 0; i < inputTags.length; i++) {
            if (inputTags[i].attributes["data-dxb-bind"]) {
                bind(inputTags[i], inputTags[i].attributes["data-dxb-bind"].value, xmlDataModel);
            }
        }
        inputTags = document.getElementsByTagName("select");
        for ( var i = 0; i < inputTags.length; i++) {
            if (inputTags[i].attributes["data-dxb-bind"]) {
                bind(inputTags[i], inputTags[i].attributes["data-dxb-bind"].value, xmlDataModel);
            }
        }
        
        // Pretty print the XML
        document.getElementById("dataModelPrettyPrint").innerHTML = printXml(xmlDataModel.documentElement, 0);
    }

    // Initialize when the document is done loading
    window.onload = init;
That takes care of the Javascript, now all we need is a simple HTML form, and a place to pretty print out the xml:


<body>
    <form>
        Street Number:
        <input data-dxb-bind="/root/address[1]/street/@number" />
        <br /> 
        State: 
        <select data-dxb-bind="/root/address[1]/state">
            <option selected="selected" ></option>
            <option>NY</option>
            <option>NJ</option>
            <option>MA</option>
            <option>PA</option>
        </select>
        <br />
        Primary:
        <input type="checkbox" value="yes" data-dxb-bind="/root/primary">
        <br />
        Sex:
        <br />
        <input type="radio" name="sex" value="male" data-dxb-bind="/root/sex"/> Male
        <br />
        <input type="radio" name="sex" value="female" data-dxb-bind="/root/sex"/> Female
        <br />
        <input type="radio" name="sex" value="not given" data-dxb-bind="/root/sex"/> Other
    </form>
    <div id="dataModelPrettyPrint"></div>
</body>



You can view this example here.
As always you can download all the source for these examples as a zip.
Remember you will need the Sarrissa library.

So that seemed pretty easy right? Now you have an XML DOM object, you can easily send it to a server asynchronously with an XMLHttpRequest. Another benefit is that your HTML code is pretty much uncluttered with code. I hope this has shown how easy it can be to get XML from an HTML form.



Next: Simplify with an XML data model - Part 2

No comments:

Post a Comment