1
\$\begingroup\$

I want to add red borders to required fields, if the fields are empty. The ShowSelection() function is run when the interface is loaded, and everytime the interface is refreshed. I have the following form fields: First Name, Last Name, Birth Date, Email, Cell Phone, Address, Post Num, Post City, and Consent Checkbox. If Consent Checkbox is checked, the other fields are required. These required fields should then have a red border. There are some exceptions; when the Email field is filled out, the Address (inc. Post Num and City) are not required anymore. And vice versa, when Address is filled out, the Email field is not required.

The code is written for Siebel OpenUI, which has some proprietary functionality such as "GetPM()", "GetInputName()" etc. The inputRedborder class contains red 2px border.

I suspect there is a better way to write the code, and I am therefore happy for all help I can get to improve the code.

Edit: Based on reading about the bind functionality, I have also understood that I should bind functions only once. So ideally I need to change the code to only bind the functions to the inputs on load.

Edit 2: Made a working JSfiddle to demonstrate what I want to accomplish: https://jsfiddle.net/gnosoxz1/

function redBorder(controlName) {
    var input = $('[name=' + controls[controlName].GetInputName() + ']'); // get input element
    // custom logic for field "EmailAddress"
    if (controlName === "EmailAddress") {
        var pAddress = $('[name=' + controls["Personal Address"].GetInputName() + ']'); // get Personal Address input element
        var postalCode = $('[name=' + controls["Personal Postal Code"].GetInputName() + ']'); // get Personal Postal Code input element
        var postalCity = $('[name=' + controls["Personal City"].GetInputName() + ']'); // get Personal City input element
        if (!pAddress.val() && !postalCode.val()) {
            if (!input.val()) {
                input.addClass("inputRedborder"); // add red border if required input element is empty
            } else if (input.val()) {
                input.removeClass("inputRedborder"); // remove red border if required input element is not empty
            }
        }
        // remove previously binded events
        input.unbind();
        // then add event listeners to input element to handle input changes
        input.bind('change keyup', function () {
            if (!pAddress.val() && !postalCode.val()) {
                var input = $(this);
                if (!input.val()) { // add red border if required input element is empty
                    input.addClass("inputRedborder");
                    pAddress.addClass("inputRedborder");
                    postalCode.addClass("inputRedborder");
                    postalCity.addClass("inputRedborder");
                } else if (input.val()) { // remove red border if required input element is empty
                    input.removeClass("inputRedborder");
                    pAddress.removeClass("inputRedborder");
                    postalCode.removeClass("inputRedborder");
                    postalCity.removeClass("inputRedborder");
                }
            }
        });
    }
    // custom logic for address fields
    else if (controlName === "Personal Address" || controlName === "Personal Postal Code" || controlName === "Personal City") {
        var eMail = $('[name=' + controls["EmailAddress"].GetInputName() + ']'); // get EmailAddress input element
        var pAddress = $('[name=' + controls["Personal Address"].GetInputName() + ']'); // get Personal Address input element
        var postalCode = $('[name=' + controls["Personal Postal Code"].GetInputName() + ']'); // get Personal Postal Code input element
        var postalCity = $('[name=' + controls["Personal City"].GetInputName() + ']'); // get Personal City input element
        if (!eMail.val()) {
            if (!input.val()) { // add red border if required input element is empty
                input.addClass("inputRedborder");
            } else if (input.val()) { // remove red border if required input element is empty
                input.removeClass("inputRedborder");
            }
        }
        // remove previously binded events
        input.unbind();
        // then add event listeners to input element to handle input changes
        input.bind('change keyup', function () {
            if (!eMail.val()) {
                var input = $(this);
                if (!input.val()) { // add red border if required input element is empty
                    input.addClass("inputRedborder");
                    if (controlName === "Personal Postal Code") { // add red border to Postal City if Postal Code is empty
                        postalCity.addClass("inputRedborder");
                    }
                    if (!pAddress.val() && !postalCode.val()) { // if all address fields are empty, add red border also to EmailAddress
                        eMail.addClass("inputRedborder");
                    }
                } else if (input.val()) { // remove red border if required input element is empty
                    $(input).removeClass("inputRedborder");
                    eMail.removeClass("inputRedborder");
                    if (controlName === "Personal Postal Code") {
                        postalCity.removeClass("inputRedborder");
                    }
                }
            }
        });
    }
    // general logic for rest of the required fields
    else {
        if (!input.val()) { // add red border if required input element is empty
            input.addClass("inputRedborder");
        } else if (input.val()) { // remove red border if required input element is empty
            input.removeClass("inputRedborder");
        }
        // remove previously binded events
        input.unbind();
        // then add event listeners to input element to handle input changes
        input.bind('change keyup', function () {
            var input = $(this);
            if (!input.val()) { // add red border if required input element is empty
                input.addClass("inputRedborder");
            } else if (input.val()) { // remove red border if required input element is empty
                input.removeClass("inputRedborder");
            }
        });
    }
}

function removeRedBorder(controlName) {
    var input = $('[name=' + controls[controlName].GetInputName() + ']'); // get input element
    input.removeClass("inputRedborder"); // remove red border
}

function fieldValidation() {
    var consentCheckbox = $('[name=' + controls["R Consent"].GetInputName() + ']');
    //setting required fields
    var requiredFields = ["Birth Date", "CellularPhoneNum", "FirstName", "LastName", "EmailAddress", "Personal Address", "Personal Postal Code", "Personal City"];
    // check if applet is in query mode
    if (!pm.Get("IsInQueryMode")) {
        // check if Consent is checked
        if (consentCheckbox.is(":checked")) {
            // calling function for each required field
            $.each(requiredFields, function (i, controlName) {
                redBorder(controlName);
            });
        } else {
            // remove all red borders if Consent is not checked
            $.each(requiredFields, function (i, controlName) {
                removeRedBorder(controlName);
            });
        }
    } else {
        // remove all red borders if user enters query mode
        $.each(requiredFields, function (i, controlName) {
            removeRedBorder(controlName);
        });
    }
}

// R 01.02.16 DGR: Added
function ShowSelection() {
    //console.log("// ShowSelection");
    pm = this.GetPM();
    var consentCheckbox = $('[name=' + controls["R Consent"].GetInputName() + ']');
    consentCheckbox.addClass("hidden");
    fieldValidation();
    var signatureType = $('[name=' + controls["R Signature Type"].GetInputName() + ']');
    $(signatureType).focusout(function () {
        fieldValidation();
    })
}
\$\endgroup\$
  • \$\begingroup\$ Does this Siebel thing you are using use HTML? \$\endgroup\$ – SirPython Feb 17 '16 at 13:16
  • \$\begingroup\$ Yep, Siebel uses HTML to render the content. \$\endgroup\$ – Daniel G. R. Feb 17 '16 at 13:28
2
\$\begingroup\$

Modularity

I'm seeing one major problem with your code. It's not very readable. Sure you can add comments in there and read those, but that just adds to the clutter.

I think that your code's clutter and low readability is causing your grief (and those trying to review it). For instance, it might seem a bit easier to incorporate SirPython's answer into your code if it were a bit more modular.

Each section below is a step-by-step attempt to show how you could approach this problem in a more modular-ready fashion. It may not be a perfect or exact solution for you, but my goal is to help you break your code into more fine-grain reusable pieces.

Wrapper Functions

Sometimes these can seem pointless but I find them quite helpful for scripts/plugins whose code base is trending toward clutter.

//
// Toggles the highlighting of invalidity via css class.
// Parameters:
//  selector - jQuery selector for the element whose validity shall be toggled.
//  optionalSet - If supplied sets the validity to the indicated boolean status.
//
function toggleInvalid(selector, optionalSet) {
    $(selector).toggleClass(invalidCssClass, optionalSet);
}

//
// Checks if an element is empty given its jQuery selector.
//
function isEmpty(selector) {
    return $(selector).val() === "" || $(selector).val() === undefined;
}

//
// Checks the status of the consent checkbox.
//
function isConsentRequired() {
    return $("#consent").is(":checked");
}

As you can see, these functions are nothing special. They're short and to the point. They almost have no benefit, almost. These may be single lines, but they give names to pieces of code. This can have a substantial effect on readability (especially when revisiting a set of code 2 weeks later or for the first time, if it's someone else's code).

Objectives

You have a few objectives you wish to accomplish. The best thing to do is list them and try to conceptually separate them before attempting to code them, otherwise your code will become very coupled/cluttered.

  • Highlight an invalid element.
  • Specify special rules for determining if a particular input is valid.
  • Validity rules only matter is consent is required.
  • Update the validity of an element.

Now that we've broken these up it should be easier to manage these. As you can see the first wrapper function above covers your first objective quite nicely. Your second objective is basically a binding from an element to a function. And your third is the ability to ignore the bindings if consent isn't required.

Updating Validity

We need to be able to update the validity of an element at a few points in time.

  • When its value has changed.
  • When the form's consent requirements have changed.

When you have to do something multiple times what is the best way to handle it? Functions.

//
// Updates a bound element's validity status.
// Parameters:
//  selector - The jQuery selector for the element whose validity is to be refreshed.
//
function refreshElementValidity(selector) {
    // Consent is required.
    // Here you could look up the element's attr and
    // a function of whether it is valid or not.
    var $element = $(selector);
    var attr = $element.attr(propertyAttr);

    if (attr === undefined || attr === "") {
        // Attr non-existant. 
        // Element is not apart of this validity check.
        toggleInvalid($element, false);
        return true;
    }

    if (!validityCheckers.hasOwnProperty(attr)) {
        // Attr not found.
        // Element is not apart of this validity check.
        toggleInvalid($element, false);
        return true;
    }

    var isValid = validityCheckers[attr].isValid();
    toggleInvalid($element, isValid);
    return true;
}

validityCheckers is an object containing our bindings. We haven't created that yet, so we should probably get to it. We create it by binding elements as its properties.

Binding an Element

//
// Binds a validity function to the element(s).
// validityFunc - Returns true if the element is valid.
//
function bindValidity(selector, validityFunc) {
    var $doc = $(document);
    var $element = $(selector);
    var attr = $element.attr(propertyAttr);

    if (attr !== undefined && attr !== "") {
        // Save the element's selector and validityFunc under its defining attribute.
        validityCheckers[$element.attr(propertyAttr)] = {
            selector: selector,
            isValid: validityFunc
        };

        // NOTE: This is an important part. See explanation below in the events section of the post.
        $doc.on("change", selector, function () {
            refreshElementValidity(selector);
        });
    }
}

The important part of binding is the element and how to check the element's validity. So we're going to have the bind function take both the element's selector and a function it can execute that returns true/false of whether the element is valid or not.

Example Usage:

bindValidity("#email", function () {
    return $("#email").val() !== "" && $("#email").val() !== undefined;
});

This binds the email input with an id of email to a function that returns whether the email element is valid or in this specific example returns true if it is not empty. Defining it as a function parameter means that you can have different validity requirements for each element that you bind and the logic for each function is not clumped into one hard to read function.

Events

The last part of the program is the events. We obviously need things to happen at certain times.

Firstly we need to update the consent checkbox and refresh the validity of our form elements.

//
// Updates validity when the signature dropdown is changed.
//
$(document).on("change", "#signature", function (e) {
    var value = $("#signature").val();
    if (value === "Paper") {
        $("#consent").prop("checked", true);
    } else {
        $("#consent").prop("checked", false);
    }

    var propagateEvents = true;
    if (!isConsentRequired()) {
        // Consent is not required.
        // Nothing is invalid.
        toggleInvalid(this, false);
        return propagateEvents;
    }

    var form = $(this).closest("form");
    for (var property in validityCheckers) {
        // Ignore prototype properties:
        if (!validityCheckers.hasOwnProperty(property)) { continue; }

        refreshElementValidity(validityCheckers[property].selector, propagateEvents);
    }

    return propagateEvents;
});

Above the consent checkbox is updated just like in your original code. Then afterward we update each element based on isConsentRequired() and the validity function of each element.

Conclusion

This got really long really quick. Anyways, my goal with this was to help you make your code more modular since it appeared to me that it was causing you some possible confusion (based on conversion in the comments of SirPython's answer). So I hope this helps you with that, whether you go with my solution, SirPython's, or your own.

JSFiddle: Here

The fiddle only has the logic for a simple "is empty" check on the "email" input, but I hope you can see the potential.

\$\endgroup\$
  • \$\begingroup\$ Wow, thank you for a well explained answer and for your help! I will try to this to write my code more modular and less of a clutter. \$\endgroup\$ – Daniel G. R. Feb 19 '16 at 6:43
1
\$\begingroup\$

Sorry, I didn't have much time to put together a review, so this will be short.


:invalid

CSS already has a special selector that handles when forms aren't valid: :invalid. This will attach a style when the form is not valid.

Your HTML input box should have required:

<input type="..." required/>

Then, in your CSS file:

form:invalid {
    ... border style...
}
\$\endgroup\$
  • \$\begingroup\$ I don't think this will work, because the fields are only required if the "R Consent" checkbox is checked. Additionally, the border on the address fields should disappear when the email field is not empty, and vice versa. \$\endgroup\$ – Daniel G. R. Feb 17 '16 at 14:13
  • \$\begingroup\$ @DanielG.R. I am confused about the "R Consent" part; is that part of this Siebel thing? As for the email, the border will only disappear when a valid email is entered. \$\endgroup\$ – SirPython Feb 17 '16 at 22:59
  • \$\begingroup\$ Made a working JSfiddle to demonstrate what I want to accomplish: jsfiddle.net/gnosoxz1. \$\endgroup\$ – Daniel G. R. Feb 18 '16 at 9:09
  • \$\begingroup\$ For the "R Consent" you could just toggle the form elements "required" attribute when it is checked/unchecked. That way you gain the simplicity of this solution without losing any apparent functionality. \$\endgroup\$ – Shelby115 Feb 18 '16 at 13:35
  • \$\begingroup\$ @Shelby115 Is this message to me to put into my answer, or to the OP? I'm still not sure what this "R Consent" thing is. \$\endgroup\$ – SirPython Feb 18 '16 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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