3
\$\begingroup\$

I'd like some comments and any suggestions that you have for where to go next with this.

I've been working on developing a JavaScript library that allows for dynamic forms by simply adding some extra HTML tags and attributes to your form. It's brand new, so it doesn't have a lot of features, but it's based on the idea that you shouldn't need to write a bunch of JavaScript and have to deal with bugs and stuff just to get your form to have multiple pages or to add new fields when you click a button... stuff like that.

All you have to do to make it work is include jQuery and this JavaScript in the page after all the form HTML.

...
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js'></script>
<script type='text/javascript' src='edomform.js'></script>
</body>

I've got the current JavaScript in a Fiddle with some example HTML and CSS, and some basic documentation/guide material in a Google doc that you can comment on if you like. Any comments and suggestions are welcome.

You're also welcome to use this for whatever you want as-is or with any modifications you or anyone comes up with.

Fiddle

eDomForm

//Configuration variables
var config = jQuery("edfconfig");

function getConfigBoolean(attr) {
    if (config != null)
    return jQuery(config).attr(attr) != undefined;
}
function getConfigValue(attr) {
    if (config != null)
    return jQuery(config).attr(attr);
}

var noasterisks         = getConfigBoolean("noasterisks");
var addafter            = getConfigBoolean("addafter");
var doactions           = getConfigBoolean("doactions");
var noappend            = getConfigBoolean("noappend");
var requiredmessage     = getConfigValue("requiredmessage");
var requiredmessageid   = getConfigValue("requiredmessageid");


//eDomForm variables
var attrs = jQuery("edfvars")[0];
var variables = {};
if (attrs != null)
for (i=0;i<attrs.attributes.length;i++) {
    variables[attrs.attributes[i].nodeName] = attrs.attributes[i].nodeValue;
}

//Function because jQuery doesn't have a selector for the name attribute
function getByName(n) {
    return document.getElementsByName(n);
}

//required fields have the class required_field
var required = jQuery(".required_field");
//message div to display global form messages
var message = jQuery("#message");
//reference to the entire form itself
var form = jQuery("#form");

//form data
jQuery(form).data("required_empty",required.length);

//Add next and back buttons
jQuery(".form_page").each(function(i){
    jQuery(this).prepend('<button type="button" class="back">Back</button> <button type="button" class="next">Next</button>');
});

//Next and Back buttons
var pages = jQuery(".form_page");
var backButtons = jQuery(".back");
var nextButtons = jQuery(".next");
for (i=0;i<pages.length;i++) {
    if (i != pages.length-1) {
        nextButtons[i].onclick = function() {
            jQuery(this).closest("div").fadeOut();
            jQuery(this).closest("div").nextAll(":not(.disabled):first").fadeIn();
        };
    } else {
        jQuery(nextButtons[i]).remove();
    }
    if (i != 0) {
        backButtons[i].onclick = function(i) {
            jQuery(this).closest("div").fadeOut();
            jQuery(this).closest("div").prevAll(":not(.disabled):first").fadeIn();
        };
    } else {
        jQuery(backButtons[i]).remove();
    }
}

//Aliases
function getByAlias(a) {
    return jQuery("[alias="+a+"]");
}
function hasClass(element, cls) {
    return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;
}

var aliases = jQuery(".alias");

for (i=0;i<aliases.length;i++) {
    var alias = jQuery(aliases[i]).attr("alias");
    var original = jQuery("."+alias);
    for (j=0;j<original.length;j++) {
        if (hasClass(original[j],"required_field")) {
            jQuery(aliases[i]).addClass("required_field");
        }
        original[j].onchange = aliasChange.bind(null,original[j],aliases[i]);
        aliases[i].onchange = aliasChange.bind(null,aliases[i],original[j]);
    }
}
function aliasChange(o,a) {
    a.value = o.value;
    a.onblur();
}
var radioGroupCounter = 1;

jQuery(document).ready(function() {
    //Prevent form submission if required fields have not been filled in
    if (form[0] != null)
    form[0].onsubmit = function() {
        var required_fields = document.getElementsByClassName("required_field");
        for (i=0;i<required_fields.length;i++) {
            if (required_fields[i].value == "") {
                jQuery("#"+requiredmessageid).html(requiredmessage);
                return false;
            }
        }
        jQuery("#"+requiredmessageid).html("");
        return true;
    };

    //Hidden pages
    function handleHiddenPages() {
        jQuery(".revealer").each(function(i){
            var page = jQuery(this).attr("page");
            jQuery(this).click(function(){
                if (jQuery(this).is(":checked")) {
                    jQuery("."+page).removeClass("disabled");
                } else {
                    jQuery("."+page).addClass("disabled");
                }
            });
        });
    }
    handleHiddenPages();

    //Switchers
    function handleSwitchers() {
        jQuery(".switcher").each(function(x){
            var connections = jQuery(this).attr("connections");
            connections = jQuery.parseJSON(connections);
            var connectedSections = {};
            for (var key in connections) {
                //if something like a-b
                if (connections[key].indexOf("-") > -1) {
                    var nums = connections[key].split("-");
                    var resultNums = [];
                    for (i=0;i<nums.length;i++) {
                        nums[i] = parseInt(nums[i]);
                    }
                    for (i=nums[0];i<=nums[nums.length-1];i++) {
                        resultNums.push(i+"");
                        connectedSections[i] = key;
                    }
                } else {
                    if (connections.hasOwnProperty(key))
                    connectedSections[connections[key]] = key;
                }
            }
            jQuery(this).change(function(){
                for (var key in connectedSections) {
                    jQuery("."+connectedSections[key]).hide();
                }
                jQuery("."+connectedSections[jQuery(this).val()]).show();
            });
        });
    }
    handleSwitchers();

    //Displayers/Hiders
    function handleDisplayers() {
        jQuery(".displayer").each(function(x){
            var connected = jQuery(this).attr("display");
            var special = "";
            var connecteds = [];
            if (connected.indexOf(" ") > -1) {
                connecteds = connected.split(" ");
                var special = connecteds[0];
                connected = connecteds[1];
            }
            var name = jQuery(this).attr("name");
            var group = getByName(name);
            jQuery(group).each(function() {
                jQuery(this).on("click",function() {
                var button = this;
                    if (jQuery(this).attr("display") != null) {
                        if (special == "") {
                            jQuery("."+connected).each(function() {
                                jQuery(this).show();
                            });
                        }
                        else if (special == "next") {
                            jQuery("."+connected).each(function() {
                                if (button.compareDocumentPosition(this) == 4) {
                                    jQuery(this).show();
                                    return false;
                                }
                            });
                        }
                        else if (special == "prev") {
                            jQuery(jQuery("."+connected).get().reverse()).each(function(i) {
                            if (button.compareDocumentPosition(this) == 2) {
                                jQuery(this).show();
                                return false;
                            }
                        });
                        }
                    }else {
                        if (special == "") {
                            jQuery("."+connected).each(function() {
                                jQuery(this).hide();
                            });
                        }
                        else if (special == "next")
                        jQuery("."+connected).each(function() {
                            if (button.compareDocumentPosition(this) == 4) {
                                jQuery(this).hide();
                                return false;
                            }
                        });
                        else if (special == "prev")
                        jQuery(jQuery("."+connected).get().reverse()).each(function(i) {
                            if (button.compareDocumentPosition(this) == 2) {
                                jQuery(this).hide();
                                return false;
                            }
                        });
                    }
                });
            });
        });
    }
    handleDisplayers();

    //findNext function from stackoverflow
    /**
     * Find the next element matching a certain selector. Differs from next() in
     *  that it searches outside the current element's parent.
     *  
     * @param selector The selector to search for
     * @param steps (optional) The number of steps to search, the default is 1
     * @param scope (optional) The scope to search in, the default is document wide 
     */
    $.fn.findNext = function(selector, steps, scope)
    {
        // Steps given? Then parse to int 
        if (steps)
        {
            steps = Math.floor(steps);
        }
        else if (steps === 0)
        {
            // Stupid case :)
            return this;
        }
        else
        {
            // Else, try the easy way
            var next = this.next(selector);
            if (next.length)
                return next;
            // Easy way failed, try the hard way :)
            steps = 1;
        }

        // Set scope to document or user-defined
        scope = (scope) ? $(scope) : $(document);

        // Find kids that match selector: used as exclusion filter
        var kids = this.find(selector);

        // Find in parent(s)
        hay = $(this);
        while(hay[0] != scope[0])
        {
            // Move up one level
            hay = hay.parent();     
            // Select all kids of parent
            //  - excluding kids of current element (next != inside),
            //  - add current element (will be added in document order)
            var rs = hay.find(selector).not(kids).add($(this));
            // Move the desired number of steps
            var id = rs.index(this) + steps;
            // Result found? then return
            if (id > -1 && id < rs.length)
                return $(rs[id]);
        }
        // Return empty result
        return $([]);
    }

    //Adding New Sections
    function handleAdds() {
        jQuery(".add").each(function(x){
            var add = jQuery(this).attr("add");
            if (add.indexOf(" ") > -1) {
                add = add.split(" ");
            }
            var to = jQuery(this).attr("to");
            var radiogroup = jQuery(this).attr("radiogroup");
            if (radiogroup != null)
            radiogroup = radiogroup.split(" ");
            var cpy = jQuery("<div />").append(jQuery("."+add).clone()).html();
            if (to == null) {
                jQuery(this).click(function() {
                    var text = cpy;
                    var counter = radioGroupCounter++;
                    if (radiogroup != null)
                    for (i=0;i<radiogroup.length;i++) {
                        var re = new RegExp(radiogroup[i]+"\\[\\d\\]","g");
                        text = text.replace(re,radiogroup[i]+"["+(counter)+"]");
                    }
                    if (addafter)
                    jQuery(this).after(text);
                    else
                    jQuery(this).before(text);

                    handleHiddenPages();
                    handleDisplayers();
                    handleSwitchers();
                });
            } else {
                if (to.indexOf(" ") > -1) {
                    to = to.split(" ");
                }
                jQuery(this).click(function() {
                    var text = cpy;
                    var counter = radioGroupCounter++;
                    if (radiogroup != null)
                    for (i=0;i<radiogroup.length;i++) {
                        var re = new RegExp(radiogroup[i]+"\\[\\d\\]","g");
                        text = text.replace(re,radiogroup[i]+"["+(counter)+"]");
                        console.log(text);
                    }
                    jQuery("#"+to).append(text);

                    handleHiddenPages();
                    handleDisplayers();
                    handleSwitchers();
                });
            }
        });
    }
    handleAdds();

    //Action tags


    function handleAll() {
        handleHiddenPages();
        handleDisplayers();
        handleSwitchers();
        handleAdds();
    }
});

required = jQuery(".required_field");

//Loop through required fields, adding the onblur event
//so that whenever the user deselects a required field,
//if it is blank the asterisk will turn red.
for (i=0;i<required.length;i++) {
    jQuery(required[i]).after("<span>*</span>");
    jQuery(required[i]).data("empty",true);
    required[i].onblur = function() {
        if (this.value == "") {
            jQuery(this).next().css("color","#f00");
        } else {
            jQuery(this).next().css("color","#000");
        }
    };
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

On the whole

Interesting, from a philosophy perspective, you are now moving from code with potential JavaScript bugs to code with potential HTML 'configuration' bugs. Debugging configuration bugs is annoying since there is no dedicated tooling for it.

Furthermore, you have some interesting design choices HTML wise;

  • You have custom tags but you are not using registerElement
  • You have custom properties that are not compatible with $().data()
  • This works, but it should be avoided when possible, and I believe it is possible

From a consistency perspective;

  • You have a tag property requiredmessage
  • You have classes required_empty and required_field
  • Your code is lowerCamelCase

I would synchronize everything to lowerCamelCase across properties, CSS classes and code, otherwise users will have to keep referring to documentation to see what the proper spelling is.

Coding*

Constants, most of these should have a well named constant name;

  • You use ".required_field" and "required_field" several times
  • You use ".alias" and "alias" several times
  • etc. etc.

Comments:

  • handleSwitchers <- really unclear what this is supposed to do
  • Other than that, the commenting throughout the code is very uneven

Some jQuery specific items:

  • This:

    jQuery(".form_page").each(function(i){
      jQuery(this).prepend('<button type="button" class="back">Back</button> <button type="button" class="next">Next</button>');
    });
    

    can be

    var buttons = '<button type="button" class="back">Back</button> <button type="button" class="next">Next</button>';
    jQuery(".form_page").prepend( buttons );
    

    basically prepend works on every selected element.

  • This:

    for (i=0;i<pages.length;i++) {
        if (i != pages.length-1) {
            nextButtons[i].onclick = function() {
                jQuery(this).closest("div").fadeOut();
                jQuery(this).closest("div").nextAll(":not(.disabled):first").fadeIn();
            };
        } else {
            jQuery(nextButtons[i]).remove();
        }
        if (i != 0) {
            backButtons[i].onclick = function(i) {
                jQuery(this).closest("div").fadeOut();
                jQuery(this).closest("div").prevAll(":not(.disabled):first").fadeIn();
            };
        } else {
            jQuery(backButtons[i]).remove();
        }
    }
    

    is basically trying to make every back and next button work, and remove the first back button and the last next button, the following code does the exact thing with fewer lines and more clarity of purpose:

    nextButtons.last().remove();  
    backButtons.first().remove();  
    
    nextButtons.click( function(){
      $(this).closest("div").fadeOut()
                          .nextAll(":not(.disabled):first").fadeIn();         
    });
    
    backButtons.click( function(){
      $(this).closest("div").fadeOut()
                            .prevAll(":not(.disabled):first").fadeIn();     
    })  
    

    Note also that I chained the two statement within the click handlers for efficiency.

  • You use jQuery everywhere, but not here: var required_fields = document.getElementsByClassName("required_field"); why ?
  • Cache jQuery(this) into $this = jQuery(this) in all your listeners where you access jQuery(this) more than once.
  • When you decide to add a class or remove a class based on a Boolean, consider toggleClass, this:

    jQuery(this).click(function(){
      if (jQuery(this).is(":checked")) {
        jQuery("."+page).removeClass("disabled");
      } else {
        jQuery("."+page).addClass("disabled");
      }
    });
    

    could be

    jQuery(this).click(function(){
      var $this = jQuery(this);
      jQuery("."+page).toggleClass( 'disabled' , !$this.is(":checked") );
    });
    
  • Not really jQuery related, but realize that ternaries can be your friend:

    required[i].onblur = function() {
    if (this.value == "") {
            jQuery(this).next().css("color","#f00");
        } else {
            jQuery(this).next().css("color","#000");
        }
    };
    

    can be ( you can put the assignment outside of the loop )

    required.blur( function() {
        jQuery(this).next().css("color", (this.value == "") ? "#f00" : "#000");    
    });
    

    When you think about it, "#f00" should be a constant, and probably you should assign a class instead of putting the color red.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.