4
\$\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");
 }
 };
}
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jun 27, 2013 at 17:38
\$\endgroup\$

1 Answer 1

3
\$\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.

answered Apr 23, 2014 at 19:58
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.