Index: src/webapp/tests/framework-tests/renderer/html/TreeBuilderTests.html
===================================================================
--- src/webapp/tests/framework-tests/renderer/html/TreeBuilderTests.html	(revision 0)
+++ src/webapp/tests/framework-tests/renderer/html/TreeBuilderTests.html	(revision 0)
@@ -0,0 +1,93 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr" id="html">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>TreeBuilder Tests</title>
+    
+    <!--  This is the jqUnit test css file -->
+    <link rel="Stylesheet" media="screen" href="../../../lib/qunit/css/testsuite.css" />
+    
+    <!--  These are the required javascript modules -->
+    <script src="../../../../lib/jquery/core/js/jquery.js" type="text/javascript"></script>
+    
+    <!--  These are the jqUnit test js files -->
+    <script type="text/javascript" src="../../../lib/qunit/js/testrunner.js"></script>
+    <script type="text/javascript" src="../../../test-core/jqUnit/js/jqUnit.js"></script>
+
+    <!--  Include the javascript files you are testing here -->
+    <script type="text/javascript" src="../../../../framework/core/js/Fluid.js"></script>
+    <script type="text/javascript" src="../../../../framework/core/js/FluidDOMUtilities.js"></script>
+    <script type="text/javascript" src="../../../../framework/core/js/DataBinding.js"></script>
+    <script type="text/javascript" src="../../../../lib/fastXmlPull/js/fastXmlPull.js"></script>
+    <script type="text/javascript" src="../../../../framework/renderer/js/fluidParser.js"></script>
+    <script type="text/javascript" src="../../../../framework/renderer/js/fluidRenderer.js"></script>
+    <script type="text/javascript" src="../../../../framework/renderer/TreeBuilder.js"></script>
+    
+    <!--  Include the javascript tests here -->
+    <script type="text/javascript" src="../../../test-core/utils/js/TestUtils.js"></script>
+    <script type="text/javascript" src="../js/TreeBuilderTests.js"></script>
+    
+</head>
+
+<body id="body">
+    <h1 id="header">TreeBuilder Tests</h1>
+    <h2 id="banner"></h2>
+    <h2 id="userAgent"></h2>
+    <ol id="tests"></ol>
+
+    <!-- Test HTML -->
+    <div id="main" style="display: none;">
+        
+        <!-- UIBound -->
+        <div id="uiBound">
+            <h1 class="value">Simple Value Goes Here</h1>
+            <form>
+                <input type="text" class="valueBinding" value="This element will be bound to a value binding">
+            </form>
+        </div>
+        
+        <!-- Branching UI Bound -->
+        <div id="branching-uiBound">
+            <ol class="authors">
+                <li class="author-name">Author name goes here</li>
+            </ol>
+        </div>
+        
+        <!-- UI Select Dropdown -->
+        <form action="#" id="uiSelect-dropdown">
+            <select name="authors" class="authors-dropdown">
+                <option value="First Author Name">First Author Name</option>
+                <option value="Second Author Name">Second Author Name</option>
+            </select>
+        </form>
+        
+        <!-- UI Container Table -->
+        <table id="uicontainer-table" summary="Rendered table of authors and their bios">
+            <tr>
+                <th>Author Name</th>
+                <th>Bio</th>
+            </tr>
+            <tr class="author-row">
+                <td class="name">Author name goes here</td>
+                <td class="bio">Bio goes here</td>
+            </tr>
+        </table>
+        
+        <!-- UISelect and UISelectChoice -->
+        <!-- Tying it all together: UIBound, UIContainer, UISelect with UISelectChoice -->
+        <div class="columns fl-col-flex3">
+            <div id="uiselect-choices" class="column fl-col">                
+                <h2 class="name">Author Name</h2>    
+                          
+                <ol class="checkbox-list">
+                    <li class="checkbox-container">
+                        <input class="checkbox" type="checkbox">
+                        <label for="checkbox" class="checkbox-label">Like</label>
+                    </li>
+                </ol>
+            </div>
+        </div>
+        
+    </div>
+</body>
+</html>
Index: src/webapp/tests/framework-tests/renderer/js/TreeBuilderTests.js
===================================================================
--- src/webapp/tests/framework-tests/renderer/js/TreeBuilderTests.js	(revision 0)
+++ src/webapp/tests/framework-tests/renderer/js/TreeBuilderTests.js	(revision 0)
@@ -0,0 +1,332 @@
+(function ($) {
+    var treeTests = jqUnit.testCase("Tree Builder Tests");
+    
+    var assertCutpointPresent = function (message, treeBuilder, name, isBranch) {
+        var qualifiedName = isBranch ? name + ":" : name;
+        jqUnit.assertDeepEq(message, {
+            id: qualifiedName,
+            selector: treeBuilder.selectorMap[name]
+        }, treeBuilder.uniqueCutpoints[qualifiedName]);
+    };
+    
+    var assertRenderedTextMatchesModel = function (dom, model, path) {
+        model = $.makeArray(model);
+        jqUnit.assertEquals("There should be as many DOM elements as items in the model.", model.length, dom.length);
+        for (var i = 0; i < model.length; i++) {
+            jqUnit.assertEquals("The text should match the model at position " + i + ".", 
+                                fluid.model.getBeanValue(model[i], path), 
+                                dom.eq(i).text());
+        }
+    };
+    
+    var authorsModel = [
+        {
+            name: "Isaac Asimov",
+            bio: "Asimov is widely considered a master of the science-fiction genre..."
+        },
+        {
+            name: "Douglas Adams",
+            bio: "Douglas Noel Adams (11 March 1952 – 11 May 2001) was an English author, dramatist, and musician."
+        }
+    ];
+    
+    treeTests.test("UIBound Tests", function () {
+        var template = $("uiBound");
+        var selectorMap = {
+            simpleValue: "h1.value",
+            boundValue: "input.valueBinding"
+        };
+        
+        var model = {
+            name: "Isaac Asimov",
+            bio: "Asimov is widely considered a master of the science-fiction genre..."
+        };
+        
+        var tb = fluid.treeBuilder(selectorMap, model);
+        
+        // Test value()
+        var expectedValueLeaf = {
+            ID: "simpleValue",
+            value: model.name
+        };
+        
+        var leaf = tb.value("simpleValue", model.name);
+        jqUnit.assertDeepEq("value should return a simple leaf with a value attribute and ID", 
+                            expectedValueLeaf, leaf);
+        assertCutpointPresent("The simpleValue selector should have been added to cutpoints structure.", 
+                              tb, "simpleValue");
+        
+        // Test boundValue()
+        var expectedBoundValueLeaf =  {
+            ID: "boundValue",
+            value: model.bio,
+            valueBinding: "bio"
+        };
+        
+        leaf = tb.boundValue("boundValue", "bio");
+        jqUnit.assertDeepEq("boundValue should return a simple leaf with a value attribute, a value and an ID", 
+                            expectedBoundValueLeaf, leaf);
+        assertCutpointPresent("The boundValue selector should have been added to cutpoints structure.",
+                              tb, "boundValue");
+        
+        // Test the whole tree.
+        jqUnit.assertDeepEq("A full and valid tree should be present on the tree builder.", {
+            children: [expectedValueLeaf, expectedBoundValueLeaf]
+        }, tb.tree);
+        
+        // Ensure that tree renders correctly.
+        tb.renderTree($("#uiBound"));
+        var result = $("#uiBound");
+        jqUnit.assertEquals("The simple value should have been rendered into the correct div.", model.name, $(".value", result).text());
+        jqUnit.assertEquals("The model-bound value should also have been rendered.", model.bio, $(".valueBinding", result).val());
+    });
+    
+    treeTests.test("Branching UIBound", function () {
+        var template = $("#branching-uiBound");
+        var selectorMap = {
+            authorName: ".author-name"
+        };
+        
+        var tb = fluid.treeBuilder(selectorMap, authorsModel);
+
+        var leaves = tb.value("authorName", authorsModel, "name");
+        jqUnit.assertDeepEq("values(), when called with an array, should return a component for each value in the model", [
+            {
+                ID: "authorName:",
+                value: authorsModel[0].name
+            },
+            {
+                ID: "authorName:",
+                value: authorsModel[1].name
+            }
+        ], leaves);         
+        assertCutpointPresent("The authorName selector should have been added to cutpoints structure with a colon.",
+                              tb, "authorName", true);
+        tb.renderTree(template);
+        var renderedListItems = $("#branching-uiBound ol").children();
+        jqUnit.assertEquals("Two list items should have been rendered.", 2, renderedListItems.length);
+        assertRenderedTextMatchesModel(renderedListItems, authorsModel, "name");
+    });
+    
+    treeTests.test("UISelect dropdown", function () {
+        var template = $("#uiSelect-dropdown");
+        var selectorMap = {
+            authors: ".authors-dropdown"
+        };
+        
+        var authorNames = fluid.transform(authorsModel, function (author) {
+            return author.name;
+        });
+        
+        var model = {
+            authors: authorNames,
+            labels: ["Asimov", "Adams"],
+            selection: authorsModel[1]
+        };
+        
+        var tb = fluid.treeBuilder(selectorMap, model);
+        
+        // Check that the correct UISelect component is generated.
+        var uiSelect = tb.selection("authors", "selection", "authors", "labels");
+        jqUnit.assertDeepEq("selection(), when called with EL paths, should render a value-bound UISelect component", {
+            ID: "authors",
+            selection: {
+                valuebinding: "selection"
+            },
+            optionlist: {
+                valuebinding: "authors"
+            },
+            optionnames: {
+                valuebinding: "labels"
+            }
+        }, uiSelect);
+        
+        // Make sure it all actually renders.
+        tb.renderTree(template);
+        var selectionEl = $(template.selector + " " + selectorMap.authors);
+        fluid.testUtils.assertNode("The select node still should be present", {
+            nodeName: "select",
+            children: [
+                {
+                    nodeName: "option",
+                    text: model.labels[0],
+                    value: model.authors[0]
+                },
+                {
+                    nodeName: "option",
+                    text: model.labels[1],
+                    value: model.authors[1]
+                }
+        ]}, selectionEl);
+        
+        // And try one that mixes direct values with bindings. There is a bug here that needs to be fixed.
+        tb = fluid.treeBuilder(selectorMap, model);
+        uiSelect = tb.selection("authors", model.selection, model.authors, "labels");
+        jqUnit.assertDeepEq("selection(), when called with both values and EL paths, should render the correct UISelect.", {
+            ID: "authors",
+            selection: model.selection,
+            optionlist: model.authors,
+            optionnames: {
+                valuebinding: "labels"
+            }
+        }, uiSelect);     
+    });
+    
+    treeTests.test("UIContainer table rows", function () {
+        var template = $("#uicontainer-table");
+        var selectorMap = {
+            authorRow: ".author-row",
+            name: ".name",
+            bio: ".bio"
+        };
+        
+        var tb = fluid.treeBuilder(selectorMap, authorsModel);
+        
+        var rows = tb.container("authorRow", authorsModel, function (author) {
+            tb.value("name", author.name);
+            tb.value("bio", author.bio);
+        });
+        jqUnit.assertDeepEq("UIContainer should have two children with the id authorRow:, each containing two children.", [
+            {
+                ID: "authorRow:",
+                children: [
+                    {
+                        ID: "name",
+                        value: authorsModel[0].name
+                    },
+                    {
+                        ID: "bio",
+                        value: authorsModel[0].bio
+                    }
+                ]
+            },
+                        {
+                ID: "authorRow:",
+                children: [
+                    {
+                        ID: "name",
+                        value: authorsModel[1].name
+                    },
+                    {
+                        ID: "bio",
+                        value: authorsModel[1].bio
+                    }
+                ]
+            }
+        ], rows);
+        assertCutpointPresent("The authorRow: cutpoint should be the only present in the list.", 
+                              tb, "authorRow", true);
+        
+        // Render the table.
+        tb.renderTree(template); // There's a tablebody in here. How did it get there?
+        var resultRows = $("tr.author-row", $(template.selector));
+        jqUnit.assertEquals("Two rows should have been rendered, one for each entry in the model.", 
+                            2, resultRows.length);        
+        resultRows.each(function (idx, row) {
+            var cells = $(row).find("td");
+            fluid.testUtils.assertNode("The row should contain name and bio cells bound to the model entry at index " + idx, [
+                {
+                    nodeName: "td",
+                    nodeText: authorsModel[idx].name
+                },
+                {
+                    nodeName: "td",
+                    nodeText: authorsModel[idx].bio
+                }
+            ], cells);
+        });
+    });
+    
+    treeTests.test("UISelect with SelectChoices", function () {
+        var template = $("#uiselect-choices");
+        var selectorMap = {
+            authorName: ".name",
+            booksRow: ".checkbox-container",
+            booksChoice: ".checkbox",
+            booksLabel: ".checkbox-label"
+        };
+        
+        var asimovAndBooks = authorsModel[0];
+        asimovAndBooks.books = ["Foundation", "Foundation and Empire", "Second Foundation"];
+        var model = {
+            author: asimovAndBooks,
+            selections: []
+        };
+        
+        var tb = fluid.treeBuilder(selectorMap, asimovAndBooks);
+        
+        var components = tb.checkbox("books", model.selections, model.author.books, model.author.books);
+        jqUnit.assertDeepEq("The tree should start with a UISelect", {
+            ID: "books",
+            selection:  model.selections,
+            optionlist: model.author.books,
+            optionnames: model.author.books
+        }, components[0]);
+        jqUnit.assertDeepEq("A set of UIContainers should follow, each containing a checkbox and label.", [
+            {
+                ID: "booksRow:",
+                children: [
+                    {
+                        ID: "booksChoice",
+                        parentRelativeID: "..::books",
+                        choiceIndex: 0
+                    },
+                    {
+                        ID: "booksLabel",
+                        parentRelativeID: "..::books",
+                        choiceIndex: 0
+                    }
+                ]
+            },
+            {
+                ID: "booksRow:",
+                children: [
+                    {
+                        ID: "booksChoice",
+                        parentRelativeID: "..::books",
+                        choiceIndex: 1
+                    },
+                    {
+                        ID: "booksLabel",
+                        parentRelativeID: "..::books",
+                        choiceIndex: 1
+                    }
+                ]
+                
+            },
+            {
+                ID: "booksRow:",
+                children: [
+                    {
+                        ID: "booksChoice",
+                        parentRelativeID: "..::books",
+                        choiceIndex: 2
+                    },
+                    {
+                        ID: "booksLabel",
+                        parentRelativeID: "..::books",
+                        choiceIndex: 2
+                    }
+                ]
+            }
+        ], components.slice(1));
+        
+        tb.renderTree(template);
+        
+        var checkboxRows = $(template.selector).find("ol.checkbox-list li");
+        jqUnit.assertEquals("There should be three rendered checkbox rows.", 3, checkboxRows.length);
+        checkboxRows.each(function (idx, checkboxContainer) {
+            var children = $(checkboxContainer).children();
+            jqUnit.assertEquals("The checkbox container at index " + idx + " should contain two children.", 2, children.length);
+            fluid.testUtils.assertNode("The first child should be the checkbox.", {
+                nodeName: "input",
+                type: "checkbox"
+            }, children.eq(0));
+            fluid.testUtils.assertNode("The second child should be the label.", {
+                nodeName: "label",
+                nodeText: asimovAndBooks.books[idx]
+            }, children.eq(1));
+        });
+    });
+    
+})(jQuery);
Index: src/webapp/framework/renderer/TreeBuilder.js
===================================================================
--- src/webapp/framework/renderer/TreeBuilder.js	(revision 0)
+++ src/webapp/framework/renderer/TreeBuilder.js	(revision 0)
@@ -0,0 +1,217 @@
+
+/*global jQuery*/
+/*global fluid_1_2*/
+
+fluid_1_2 = fluid_1_2 || {};
+
+(function ($, fluid) {
+
+    // TODO:
+    //  * Refactor repetitive code for one vs. many
+    //  * Check consistency of model paths... will we have a problem?
+    //  * Implement decorate()
+    //  * Bugs:
+    //    - selection() assumes a non-array "selection" parameter is an EL path.
+    //    - checkbox() doesn't work with an EL path instead of a literal value for "options"
+    
+    fluid.treeBuilder = function (selectorMap, model) {
+        var that = {
+            selectorMap: selectorMap,
+            model: model,
+            uniqueCutpoints: {},
+            tree: {
+                children: []
+            },
+            templates: null,
+            contexts: []
+        };
+        
+        var setContext = function (context) {
+            that.contexts.push(context);
+            that.currentContext = context;
+        };
+        
+        var restorePreviousContext = function () {
+            that.contexts.pop();
+            that.currentContext = that.contexts[that.contexts.length - 1];
+        };
+        
+        that.addCutpoint = function (id) {
+            var selector = that.selectorMap[(id.charAt(id.length - 1) === ":") ? id.slice(0, id.length - 1) : id];
+            
+            if (selector) {
+                that.uniqueCutpoints[id] = {
+                    id: id,
+                    selector: selector
+                };
+            }
+        };
+        
+        that.generateCutpointsArray = function () {
+            var cutpoints = [];
+            for (var id in that.uniqueCutpoints) {
+                cutpoints.push(that.uniqueCutpoints[id]);
+            }     
+            return cutpoints;
+        };
+        
+        that.addComponent = function (name, leaf) {
+            that.addCutpoint(name);
+            that.currentContext.push(leaf);
+            return leaf;
+        };
+        
+        var addValues = function (name, values, path, eachFn) {
+            return fluid.transform(values, function (value, idx) {
+                if (path) {
+                    value = fluid.model.getBeanValue(value, path);
+                }
+                
+                var component = that.addComponent(name, {
+                    ID: name,
+                    value: value
+                });
+                
+                if (eachFn) {
+                    eachFn(value, idx, component);
+                }
+                
+                return component;
+            });
+        };
+      
+        that.value = function (name, value, path, eachFn) {
+            var isBranch = $.isArray(value);
+            var leaves = addValues(isBranch ? name + ":" : name, $.makeArray(value), path, eachFn);
+            return isBranch ? leaves : leaves[0];
+        };
+        
+        var addBoundValues = function (name, paths, eachFn) {
+            return fluid.transform(paths, function (path, idx) {
+                var component = that.addComponent(name, {
+                    ID: name,
+                    value: fluid.model.getBeanValue(that.model, path),
+                    valueBinding: path
+                });
+                
+                if (eachFn) {
+                    eachFn(value, idx, component);
+                }
+                
+                return component;
+            });
+        };
+        
+        that.boundValue = function (name, path, eachFn) {
+            var isBranch = $.isArray(path);
+            var leaves = addBoundValues(isBranch ? name + ":" : name, $.makeArray(path), eachFn);
+            return isBranch ? leaves : leaves[0];
+        };
+        
+        var setValueOrBinding = function (obj, key, value) {
+            obj[key] = $.isArray(value) ? value : {valuebinding: value};
+        };
+        
+        that.selection = function (name, selection, options, labels) {
+            var uiSelect = {
+                ID: name
+            };
+            setValueOrBinding(uiSelect, "selection", selection); // Bug. selection can be a list or a string. Can't assume a string is an EL path.
+            setValueOrBinding(uiSelect, "optionlist", options);
+            setValueOrBinding(uiSelect, "optionnames", labels);
+            
+            return that.addComponent(name, uiSelect);
+        };
+        
+        that.dropdown = that.selection;
+        
+        that.choice = function (name, choiceIndex, selection) {
+            return that.addComponent(name, {
+                ID: name,
+                parentRelativeID: "..::" + selection.ID, // Bug: need to determine the correct path for the selection, not just assume it's above us.
+                choiceIndex: choiceIndex
+            });
+        };
+        
+        that.label = function (name, choiceIndex, selection) {
+            return that.choice(name, choiceIndex, selection);
+        };
+        
+        that.choiceWithLabel = function (choiceName, labelName, choiceIndex, selection) {
+            return [that.choice(choiceName, choiceIndex, selection), that.label(labelName, choiceIndex, selection)];
+        };
+        
+        var addSelectionWithChoice = function (namesObj, selection, options, labels, eachFn) {
+            var selectComponent = that.selection(namesObj.selection, selection, options, labels);
+            var choiceContainers = that.container(namesObj.row, options, function (option, idx, component) {
+                that.choiceWithLabel(namesObj.choice, namesObj.label, idx, selectComponent);
+                if (eachFn) {
+                    eachFn(option, idx, component);
+                }
+            });
+            
+            return [selectComponent].concat(choiceContainers);
+        };
+        
+        that.selectionWithChoice = function (namesObj, selection, options, labels, eachFn) {           
+            // Provide default names if the user didn't give us them all.
+            if (typeof(namesObj) === "string") {
+                namesObj = {
+                    selection: namesObj,
+                    row: namesObj + "Row",
+                    choice: namesObj + "Choice",
+                    label: namesObj + "Label"    
+                };
+            }
+            
+            // Huge bug... we assume options is a literal, not an EL path.
+            return addSelectionWithChoice(namesObj, selection, $.makeArray(options), labels, eachFn);
+        };
+        
+        that.checkbox = that.selectionWithChoice;
+        that.radiobutton = that.checkbox;
+
+        var addContainers = function (name, list, eachFn) {
+            var uiContainer;
+            return fluid.transform(list, function (item, idx) {
+                uiContainer = {
+                    ID: name,
+                    children: []
+                };
+                that.addComponent(name, uiContainer);
+                
+                setContext(uiContainer.children);
+                if (eachFn) {
+                    eachFn(item, idx, uiContainer);
+                }
+                restorePreviousContext();
+                
+                return uiContainer;
+            }); 
+        };
+             
+        that.container = function (name, value, eachFn) {
+            var isBranch = $.isArray(value);
+            var components = addContainers(isBranch ? name + ":" : name, $.makeArray(value), eachFn);
+            return isBranch ? components : components[0];
+        };
+        
+        that.repeat = that.container;
+        
+        that.renderTree = function (container, options) {
+            options = $.extend(true, {
+                model: that.model,
+                cutpoints: that.generateCutpointsArray()
+            }, options);
+            
+            if (that.templates) {
+                fluid.reRender(that.templates, container, that.tree, options);
+            } else {
+                fluid.selfRender(container, that.tree, options);
+            }
+        };
+        
+        setContext(that.tree.children);
+        return that;
+    };
+})(jQuery, fluid_1_2);
