递归构建器嵌套问题

时间:2013-05-21 22:12:40

标签: javascript dom recursion builder

我正在尝试将JSON / JavaScript对象解析为HTML ...其中大部分工作正常,但我无法弄清楚我的递归构建方法(App.Layout.Form.DocumentDirector)......

它不应该嵌套(追加)元素。请参阅http://jsfiddle.net/blogshop/DSxft/6/

上的示例和代码

有人可以帮我搞清楚吗?构建方法在408行(在小提琴中 - 向下滚动DocumentDirector只需稍微查看一下)。谢谢!


构建方法

build: function (obj, level) {
        var that = this,
            builder = this.getBuilder(),
            iterator = Object.create(App.Utilities.RecursiveIterator(obj)),
            level = level || 0,
            prev = this._prevLevel || 0,
            next = level,
            key,
            current,
            attributes,
            pair,
            children,
            hasType = false,
            maxNesting = false;

        do {
            // Get the current node and build it
            key = iterator.key();
            current = iterator.current();

            console.log(iterator.key() + ": " + iterator.current().toSource());

            if (current.hasOwnProperty('type') && current.type !== '') {

                if (current.hasOwnProperty('type') && current.type !== '') {
                    //console.log(iterator.current().type);

                    attributes = {};

                    $.each(current, function(key, value) {
                        if (that._validAttributes.indexOf(key) !== -1) {
                            attributes[key] = value;
                        }
                    });
                    //console.log(attributes);
                }

                //console.log("Prev: " + prev);
                console.log("Level: " + level);

                if (level == prev && level == 0) {
                    builder.append(current.type, attributes);
                } else if (level == prev && level > 0) {
                    builder.add(current.type, attributes);                          
                } else if (level < prev && level > 0) {
                    builder.parent().append(current.type, attributes);
                } else if (level > prev && level > 0) {
                    builder.append(current.type, attributes);
                } else {
                    // Do nothing
                }

                if (current.hasOwnProperty('label')) {
                    var label = builder.addBefore(builder.getCurrent(), 'label');
                    builder.text(current.label, label);
                }
            }

            // Are there child nodes? If so, recurse...
            if (iterator.hasChildren()) {
                children = iterator.getChildren();

                if (this.isCollection(key, current)) {
                    //console.log("I am a container");
                }


                //console.log("I have children");
                console.log("-----------------------------");

                hasType = (current.hasOwnProperty('type') && current.type !== '') ? current.type : false;

                if (hasType) {
                    maxNesting = (this.isVoid(current.type)) ? true : false;
                }

                if (maxNesting === false) {
                    next = level;

                    if (hasType) {
                        this._prevLevel = level;
                        next = level + 1;
                    }

                    this.build(children, next);
                }

            } else {
                if (current.hasOwnProperty('type') && current.type !== '') {
                    //console.log("I am childless");
                    console.log("-----------------------------");
                }
            }
            iterator.next();
        } while (iterator.hasNext());

        if (current && current.hasOwnProperty('type') && current.type !== '') {

            if (iterator.hasNext() === false) {
                //console.log("<----- MOVING UP " + parseInt(level - prev) + " LEVELS ----->");
                //console.log("Diff: " + parseInt(level - prev));
                //console.log("Level: " + level);
                //console.log("Prev: " + prev);

                builder.parent();
                this._prevLevel = level - prev;
            }
        }
    },

附加信息

DocumentDirector: function (builder) {
    var director = Object.create({
        _builder: {},
        _validCollection: ['sections', 'forms', 'fieldsets', 'rows', 'fields'],
        _validAttributes: ['id', 'name'],
        _voidElements: ['base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'img', 'input', 'link', 'meta', 'param', 'source'],
        _inputElements: ['text', 'select', 'radio', 'checkbox', 'textarea', 'datepicker', 'yesno'],
        _prevLevel: 0,

        init: function (builder) {
            builder = builder || '';
            if (builder) this.setBuilder(builder);

            return this;
        },
        setBuilder: function (builder) {
            this._builder = builder;

            return this;
        },
        getBuilder: function () {
            return this._builder;
        },
        isCollection: function (key, node) {
            // Collections MUST have a key, as they don't have a type
            key = key || '';
            if (key === '') return false;

            return (node.constructor === Array && this._validCollection.indexOf(key) !== -1) ? true : false;
        },
        isVoid: function (type) {
            var isVoidElement = false;

            if (this._voidElements.indexOf(type.toString()) !== -1) {
                isVoidElement = true;
            }

            if (this._inputElements.indexOf(type.toString()) !== -1) {
                isVoidElement = true;
            }

            return isVoidElement;
        },
        build: function (obj, level) {
            var that = this,
                builder = this.getBuilder(),
                iterator = Object.create(App.Utilities.RecursiveIterator(obj)),
                level = level || 0,
                prev = this._prevLevel || 0,
                next = level,
                key,
                current,
                attributes,
                pair,
                children,
                hasType = false,
                maxNesting = false;

            do {
                // Get the current node and build it
                key = iterator.key();
                current = iterator.current();

                console.log(iterator.key() + ": " + iterator.current().toSource());

                if (current.hasOwnProperty('type') && current.type !== '') {

                    if (current.hasOwnProperty('type') && current.type !== '') {
                        //console.log(iterator.current().type);

                        attributes = {};

                        $.each(current, function(key, value) {
                            if (that._validAttributes.indexOf(key) !== -1) {
                                attributes[key] = value;
                            }
                        });
                        //console.log(attributes);
                    }

                    //console.log("Prev: " + prev);
                    console.log("Level: " + level);

                    if (level == prev && level == 0) {
                        builder.append(current.type, attributes);
                    } else if (level == prev && level > 0) {
                        builder.add(current.type, attributes);                          
                    } else if (level < prev && level > 0) {
                        builder.parent().append(current.type, attributes);
                    } else if (level > prev && level > 0) {
                        builder.append(current.type, attributes);
                    } else {
                        // Do nothing
                    }

                    if (current.hasOwnProperty('label')) {
                        var label = builder.addBefore(builder.getCurrent(), 'label');
                        builder.text(current.label, label);
                    }
                }

                // Are there child nodes? If so, recurse...
                if (iterator.hasChildren()) {
                    children = iterator.getChildren();

                    if (this.isCollection(key, current)) {
                        //console.log("I am a container");
                    }


                    //console.log("I have children");
                    console.log("-----------------------------");

                    hasType = (current.hasOwnProperty('type') && current.type !== '') ? current.type : false;

                    if (hasType) {
                        maxNesting = (this.isVoid(current.type)) ? true : false;
                    }

                    if (maxNesting === false) {
                        next = level;

                        if (hasType) {
                            this._prevLevel = level;
                            next = level + 1;
                        }

                        this.build(children, next);
                    }

                } else {
                    if (current.hasOwnProperty('type') && current.type !== '') {
                        //console.log("I am childless");
                        console.log("-----------------------------");
                    }
                }
                iterator.next();
            } while (iterator.hasNext());

            if (current && current.hasOwnProperty('type') && current.type !== '') {

                if (iterator.hasNext() === false) {
                    //console.log("<----- MOVING UP " + parseInt(level - prev) + " LEVELS ----->");
                    //console.log("Diff: " + parseInt(level - prev));
                    //console.log("Level: " + level);
                    //console.log("Prev: " + prev);

                    builder.parent();
                    this._prevLevel = level - prev;
                }
            }
        },
        getDocument: function () {
            return this.getBuilder().getDocument();
        }
    });

    return director.init(builder);
}

DOMBuilder(注入到DocumentDirector中 - 处理DOM操作):

DOMBuilder: function () {
    var domBuilder = Object.create({
        _document: {},
        _rootNode: {},
        _currentNode: {},

        init: function (viewModel, domBuilder) {
            var doc, rootNode;

            this._document = doc = document.createDocumentFragment();
            this._rootNode = rootNode = this.appendNode(doc, 'section');
            this._currentNode = rootNode;

            return this;
        },

        /* Generic methods
        ------------------ */

        /**
         *  Returns the document
         *
         *  @return DOM Node: The DOM document fragment
         */
        getDocument: function () {
            return this._document;
        },
        /**
         *  Returns the root node
         *
         *  @return DOM Node: The root node
         */
        getRoot: function () {
            return this._rootNode;
        },
        /**
         *  Returns the current node
         *
         *  @return DOM Node: The current node
         */
        getCurrent: function () {
            return this._currentNode;
        },
        /**
         *  Sets the current node
         *
         *  @return DOM Node: The current node
         */
        setCurrent: function (node) {
            this._currentNode = node;

            return this;
        },
        /**
         *  Returns the parent of the current node
         *
         *  @return DOM Node: The parent node
         */
        getParent: function () {
            return this._currentNode.parentNode;
        },
        /**
         *  Creates and appends a node inside a specified parent
         *  
         *  @ref DOM Node: The insertion target for the new node
         *  @type String: A valid HTML5 element type
         *  @attributes Object: And object containing key-value pairs of attributes and values
         *
         *  @return DOM Node: The newly created node
         */
        appendNode: function (ref, type, attributes) {
            var node = document.createElement(type);
            ref.appendChild(node);
            //this._currentNode = node;

            return node;
        },
        /**
         *  Creates a node and inserts it before the specified element
         *  
         *  @ref DOM Node: A reference node for inserting the new node
         *  @type String: A valid HTML5 element type
         *  @attributes Object: And object containing key-value pairs of attributes and values
         *
         *  @return DOM Node: The newly created node
         */
        addBefore: function (ref, type, attributes) {
            var node = document.createElement(type);
            ref.parentNode.insertBefore(node, ref);
            //this._currentNode = node;

            return node;
        },
        /**
         *  Creates a node and inserts it after the specified element
         *  
         *  @parent DOM Node: A reference node for inserting the new node
         *  @type String: A valid HTML5 element type
         *  @attributes Object: And object containing key-value pairs of attributes and values
         *
         *  @return DOM Node: The newly created node
         */
        addAfter: function (ref, type, attributes) {
            var node = document.createElement(type);
            ref.parentNode.insertBefore(node, ref.nextSibling);
            //this._currentNode = node;

            return node;
        },

        /* Chainable methods
        ---------------------- */

        /**
         *  Creates and appends a node inside a specified parent
         *  
         *  @type String: A valid HTML5 element type
         *  @attributes Object: And object containing key-value pairs of attributes and values
         *  @ref DOM Node: (Optional) The insertion target for the new node
         *
         *  @return DOMBuilder: this
         */
        append: function (type, attributes, ref) {
            var parent, node;

            ref = ref || this._currentNode;
            node = document.createElement(type);
            ref.appendChild(node);
            this._currentNode = node;

            if (attributes) {
                // TODO: Use map instead
                $.each(attributes, function (key, value) {
                    node.setAttribute(key, value);
                });
            }

            return this;
        },
        /**
         *  Creates a node and inserts it after the specified element
         *  
         *  @type String: A valid HTML5 element type
         *  @attributes Object: And object containing key-value pairs of attributes and values
         *  @ref DOM Node: A reference node for inserting the new node
         *
         *  @return DOMBuilder: this
         */
        add: function (type, attributes, ref) {
            var ref, node;

            ref = ref || this._currentNode;
            node = document.createElement(type);
            //console.log(ref);
            ref.parentNode.insertBefore(node, ref.nextSibling);
            this._currentNode = node;

            if (attributes) {
                // TODO: Use map instead
                $.each(attributes, function (key, value) {
                    node.setAttribute(key, value);
                });
            }

            return this;
        },
        /**
         *  Creates a node and inserts it before the specified element
         *  
         *  @type String: A valid HTML5 element type
         *  @attributes Object: And object containing key-value pairs of attributes and values
         *  @ref DOM Node: A reference node for inserting the new node
         *
         *  @return DOMBuilder: this
         */
        before: function (type, attributes, ref) {
            var ref, node;

            ref = ref || this._currentNode;
            node = document.createElement(type);
            ref.parentNode.insertBefore(node, ref);
            this._currentNode = node;

            if (attributes) {
                // TODO: Use map instead
                $.each(attributes, function (key, value) {
                    node.setAttribute(key, value);
                });
            }

            return this;
        },
        /**
         *  Sets the internal current node reference to the parent of the current node
         *
         *  @return DOMBuilder: this
         */
        parent: function () {
            var ref, node;

            ref = ref || this._currentNode;
            this._currentNode = this._currentNode.parentNode;

            return this;
        },
        /**
         *  Sets the text for a specified node
         *
         *  @return DOMBuilder: this
         */
        text: function (value, ref) {
            var node = document.createTextNode(value);
            ref.appendChild(node);

            return this;
        }
    });

    return domBuilder.init();
}

自举:

var formLayout = [
        {
            type: 'section',
            id: 'header',
        },
        {
            type: 'div',
            id: 'center-pane',
            forms: [
                {
                    type: 'section',
                    id: 'claim-history',
                    label: 'Add Claim History',
                    templates: [
                        {
                            fieldsets: [
                                {
                                    type: 'fieldset',
                                    id: 'claim-history-details',
                                    label: 'Details',
                                    rows: [
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_LossDate',
                                                    name: 'LossDate',
                                                    label: 'Date of Loss',
                                                    type: 'datepicker', // Types: any standard HTML5 form element or Kendo UI widget
                                                    className: 'small',
                                                    data: {
                                                        role: 'datepicker', // Redundant proxy for type parameter
                                                        bind: {
                                                            value: 'datePickerValue' // Bind Kendo UI Datepicker
                                                        }
                                                    }
                                                }
                                            ]
                                        },
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_TypeOfLoss',
                                                    name: 'TypeOfLoss',
                                                    label: 'Type of Loss',
                                                    type: 'select', // This could be a combobox, really
                                                    className: ''
                                                }
                                            ]
                                        },
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_AtFaultPercentage',
                                                    name: 'AtFaultPercentage',
                                                    label: 'At Fault %',
                                                    type: 'text',
                                                    className: 'tiny'
                                                },
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_CauseOfLoss',
                                                    name: 'CauseOfLoss',
                                                    label: 'Cause of Loss',
                                                    type: 'select',
                                                    className: ''
                                                }
                                            ]
                                        },
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: '',
                                                    name: '',
                                                    label: 'Charges Laid',
                                                    type: 'yesno',
                                                    className: '',
                                                    data: {
                                                        bind: {
                                                            source: 'yesno',
                                                            value: 'datePickerValue' // Bind Kendo UI Datepicker
                                                        }
                                                    }
                                                },
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_ChargesLaid',
                                                    name: 'ChargesLaid',
                                                    label: 'Details',
                                                    type: 'textarea',
                                                    className: 'tiny'
                                                }
                                            ]
                                        }
                                    ],
                                },
                                {
                                    type: 'fieldset',
                                    id: 'claim-history-amounts',
                                    label: 'Amounts',
                                    rows: [
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_SecA',
                                                    name: 'SecA',
                                                    label: 'Sec A',
                                                    type: 'yesno',
                                                    className: 'tiny'
                                                },
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_SecATotal',
                                                    name: 'SecATotal',
                                                    label: 'Sec A Total',
                                                    type: 'text',
                                                    className: 'small'
                                                }
                                            ]
                                        },
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_SecC',
                                                    name: 'SecC',
                                                    label: 'Sec C',
                                                    type: 'text',
                                                    className: 'tiny'
                                                },
                                            ]
                                        },
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_IgnoreReason',
                                                    name: 'IgnoreReason',
                                                    label: 'Ignore Reason',
                                                    type: 'select',
                                                    className: ''
                                                }
                                            ]
                                        }
                                    ],
                                },
                                {
                                    type: 'fieldset',
                                    id: 'claim-history-vehicle-driver',
                                    label: 'Vehicle and Driver',
                                    rows: [
                                        {
                                            type: 'div',
                                            fields: [
                                                {
                                                    // Any standard html5 attributes can be defined
                                                    id: 'MainContent_DetailsContent_ClaimVehicle',
                                                    name: 'ClaimVehicle',
                                                    label: 'Claim Vehicle',
                                                    type: 'select',
                                                    className: ''
                                                }
                                            ]
                                        }
                                    ],
                                } // END fieldset
                            ] // END fieldsets  
                        } // END template
                    ] // END templates
                } // END form
            ] // END forms
        },
        {
            type: 'div',
            id: 'left-pane'
        },
        {
            type: 'div',
            id: 'right-pane'
        }
    ]; // END sections
var formBuilder = Object.create(App.Layout.Form.DOMBuilder());
var formDirector = Object.create(App.Layout.Form.DocumentDirector(formBuilder));
formDirector.build(formLayout);
document.getElementById("test").appendChild(formDirector.getDocument());

1 个答案:

答案 0 :(得分:0)

解决它 - 对于每次连续的递归,我应该追加第一个子节点(传入的对象),添加剩余的节点。添加了一个isFirst变量来跟踪。

如果有人感兴趣的话,工作小提琴...... http://jsfiddle.net/blogshop/DSxft/

build: function (obj, level) {
            var that = this,
                builder = this.getBuilder(),
                iterator = Object.create(App.Utilities.RecursiveIterator(obj)),
                level,
                key,
                current,
                attributes,
                pair,
                children,
                isFirst = true,
                hasType = false,
                maxNesting = false;

            do {
                // Get the current node and build it
                key = iterator.key();
                current = iterator.current();

                hasType = (current.hasOwnProperty('type') && current.type !== '') ? current.type : false;

                if (hasType) {
                    maxNesting = (this.isVoid(current.type)) ? true : false;

                    attributes = {};

                    $.each(current, function(key, value) {
                        if (that._validAttributes.indexOf(key) !== -1) {
                            attributes[key] = value;
                        }
                    });

                    if (isFirst == true) {
                        builder.append(current.type, attributes);
                        isFirst = false;
                    } else {
                        builder.add(current.type, attributes)
                    }

                    // Prepend label to field
                    if (current.hasOwnProperty('label')) {
                        var label = builder.addBefore(builder.getCurrent(), 'label');
                        builder.text(current.label, label);
                    }
                }

                // Are there child nodes? If so, recurse...
                if (iterator.hasChildren()) {
                    children = iterator.getChildren();

                    if (maxNesting === false) {                     
                        // Recurse
                        level = (hasType) ? level + 1 : level;
                        this.build(children, level);
                    }
                }

                // Move to the next node
                iterator.next();
            } while (iterator.hasNext());

            if (current && current.hasOwnProperty('type') && current.type !== '') {
                if (iterator.hasNext() === false) builder.parent();
            }
        },