MDL-54778 form: Fix lots of issues with form dependencies
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 7 Jun 2016 05:26:42 +0000 (13:26 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 20 Jun 2016 23:58:13 +0000 (07:58 +0800)
The original issue here was that each loop of the named values did not
check for prototypal properties. As a result, if there were input fields
with names such as 'sort', 'valueOf', 'constructor', etc. these would
return their prototypal functions instead of a falsy value, and be treated
as though they are array - hence the 'Cannot push to Function' type error.

Following on from this I discovered that the data stores were being created
as arrays, but used as objects. This can also cause issues with some form
input names -- e.g. if they are numeric.

These two issues were resolved together by correctly storing them in
objects, and checking that those objects had real properties
(hasOwnProperty). This itself has to use the prototypal function to cater
for the potential of a field name called 'hasOwnProperty'.

I also found that the instance value stores were being initialised in the
prototype (and therefore shared), which meant that there were numerous
issues if two forms were present on the same page, or one form replaced an
existing one (e.g. forms initialised in JS).

In addition, it also became apparant that several values were being used
outside of scope, or in the wrong scope. This caused further issues when
creating multiple forms on a page.

lib/form/form.js

index 6c571c6..f1d95c9 100644 (file)
 // Namespace for the form bits and bobs
 M.form = M.form || {};
 
-/**
- * Stores a list of the dependencyManager for each form on the page.
- */
-M.form.dependencyManagers = {};
-
-/**
- * Initialises a manager for a forms dependencies.
- * This should happen once per form.
- */
-M.form.initFormDependencies = function(Y, formid, dependencies) {
-
-    // If the dependencies isn't an array or object we don't want to
-    // know about it
-    if (!Y.Lang.isArray(dependencies) && !Y.Lang.isObject(dependencies)) {
-        return false;
-    }
-
-    /**
-     * Fixes an issue with YUI's processing method of form.elements property
-     * in Internet Explorer.
-     *     http://yuilibrary.com/projects/yui3/ticket/2528030
-     */
-    Y.Node.ATTRS.elements = {
-        getter: function() {
-            return Y.all(new Y.Array(this._node.elements, 0, true));
-        }
+if (typeof M.form.dependencyManager === 'undefined') {
+    var dependencyManager = function() {
+        dependencyManager.superclass.constructor.apply(this, arguments);
     };
+    Y.extend(dependencyManager, Y.Base, {
+        _locks: null,
+        _hides: null,
+        _dirty: null,
+        _nameCollections: null,
+        _fileinputs: null,
 
-    // Define the dependency manager if it hasn't already been defined.
-    M.form.dependencyManager = M.form.dependencyManager || (function(){
-        var dependencyManager = function(config) {
-            dependencyManager.superclass.constructor.apply(this, arguments);
-        };
-        dependencyManager.prototype = {
-            _form : null,
-            _locks : [],
-            _hides : [],
-            _dirty : [],
-            _nameCollections : null,
-            _fileinputs : null,
-            initializer : function(config) {
-                var i = 0, nodeName;
-                this._form = Y.one('#'+formid);
-                for (i in dependencies) {
-                    var elements = this.elementsByName(i);
-                    if (elements.size() == 0) {
-                        continue;
-                    }
-                    elements.each(function(node){
-                        nodeName = node.get('nodeName').toUpperCase();
-                        if (nodeName == 'INPUT') {
-                            if (node.getAttribute('type').match(/^(button|submit|radio|checkbox)$/)) {
-                                node.on('click', this.updateEventDependencies, this);
-                            } else {
-                                node.on('blur', this.updateEventDependencies, this);
-                            }
-                            node.on('change', this.updateEventDependencies, this);
-                        } else if (nodeName == 'SELECT') {
-                            node.on('change', this.updateEventDependencies, this);
-                        } else {
+        initializer: function() {
+            // Setup initial values for complex properties.
+            this._locks = {};
+            this._hides = {};
+            this._dirty = {};
+
+            // Setup event handlers.
+            Y.Object.each(this.get('dependencies'), function(value, i) {
+                var elements = this.elementsByName(i);
+                elements.each(function(node){
+                    var nodeName = node.get('nodeName').toUpperCase();
+                    if (nodeName == 'INPUT') {
+                        if (node.getAttribute('type').match(/^(button|submit|radio|checkbox)$/)) {
                             node.on('click', this.updateEventDependencies, this);
+                        } else {
                             node.on('blur', this.updateEventDependencies, this);
-                            node.on('change', this.updateEventDependencies, this);
                         }
-                    }, this);
-                }
-                this._form.get('elements').each(function(input){
-                    if (input.getAttribute('type')=='reset') {
-                        input.on('click', function(){
-                            this._form.reset();
-                            this.updateAllDependencies();
-                        }, this);
+                        node.on('change', this.updateEventDependencies, this);
+                    } else if (nodeName == 'SELECT') {
+                        node.on('change', this.updateEventDependencies, this);
+                    } else {
+                        node.on('click', this.updateEventDependencies, this);
+                        node.on('blur', this.updateEventDependencies, this);
+                        node.on('change', this.updateEventDependencies, this);
                     }
                 }, this);
+            }, this);
 
-                return this.updateAllDependencies();
-            },
-            /**
-             * Initializes the mapping from element name to YUI NodeList
-             */
-            initElementsByName : function() {
-                var names = [];
-                // Collect element names
-                for (var i in dependencies) {
-                    names[i] = new Y.NodeList();
-                    for (var condition in dependencies[i]) {
-                        for (var value in dependencies[i][condition]) {
-                            for (var ei in dependencies[i][condition][value]) {
-                                names[dependencies[i][condition][value][ei]] = new Y.NodeList();
-                            }
+            // Handle the reset button.
+            this.get('form').get('elements').each(function(input){
+                if (input.getAttribute('type') == 'reset') {
+                    input.on('click', function(){
+                        this.get('form').reset();
+                        this.updateAllDependencies();
+                    }, this);
+                }
+            }, this);
+
+            this.updateAllDependencies();
+        },
+
+        /**
+         * Initializes the mapping from element name to YUI NodeList
+         */
+        initElementsByName: function() {
+            var names = {};
+
+            // Collect element names.
+            Y.Object.each(this.get('dependencies'), function(conditions, i) {
+                names[i] = new Y.NodeList();
+                for (var condition in conditions) {
+                    for (var value in conditions[condition]) {
+                        for (var ei in conditions[condition][value]) {
+                            names[conditions[condition][value][ei]] = new Y.NodeList();
                         }
                     }
                 }
-                // Locate elements for each name
-                this._form.get('elements').each(function(node){
-                    var name = node.getAttribute('name');
-                    if (names[name]) {
-                        names[name].push(node);
-                    }
-                });
-                this._nameCollections = names;
-            },
-            /**
-             * Gets all elements in the form by their name and returns
-             * a YUI NodeList
-             *
-             * @param {string} name The form element name.
-             * @return {Y.NodeList}
-             */
-            elementsByName : function(name) {
-                if (!this._nameCollections) {
-                    this.initElementsByName();
-                }
-                if (!this._nameCollections[name]) {
-                    return new Y.NodeList();
-                }
-                return this._nameCollections[name];
-            },
-            /**
-             * Checks the dependencies the form has an makes any changes to the
-             * form that are required.
-             *
-             * Changes are made by functions title _dependency_{dependencytype}
-             * and more can easily be introduced by defining further functions.
-             *
-             * @param {EventFacade | null} e The event, if any.
-             * @param {string} name The form element name to check dependencies against.
-             */
-            checkDependencies : function(e, dependon) {
-                var tohide = [],
-                    tolock = [],
-                    condition, value, lock, hide,
-                    checkfunction, result, elements;
-                if (!dependencies[dependon]) {
-                    return true;
+            });
+
+            // Locate elements for each name.
+            this.get('form').get('elements').each(function(node){
+                var name = node.getAttribute('name');
+                if (({}).hasOwnProperty.call(names, name)) {
+                    names[name].push(node);
                 }
-                elements = this.elementsByName(dependon);
-                for (condition in dependencies[dependon]) {
-                    for (value in dependencies[dependon][condition]) {
-                        checkfunction = '_dependency_'+condition;
-                        if (Y.Lang.isFunction(this[checkfunction])) {
-                            result = this[checkfunction].apply(this, [elements, value, e]);
+            });
+            this._nameCollections = names;
+        },
+
+        /**
+         * Gets all elements in the form by their name and returns
+         * a YUI NodeList
+         *
+         * @param {string} name The form element name.
+         * @return {Y.NodeList}
+         */
+        elementsByName: function(name) {
+            if (!this._nameCollections) {
+                this.initElementsByName();
+            }
+            if (!({}).hasOwnProperty.call(this._nameCollections, name)) {
+                return new Y.NodeList();
+            }
+            return this._nameCollections[name];
+        },
+
+        /**
+         * Checks the dependencies the form has an makes any changes to the
+         * form that are required.
+         *
+         * Changes are made by functions title _dependency_{dependencytype}
+         * and more can easily be introduced by defining further functions.
+         *
+         * @param {EventFacade | null} e The event, if any.
+         * @param {string} name The form element name to check dependencies against.
+         */
+        checkDependencies: function(e, dependon) {
+            var dependencies = this.get('dependencies'),
+                tohide = {},
+                tolock = {},
+                condition, value, lock, hide,
+                checkfunction, result, elements;
+            if (!({}).hasOwnProperty.call(dependencies, dependon)) {
+                return true;
+            }
+            elements = this.elementsByName(dependon);
+            for (condition in dependencies[dependon]) {
+                for (value in dependencies[dependon][condition]) {
+                    checkfunction = '_dependency_'+condition;
+                    if (Y.Lang.isFunction(this[checkfunction])) {
+                        result = this[checkfunction].apply(this, [elements, value, e]);
+                    } else {
+                        result = this._dependency_default(elements, value, e);
+                    }
+                    lock = result.lock || false;
+                    hide = result.hide || false;
+                    for (var ei in dependencies[dependon][condition][value]) {
+                        var eltolock = dependencies[dependon][condition][value][ei];
+                        if (({}).hasOwnProperty.call(tohide, eltolock)) {
+                            tohide[eltolock] = tohide[eltolock] || hide;
                         } else {
-                            result = this._dependency_default(elements, value, e);
+                            tohide[eltolock] = hide;
                         }
-                        lock = result.lock || false;
-                        hide = result.hide || false;
-                        for (var ei in dependencies[dependon][condition][value]) {
-                            var eltolock = dependencies[dependon][condition][value][ei];
-                            tohide[eltolock] = tohide[eltolock] || hide;
+
+                        if (({}).hasOwnProperty.call(tolock, eltolock)) {
                             tolock[eltolock] = tolock[eltolock] || lock;
+                        } else {
+                            tolock[eltolock] = lock;
                         }
                     }
                 }
-                for (var el in tolock) {
-                    var needsupdate = false;
-                    if (tolock[el]) {
-                        this._locks[el] = this._locks[el] || [];
-                        if (!this._locks[el][dependon]) {
-                            this._locks[el][dependon] = true;
-                            needsupdate = true;
-                        }
-                    } else if (this._locks[el] && this._locks[el][dependon]) {
-                        delete this._locks[el][dependon];
+            }
+
+            for (var el in tolock) {
+                var needsupdate = false;
+                if (!({}).hasOwnProperty.call(this._locks, el)) {
+                    this._locks[el] = {};
+                }
+                if (({}).hasOwnProperty.call(tolock, el) && tolock[el]) {
+                    if (!({}).hasOwnProperty.call(this._locks[el], dependon) || this._locks[el][dependon]) {
+                        this._locks[el][dependon] = true;
                         needsupdate = true;
                     }
-                    if (tohide[el]) {
-                        this._hides[el] = this._hides[el] || [];
-                        if (!this._hides[el][dependon]) {
-                            this._hides[el][dependon] = true;
-                            needsupdate = true;
-                        }
-                    } else if (this._hides[el] && this._hides[el][dependon]) {
-                        delete this._hides[el][dependon];
+                } else if (({}).hasOwnProperty.call(this._locks[el], dependon) && this._locks[el][dependon]) {
+                    delete this._locks[el][dependon];
+                    needsupdate = true;
+                }
+
+                if (!({}).hasOwnProperty.call(this._hides, el)) {
+                    this._hides[el] = {};
+                }
+                if (({}).hasOwnProperty.call(tohide, el) && tohide[el]) {
+                    if (!({}).hasOwnProperty.call(this._hides[el], dependon) || this._hides[el][dependon]) {
+                        this._hides[el][dependon] = true;
                         needsupdate = true;
                     }
-                    if (needsupdate) {
-                        this._dirty[el] = true;
-                    }
+                } else if (({}).hasOwnProperty.call(this._hides[el], dependon) && this._hides[el][dependon]) {
+                    delete this._hides[el][dependon];
+                    needsupdate = true;
                 }
-                return true;
-            },
-            /**
-             * Update all dependencies in form
-             */
-            updateAllDependencies : function() {
-                for (var el in dependencies) {
-                    this.checkDependencies(null, el);
+
+                if (needsupdate) {
+                    this._dirty[el] = true;
                 }
-                this.updateForm();
-            },
-            /**
-             * Update dependencies associated with event
-             *
-             * @param {Event} e The event.
-             */
-            updateEventDependencies : function(e) {
-                var el = e.target.getAttribute('name');
-                this.checkDependencies(e, el);
-                this.updateForm();
-            },
-            /**
-             * Flush pending changes to the form
-             */
-            updateForm : function() {
-                for (var el in this._dirty) {
-                    if (this._locks[el]) {
-                        var locked = !this._isObjectEmpty(this._locks[el]);
-                        this._disableElement(el, locked);
-                    }
-                    if (this._hides[el]) {
-                        var hidden = !this._isObjectEmpty(this._hides[el]);
-                        this._hideElement(el, hidden);
-                    }
+            }
+
+            return true;
+        },
+        /**
+         * Update all dependencies in form
+         */
+        updateAllDependencies: function() {
+            Y.Object.each(this.get('dependencies'), function(value, name) {
+                this.checkDependencies(null, name);
+            }, this);
+
+            this.updateForm();
+        },
+        /**
+         * Update dependencies associated with event
+         *
+         * @param {Event} e The event.
+         */
+        updateEventDependencies: function(e) {
+            var el = e.target.getAttribute('name');
+            this.checkDependencies(e, el);
+            this.updateForm();
+        },
+        /**
+         * Flush pending changes to the form
+         */
+        updateForm: function() {
+            var el;
+            for (el in this._dirty) {
+                if (({}).hasOwnProperty.call(this._locks, el)) {
+                    this._disableElement(el, !Y.Object.isEmpty(this._locks[el]));
                 }
-                this._dirty = [];
-            },
-            /**
-             * Disables or enables all form elements with the given name
-             *
-             * @param {string} name The form element name.
-             * @param {boolean} disabled True to disable, false to enable.
-             */
-            _disableElement : function(name, disabled) {
-                var els = this.elementsByName(name);
-                var filepicker = this.isFilePicker(name);
-                els.each(function(node){
-                    if (disabled) {
-                        node.setAttribute('disabled', 'disabled');
-                    } else {
-                        node.removeAttribute('disabled');
-                    }
+                if (({}).hasOwnProperty.call(this._hides, el)) {
+                    this._hideElement(el, !Y.Object.isEmpty(this._hides[el]));
+                }
+            }
 
-                    // Extra code to disable filepicker or filemanager form elements
-                    if (filepicker) {
-                        var fitem = node.ancestor('.fitem');
-                        if (fitem) {
-                            if (disabled){
-                                fitem.addClass('disabled');
-                            } else {
-                                fitem.removeClass('disabled');
-                            }
+            this._dirty = {};
+        },
+        /**
+         * Disables or enables all form elements with the given name
+         *
+         * @param {string} name The form element name.
+         * @param {boolean} disabled True to disable, false to enable.
+         */
+        _disableElement: function(name, disabled) {
+            var els = this.elementsByName(name);
+            var filepicker = this.isFilePicker(name);
+            els.each(function(node){
+                if (disabled) {
+                    node.setAttribute('disabled', 'disabled');
+                } else {
+                    node.removeAttribute('disabled');
+                }
+
+                // Extra code to disable filepicker or filemanager form elements
+                if (filepicker) {
+                    var fitem = node.ancestor('.fitem');
+                    if (fitem) {
+                        if (disabled){
+                            fitem.addClass('disabled');
+                        } else {
+                            fitem.removeClass('disabled');
                         }
                     }
-                })
-            },
-            /**
-             * Hides or shows all form elements with the given name.
-             *
-             * @param {string} name The form element name.
-             * @param {boolean} disabled True to hide, false to show.
-             */
-            _hideElement : function(name, hidden) {
-                var els = this.elementsByName(name);
-                els.each(function(node){
-                    var e = node.ancestor('.fitem');
-                    if (e) {
-                        e.setStyles({
-                            display : (hidden)?'none':''
-                        })
-                    }
-                });
-            },
-            /**
-             * Is the form element inside a filepicker or filemanager?
-             *
-             * @param {string} el The form element name.
-             * @return {boolean}
-             */
-            isFilePicker : function(el) {
-                if (!this._fileinputs) {
-                    var fileinputs = [];
-                    var els = this._form.all('.fitem.fitem_ffilepicker input,.fitem.fitem_ffilemanager input');
-                    els.each(function(node){
-                        fileinputs[node.getAttribute('name')] = true;
+                }
+            });
+        },
+        /**
+         * Hides or shows all form elements with the given name.
+         *
+         * @param {string} name The form element name.
+         * @param {boolean} disabled True to hide, false to show.
+         */
+        _hideElement: function(name, hidden) {
+            var els = this.elementsByName(name);
+            els.each(function(node){
+                var e = node.ancestor('.fitem');
+                if (e) {
+                    e.setStyles({
+                        display: (hidden)?'none':''
                     });
-                    this._fileinputs = fileinputs;
                 }
+            });
+        },
+        /**
+         * Is the form element inside a filepicker or filemanager?
+         *
+         * @param {string} el The form element name.
+         * @return {boolean}
+         */
+        isFilePicker: function(el) {
+            if (!this._fileinputs) {
+                var fileinputs = {};
+                var els = this.get('form').all('.fitem.fitem_ffilepicker input,.fitem.fitem_ffilemanager input');
+                els.each(function(node){
+                    fileinputs[node.getAttribute('name')] = true;
+                });
+                this._fileinputs = fileinputs;
+            }
+
+            if (({}).hasOwnProperty.call(this._fileinputs, el)) {
                 return this._fileinputs[el] || false;
-            },
-            /**
-             * Check if the object is empty
-             *
-             * @param {object} obj
-             * @return {boolean}
-             */
-            _isObjectEmpty : function(obj) {
-                for(var prop in obj) {
-                    if(obj.hasOwnProperty(prop))
-                        return false;
+            }
+
+            return false;
+        },
+        _dependency_notchecked: function(elements, value) {
+            var lock = false;
+            elements.each(function(){
+                if (this.getAttribute('type').toLowerCase()=='hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
+                    // This is the hidden input that is part of an advcheckbox.
+                    return;
                 }
-                return true;
-            },
-            _dependency_notchecked : function(elements, value) {
-                var lock = false;
-                elements.each(function(){
-                    if (this.getAttribute('type').toLowerCase()=='hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
-                        // This is the hidden input that is part of an advcheckbox.
-                        return;
-                    }
-                    if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
-                        return;
-                    }
-                    lock = lock || !Y.Node.getDOMNode(this).checked;
-                });
-                return {
-                    lock : lock,
-                    hide : false
+                if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
+                    return;
                 }
-            },
-            _dependency_checked : function(elements, value) {
-                var lock = false;
-                elements.each(function(){
-                    if (this.getAttribute('type').toLowerCase()=='hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
-                        // This is the hidden input that is part of an advcheckbox.
-                        return;
-                    }
-                    if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
-                        return;
-                    }
-                    lock = lock || Y.Node.getDOMNode(this).checked;
-                });
-                return {
-                    lock : lock,
-                    hide : false
+                lock = lock || !Y.Node.getDOMNode(this).checked;
+            });
+            return {
+                lock: lock,
+                hide: false
+            };
+        },
+        _dependency_checked: function(elements, value) {
+            var lock = false;
+            elements.each(function(){
+                if (this.getAttribute('type').toLowerCase()=='hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
+                    // This is the hidden input that is part of an advcheckbox.
+                    return;
                 }
-            },
-            _dependency_noitemselected : function(elements, value) {
-                var lock = false;
-                elements.each(function(){
-                    lock = lock || this.get('selectedIndex') == -1;
-                });
-                return {
-                    lock : lock,
-                    hide : false
+                if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
+                    return;
                 }
-            },
-            _dependency_eq : function(elements, value) {
-                var lock = false;
-                var hidden_val = false;
-                var options, v, selected, values;
-                elements.each(function(){
-                    if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
-                        return;
-                    } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
-                        // This is the hidden input that is part of an advcheckbox.
-                        hidden_val = (this.get('value') == value);
-                        return;
-                    } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
-                        lock = lock || hidden_val;
-                        return;
+                lock = lock || Y.Node.getDOMNode(this).checked;
+            });
+            return {
+                lock: lock,
+                hide: false
+            };
+        },
+        _dependency_noitemselected: function(elements, value) {
+            var lock = false;
+            elements.each(function(){
+                lock = lock || this.get('selectedIndex') == -1;
+            });
+            return {
+                lock: lock,
+                hide: false
+            };
+        },
+        _dependency_eq: function(elements, value) {
+            var lock = false;
+            var hidden_val = false;
+            var options, v, selected, values;
+            elements.each(function(){
+                if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
+                    return;
+                } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
+                    // This is the hidden input that is part of an advcheckbox.
+                    hidden_val = (this.get('value') == value);
+                    return;
+                } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
+                    lock = lock || hidden_val;
+                    return;
+                }
+                if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
+                    // Check for filepicker status.
+                    var elementname = this.getAttribute('name');
+                    if (elementname && M.form_filepicker.instances[elementname].fileadded) {
+                        lock = false;
+                    } else {
+                        lock = true;
                     }
-                    if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
-                        // Check for filepicker status.
-                        var elementname = this.getAttribute('name');
-                        if (elementname && M.form_filepicker.instances[elementname].fileadded) {
-                            lock = false;
-                        } else {
-                            lock = true;
+                } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
+                    // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
+                    // when multiple values have to be selected at the same time.
+                    values = value.split('|');
+                    selected = [];
+                    options = this.get('options');
+                    options.each(function() {
+                        if (this.get('selected')) {
+                            selected[selected.length] = this.get('value');
                         }
-                    } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
-                        // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
-                        // when multiple values have to be selected at the same time.
-                        values = value.split('|');
-                        selected = [];
-                        options = this.get('options');
-                        options.each(function() {
-                            if (this.get('selected')) {
-                                selected[selected.length] = this.get('value');
-                            }
-                        });
-                        if (selected.length > 0 && selected.length === values.length) {
-                            for (var i in selected) {
-                                v = selected[i];
-                                if (values.indexOf(v) > -1) {
-                                    lock = true;
-                                } else {
-                                    lock = false;
-                                    return;
-                                }
+                    });
+                    if (selected.length > 0 && selected.length === values.length) {
+                        for (var i in selected) {
+                            v = selected[i];
+                            if (values.indexOf(v) > -1) {
+                                lock = true;
+                            } else {
+                                lock = false;
+                                return;
                             }
-                        } else {
-                            lock = false;
                         }
                     } else {
-                        lock = lock || this.get('value') == value;
+                        lock = false;
                     }
-                });
-                return {
-                    lock : lock,
-                    hide : false
+                } else {
+                    lock = lock || this.get('value') == value;
                 }
-            },
-            /**
-             * Lock the given field if the field value is in the given set of values.
-             *
-             * @param elements
-             * @param values
-             * @returns {{lock: boolean, hide: boolean}}
-             * @private
-             */
-            _dependency_in : function(elements, values) {
-                // A pipe (|) is used as a value separator
-                // when multiple values have to be passed on at the same time.
-                values = values.split('|');
-                var lock = false;
-                var hidden_val = false;
-                var options, v, selected, value;
-                elements.each(function(){
-                    if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
-                        return;
-                    } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
-                        // This is the hidden input that is part of an advcheckbox.
-                        hidden_val = (values.indexOf(this.get('value')) > -1);
-                        return;
-                    } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
-                        lock = lock || hidden_val;
-                        return;
+            });
+            return {
+                lock: lock,
+                hide: false
+            };
+        },
+        /**
+         * Lock the given field if the field value is in the given set of values.
+         *
+         * @param elements
+         * @param values
+         * @returns {{lock: boolean, hide: boolean}}
+         * @private
+         */
+        _dependency_in: function(elements, values) {
+            // A pipe (|) is used as a value separator
+            // when multiple values have to be passed on at the same time.
+            values = values.split('|');
+            var lock = false;
+            var hidden_val = false;
+            var options, v, selected, value;
+            elements.each(function(){
+                if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
+                    return;
+                } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
+                    // This is the hidden input that is part of an advcheckbox.
+                    hidden_val = (values.indexOf(this.get('value')) > -1);
+                    return;
+                } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
+                    lock = lock || hidden_val;
+                    return;
+                }
+                if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
+                    // Check for filepicker status.
+                    var elementname = this.getAttribute('name');
+                    if (elementname && M.form_filepicker.instances[elementname].fileadded) {
+                        lock = false;
+                    } else {
+                        lock = true;
                     }
-                    if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
-                        // Check for filepicker status.
-                        var elementname = this.getAttribute('name');
-                        if (elementname && M.form_filepicker.instances[elementname].fileadded) {
-                            lock = false;
-                        } else {
-                            lock = true;
+                } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
+                    // Multiple selects can have one or more value assigned.
+                    selected = [];
+                    options = this.get('options');
+                    options.each(function() {
+                        if (this.get('selected')) {
+                            selected[selected.length] = this.get('value');
                         }
-                    } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
-                        // Multiple selects can have one or more value assigned.
-                        selected = [];
-                        options = this.get('options');
-                        options.each(function() {
-                            if (this.get('selected')) {
-                                selected[selected.length] = this.get('value');
-                            }
-                        });
-                        if (selected.length > 0 && selected.length === values.length) {
-                            for (var i in selected) {
-                                v = selected[i];
-                                if (values.indexOf(v) > -1) {
-                                    lock = true;
-                                } else {
-                                    lock = false;
-                                    return;
-                                }
+                    });
+                    if (selected.length > 0 && selected.length === values.length) {
+                        for (var i in selected) {
+                            v = selected[i];
+                            if (values.indexOf(v) > -1) {
+                                lock = true;
+                            } else {
+                                lock = false;
+                                return;
                             }
-                        } else {
-                            lock = false;
                         }
                     } else {
-                        value = this.get('value');
-                        lock = lock || (values.indexOf(value) > -1);
+                        lock = false;
                     }
-                });
-                return {
-                    lock : lock,
-                    hide : false
+                } else {
+                    value = this.get('value');
+                    lock = lock || (values.indexOf(value) > -1);
                 }
-            },
-            _dependency_hide : function(elements, value) {
-                return {
-                    lock : false,
-                    hide : true
+            });
+            return {
+                lock: lock,
+                hide: false
+            };
+        },
+        _dependency_hide: function(elements, value) {
+            return {
+                lock: false,
+                hide: true
+            };
+        },
+        _dependency_default: function(elements, value, ev) {
+            var lock = false,
+                hidden_val = false,
+                values
+                ;
+            elements.each(function(){
+                var selected;
+                if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
+                    return;
+                } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
+                    // This is the hidden input that is part of an advcheckbox.
+                    hidden_val = (this.get('value') != value);
+                    return;
+                } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
+                    lock = lock || hidden_val;
+                    return;
                 }
-            },
-            _dependency_default : function(elements, value, ev) {
-                var lock = false;
-                var hidden_val = false;
-                elements.each(function(){
-                    if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
-                        return;
-                    } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
-                        // This is the hidden input that is part of an advcheckbox.
-                        hidden_val = (this.get('value') != value);
-                        return;
-                    } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
-                        lock = lock || hidden_val;
-                        return;
+                //check for filepicker status
+                if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
+                    var elementname = this.getAttribute('name');
+                    if (elementname && M.form_filepicker.instances[elementname].fileadded) {
+                        lock = true;
+                    } else {
+                        lock = false;
                     }
-                    //check for filepicker status
-                    if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
-                        var elementname = this.getAttribute('name');
-                        if (elementname && M.form_filepicker.instances[elementname].fileadded) {
-                            lock = true;
-                        } else {
-                            lock = false;
+                } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
+                    // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
+                    // when multiple values have to be selected at the same time.
+                    values = value.split('|');
+                    this.get('options').each(function() {
+                        if (this.get('selected')) {
+                            selected[selected.length] = this.get('value');
                         }
-                    } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
-                        // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
-                        // when multiple values have to be selected at the same time.
-                        values = value.split('|');
-                        selected = [];
-                        options = this.get('options');
-                        options.each(function() {
-                            if (this.get('selected')) {
-                                selected[selected.length] = this.get('value');
-                            }
-                        });
-                        if (selected.length > 0 && selected.length === values.length) {
-                            for (var i in selected) {
-                                v = selected[i];
-                                if (values.indexOf(v) > -1) {
-                                    lock = false;
-                                } else {
-                                    lock = true;
-                                    return;
-                                }
+                    });
+                    if (selected.length > 0 && selected.length === values.length) {
+                        for (var i in selected) {
+                            if (values.indexOf(selected[i]) > -1) {
+                                lock = false;
+                            } else {
+                                lock = true;
+                                return;
                             }
-                        } else {
-                            lock = true;
                         }
                     } else {
-                        lock = lock || this.get('value') != value;
+                        lock = true;
                     }
-                });
-                return {
-                    lock : lock,
-                    hide : false
+                } else {
+                    lock = lock || this.get('value') != value;
                 }
+            });
+            return {
+                lock: lock,
+                hide: false
+            };
+        }
+    }, {
+        NAME: 'mform-dependency-manager',
+        ATTRS: {
+            form: {
+                setter: function(value) {
+                    return Y.one('#' + value);
+                },
+                value: null
+            },
+
+            dependencies: {
+                value: {}
             }
-        };
-        Y.extend(dependencyManager, Y.Base, dependencyManager.prototype, {
-            NAME : 'mform-dependency-manager'
-        });
+        }
+    });
+
+    M.form.dependencyManager = dependencyManager;
+}
 
-        return dependencyManager;
-    })();
+/**
+ * Stores a list of the dependencyManager for each form on the page.
+ */
+M.form.dependencyManagers = {};
+
+/**
+ * Initialises a manager for a forms dependencies.
+ * This should happen once per form.
+ */
+M.form.initFormDependencies = function(Y, formid, dependencies) {
+
+    // If the dependencies isn't an array or object we don't want to
+    // know about it
+    if (!Y.Lang.isArray(dependencies) && !Y.Lang.isObject(dependencies)) {
+        return false;
+    }
+
+    /**
+     * Fixes an issue with YUI's processing method of form.elements property
+     * in Internet Explorer.
+     *     http://yuilibrary.com/projects/yui3/ticket/2528030
+     */
+    Y.Node.ATTRS.elements = {
+        getter: function() {
+            return Y.all(new Y.Array(this._node.elements, 0, true));
+        }
+    };
 
-    M.form.dependencyManagers[formid] = new M.form.dependencyManager();
+    M.form.dependencyManagers[formid] = new M.form.dependencyManager({form: formid, dependencies: dependencies});
     return M.form.dependencyManagers[formid];
 };