Remove duplication to improve code

Cleaning up issues found after the previous work is what we’re up to today. We will be doing the following:

  • moving showValid code into the check function
  • removing the other password validation rules from elsewhere in the code
  • replacing checkEmpty with hasContent throughout all of the code

Move the showValid inline into the check function

The showValid function is a lonely leftover from after cleaning up the rules functions. The showValid function doesn’t really belong by itself there now, and it’s only used in one place, so we should move it in to the check function where it belongs.

    // function showValid(inputGroup) {
    //     const msg = " is Ok: Your data has been entered correctly";
    //     inputStatus.ok(inputGroup, getName(inputGroup) + msg);
    // }
//...
    function check(inputGroup, validators) {
        //...
        const isValid = validateByTypes(inputGroup);
        if (isValid) {
            // showValid(inputGroup);
            const msg = " is Ok: Your data has been entered correctly";
            inputStatus.ok(inputGroup, getName(inputGroup) + msg);
        }
    }

Bring password and email code into the one place

When taking care of the validate rule functions, we noticed that the password validation is being used in multiple places. Do I remove the password rules from the validate code and have it repeated in several different places? I don’t think so. As we have had email and password rules kept within the validate code, it makes sense to have it remain there.

That means making the email and password validations available from the validate code. As that’s changing the public interface to the code, we should use tests to help check that everything’s working as expected:

Here is the email test that we add to validate.test.js

validate.test.js

    describe("makes matchers public", function () {
        it("matches using public email matcher", function () {
            emailInput.value = "email@example.com";
            validate.check($(emailGroup), {
                "E-mail": [
                    validate.fn.isEmail
                ]
            });
            expect($emailError.html()).to.contain("is Ok");
        });
        it("doesn't match using public email matcher", function () {
            emailInput.value = "not an email";
            validate.check($(emailGroup), {
                "E-mail": [
                    validate.fn.isEmail
                ]
            });
            expect($emailError.html()).to.contain("is Incorrect");
        });
    });

and here are the password tests that we add to validate.test.js

        describe("matches with password", function () {
            const passwordGroup = $("#login .form-group").has("[name='Password']").get(0);
            const passwordInput = $(passwordGroup).find("input").get(0);
            const $passwordError = $(passwordGroup).find(".error");
            it("is at least six characters", function () {
                passwordInput.value = "abcdefg";
                validate.check($(passwordGroup), {
                    "Password": [
                        validate.fn.passwordAtLeastSix
                    ]
                });
                expect($passwordError.html()).to.contain("is Ok");
            });
            it("is less than six characters", function () {
                passwordInput.value = "abcde";
                validate.check($(passwordGroup), {
                    "E-mail": [
                        validate.fn.passwordAtLeastSix
                    ]
                });
                expect($passwordError.html()).to.contain("is Incorrect");
            });
            it("is less than thirteen characters", function () {
                passwordInput.value = "abcdefghijkl";
                validate.check($(passwordGroup), {
                    "Password": [
                        validate.fn.passwordBelowThirteen
                    ]
                });
                expect($passwordError.html()).to.contain("is Ok");
            });
            it("is less than six characters", function () {
                passwordInput.value = "abcdefghijklm";
                validate.check($(passwordGroup), {
                    "E-mail": [
                        validate.fn.passwordBelowThirteen
                    ]
                });
                expect($passwordError.html()).to.contain("is Incorrect");
            });
        });

Those tests result help to confirm that we are returning the isEmail and password matchers from the validate code:

    return {
        //...
        fn: {
            getName,
            getValue,
            checkEmpty,
            isEmail: defaultValidators["E-mail"][1],
            passwordAtLeastSix: defaultValidators.Password[1],
            passwordBelowThirteen: defaultValidators.Password[2]
        }
    };

We can now use those matchers from the registration code, letting us remove those parts from the registration validation:

        const validators = {
            //...
            // passwordAtLeastSix: {
            //     regex: /^([a-zA-Z0-9]{6,})+$/,
            //     error: "Please enter at least 6 characters"
            // },
            // passwordBelowThirteen: {
            //     regex: /^[a-zA-Z0-9]{1,12}$/,
            //     error: "Please enter no more than 12 characters"
            // },
            //...
        };
//...
            "Password": [
                validate.fn.checkEmpty,
                createMatcher("differentThanFirstname"),
                createMatcher("differentThanLastname"),
                createMatcher("differentThanCity"),
                // createMatcher("passwordAtLeastSix"),
                validate.fn.passwordAtLeastSix,
                // createMatcher("passwordBelowThirteen"),
                validate.fn.passwordBelowThirteen
            ],

Replace checkEmpty with hasContent

When I remove checkEmpty from what validate returns, that leads to a few problems that we can fix up.

        fn: {
            getName,
            getValue,
            // checkEmpty,
            hasContent: createMatcher(validators.hasContent),

The first issue is: TypeError: check is not a function at checkInput

    function check(inputGroup, validators) {
        //...
            return types.every(function checkInput(check) {
                return check(inputGroup);
            });
        }
        //...
    }

Having the name check used in multiple places for different things becomes confusing. The validation types contain different matchers, so we can rename check to be matcher instead.

            // return types.every(function checkInput(check) {
            return types.every(function checkInput(matcher) {
                // return check(inputGroup);
                return matcher(inputGroup);
            });

Now that we have that potential confusion out of the way, we can investigate what happens there. We find that the list of types has an undefined one in there.

types: (4) [undefined, f, f, f]

and that brings us back to the test that triggered this issue, caused by us removing checkEmpty.

    it("is empty", function () {
        $firstnameInput.val("");
        callRegistrationInputHandler($firstnameInputGroup);
        expect($firstnameError.html()).to.equal("First Name is Empty: Please enter data into this input");
    });

The registration input handler is where we must next look:

        const nameValidationConfig = [
            validate.fn.checkEmpty,
            createMatcher("lessThanTwentyChars"),
            createMatcher("moreThanOneChar"),
            createMatcher("onlyAlphaChars")
        ];

and that is where the undefined part comes from. We removed checkEmpty, and it’s no longer available. We can update checkEmpty to instad be hasContent, and things should return back to working.

        const nameValidationConfig = [
            // validate.fn.checkEmpty,
            validate.fn.hasContent,
            createMatcher("lessThanTwentyChars"),
            createMatcher("moreThanOneChar"),
            createMatcher("onlyAlphaChars")
        ];

and that part of things returns back to working.

Checking for characters or no whitespace

The next issue helps to improve on an assumption that was made. The test called registration-input first name has strange characters: is failing because ## for the strange characters test is being thought of as being empty.

      + expected - actual

      -First Name is Empty: Please enter data into this input
      +First Name is Incorrect: Please enter upper case and lower case only

That’s because the hasContent test of /\w/ to match at a word character, is being too effective. We want the hasContent test to be less effective so that other tests can catch the situations that they need to look for.

Instead of checking for a word character, we can check that something that isn’t whitespace is there instead.

        hasContent: {
            // regex: /\w/,
            regex: /\S/,
            errorType: "Empty",
            error: "Please enter data into this input"
        },

and that strange characters test no longer has any trouble.

Convert remaining checkEmpty over to hasContent

We can now use the remaining failing tests to have us convert checkEmpty over to hasContent.

registration.js

        validate.check($cityGroup, {
            // "Your City": [validate.fn.checkEmpty]
            "Your City": [validate.fn.hasContent]
        });
//...
        return validate.check($formGroup, {
            //...
            "Phone Number": [
                // validate.fn.checkEmpty,
                validate.fn.hasContent,
                createMatcher("isPhoneNumber")
            ],
            "E-mail": [
                // validate.fn.checkEmpty,
                validate.fn.hasContent,
                createMatcher("isEmail")
            ],
            "Postal Address": [
                // validate.fn.checkEmpty,
                validate.fn.hasContent,
                createMatcher("postalAddress")
            ],
            "zip code": [
                // validate.fn.checkEmpty,
                validate.fn.hasContent,
                createMatcher("postcode")
            ],
            "Your City": [
                // validate.fn.checkEmpty
                validate.fn.hasContent,
            ],
            "Password": [
                // validate.fn.checkEmpty,
                validate.fn.hasContent,
                //...
            ],
            "Retype Password": [
                // validate.fn.checkEmpty,
                validate.fn.hasContent,
                createMatcher("matchesPassword")
            ]
        });

We are also directed to make similar changes to the change-password code:

change-password.js

    const retypeValidator = {
        "Password Retype": [
            // validate.fn.checkEmpty,
            validate.fn.hasContent,
            validate.createMatcher({
                fieldname: "Password",
                error: "Password doesn't match retyped password"
            })
        ]
    };

On searching the code for other occurances of checkEmpty, we only find one more in the validate.js code. That’s good news, for it also means that our tests have successfully covered all other uses of it.

Remove checkEmpty from the validate code

Now that other uses of checkEmpty have been removed, it’s relatively easy to remove checkEmpty from the validate code now.

    // function checkEmpty(inputGroup) {
    //     if (getValue(inputGroup) === "") {
    //         const msg = "Please enter data into this input";
    //         inputStatus.warning(
    //             inputGroup,
    //             getName(inputGroup) + " is Empty: " + msg
    //         );
    //         return false;
    //     }
    //     return true;
    // }
//...
    const validators = {
        hasContent: {
            regex: /\S/,
            errorType: "Empty",
            error: "Please enter data into this input"
        },
//...
    function checkFieldEmpty(formGroup) {
        const $inputField = $(formGroup).find("input, textarea");
        const name = $inputField.attr("name");
        const validations = [
            // checkEmpty
            createMatcher(validators.hasContent)
        ];
        check(formGroup, Object.fromEntries([
            [name, validations]
        ]));
    }

Summary

We moved showValid code into the check function, removed the other password validation rules from elsewhere in the code, and replaced checkEmpty with hasContent throughout all of the code.

The code as it stands today is found at v0.0.40 in releases

We only have two more sets of duplication to inspect. Next time we look at duplication in the change-password and login code.

2 Likes

Cleaning up the code is what we’re up to today. We will be doing the following:

  • Investigating a case of duplication
  • Finishing off linting the code

Duplication found in login and change-password code?

Here is the duplication that jsInspect has found.

Match - 2 instances

./registration3\change-password.js:1,43
// TODO: Clean up with JSLint
const changePassword = (function makeChangePassword() {
    function passwordsMatchRule(input) {
        return validate.fieldMatch("Password", input);
    }
    const retypeValidator = {
        "Password Retype": [
            validate.fn.hasContent,
            validate.createMatcher({
                fieldname: "Password",
                error: "Password doesn't match retyped password"
            })
        ]
    };

    function passwordInputHandler(evt) {
        validate.check(evt.target, retypeValidator);
    }

    function passwordSubmitHandler(evt) {
        validate.checkFormEmpty("#changepw");
        if ($("#changepw .warning").length !== 0) {
            evt.preventDefault();
        }
    }

    function passwordResetHandler() {
        inputStatus.resetForm($("#changepw"));
    }

    $("#changepw .form-group").on(
        "focusin focusout input",
        passwordInputHandler
    );
    $("#changepw").on("submit", passwordSubmitHandler);
    $("#changepw").on("reset", passwordResetHandler);
    return {
        eventHandler: {
            passwordInput: passwordInputHandler,
            passwordSubmit: passwordSubmitHandler,
            passwordReset: passwordResetHandler
        }
    };

./registration3\login.js:1,27
// TODO: Clean up with JSLint
const login = (function() {
    function loginInputHandler() {
        validate.check(this);
    }

    function loginSubmitHandler(evt) {
        validate.checkFormEmpty("#login");
        if ($("#login .warning").length !== 0) {
            evt.preventDefault();
        }
    }

    function loginResetHandler() {
        inputStatus.resetForm($("#login"));
    }

    $("#login .form-group").on("focusin focusout input", loginInputHandler);
    $("#login").on("submit", loginSubmitHandler);
    $("#login").on("reset", loginResetHandler);
    return {
        eventHandler: {
            loginInput: loginInputHandler,
            loginSubmit: loginSubmitHandler,
            loginReset: loginResetHandler
        }
    };

That’s the whole login and change-password code!

Yes I agree that there is some duplication there. The login and change-password forms are similar, but no real benefit is gained in trying to remove that duplication from here.

I do notice that we haven’t yet linted the code to clean up any remaining issues. The registation code that was cleaned up in v0.0.37 wasn’t quite complete, so we should get that done now.

Completing the cleanup of registration code

When linting the registration code I saw in the report that there was only one last issue left, that being to use double quotes instead of single quotes.

    // $('#terms').click(termsClickHandler);
    $("#terms").click(termsClickHandler);

My mistake was assuming that was the last error, and I only updated the example code in the post, leaving the registration code itself unchanged.

After making the desired change and running it through the linter again, I find that there’s a new range of issues that are found.

Anonymous function 1

Having previously used beautifier.io to tidy up the code, that lets JSLint easily find anonymous functions.

Anonymous functions are typically not a good idea, as it can be tricky to understand what they are supposed to do. Naming anonymous functions provides a significant benefit to us when we read through the code.

The first anonymous function is the main one that makes the registration object. I used to use the create keyword for this type of function, but that now gets confusing when we also use the Object.create method. As a result we use make now instead of create.

We can name that function makeRegistration, to help inform us that it’s being invoked and gives us an object in return.

// const registration = (function makeRegistration() {
const registration = (function() {

Anonymous function 2

The next anonymous function is right at the start of the registration code:

const registration = (function makeRegistration() {
    $(".icon").click(function() {
        $(".bar1").toggleClass("blue");
        $(".bar1").toggleClass("rotate45dg");
        $(".bar2").toggleClass("opacity");
        $(".bar3").toggleClass("rotate-45dg");
    });

We haven’t had the chance through the duplication removal to investigate this code. What is icon? What are the bar1/bar2/bar3 things? We can answer those while trying to figure out what to name the function.

That icon code doesn’t belong in the registration code. It defines behaviour for the collapsed menu button when used on a narrow screen. We should move it out to the setup code instead.

registration.js

const registration = (function makeRegistration() {
    // $(".icon").click(function() {
    //     $(".bar1").toggleClass("blue");
    //     $(".bar1").toggleClass("rotate45dg");
    //     $(".bar2").toggleClass("opacity");
    //     $(".bar3").toggleClass("rotate-45dg");
    // });

setup.js

$(".icon").click(function() {
    $(".bar1").toggleClass("blue");
    $(".bar1").toggleClass("rotate45dg");
    $(".bar2").toggleClass("opacity");
    $(".bar3").toggleClass("rotate-45dg");
});

Instead of using .icon, it gives a better understanding to instead refer to the navbar button.

// $(".icon").click(function() {
$(".navbar-header button").click(function() {
    $(".bar1").toggleClass("blue");
    $(".bar1").toggleClass("rotate45dg");
    $(".bar2").toggleClass("opacity");
    $(".bar3").toggleClass("rotate-45dg");
});

What the code does is to animate the three bars of a hamburger menu icon, into a cross shape for a close button. As a result, I can call the function animateClosingCross and leave things as they are.

// $(".navbar-header button").click(function() {
$(".navbar-header button").click(function animateClosingCross() {
    $(".bar1").toggleClass("blue");
    $(".bar1").toggleClass("rotate45dg");
    $(".bar2").toggleClass("opacity");
    $(".bar3").toggleClass("rotate-45dg");
});

Remove duplicate code

Also at the top of the registration code is this section of code to initialize the modal dialog.

registration.js

    $("#myBtn").click(function() {
        $("#myModal").modal();
    });

That is a duplicate of code that’s already in the setup.js code, so the registration set of code can be removed.

    // $("#myBtn").click(function() {
    //     $("#myModal").modal();
    // });

Unused warnings

We now have a range of unused warnings from the linter to take care of.

Unused ‘createNomatcher’

When we removed the need for the nomatcher method, we didn’t remove this function that’s no longer used. We can remove it now.

        // function createNomatcher(validatorName) {
        //     const validatorConfig = validators[validatorName];
        //     return validate.createNomatcher(validatorConfig);
        // }

Unused ‘i’

The resetMessages function that’s used by the jQuery each method, has the formGroup parameter being used but not the index parameter. jQuery’s each method is backwards compared with the JavaScript normal usage of forEach methods so it’s tricky to ignore the i parameter. Fortunately JSLint lets us name it ignore to help us know that we shouldn’t pay more attention to it.

    function resetMessages(i, formGroup) {
    function resetMessages(ignore, formGroup) {
        //...
    }

My personal preference is to use the JavaScript forEach method instead, which has formGroup as the first argument. But as jQuery is wanting to be used here, we had better stay as much as practical with jQuery usage.

Unused ‘$error’

In the resetMessages function, because we are using inputStatus to show the messages, we no longer need direct access to the error field. That part can be removed.

    function resetMessages(formGroup) {
        // const $error = $(formGroup).find(".error");
        const name = $(formGroup).find(".check").attr("name");
        inputStatus.errorOk(formGroup, name);
        inputStatus.feedbackNone(formGroup);
        inputStatus.requiredOk(formGroup);
    }

Unused evt

Lastly, when setting up the registration reset handler, we used an evt parameter to provide easy access to the event objct should we need it. We don’t need it, so that can be removed too.

    // function registrationResetHandler(evt) {
    function registrationResetHandler() {
        document.querySelectorAll(".form-group").forEach(resetMessages);
        removeTermWarning();
    }

All of those updates result in the registration code being much more cleaned up than it was before.

Clean up the login code

As it doesn’t seem that we will revisit the login code, we should perform a final cleanup of that code too.

Unexpected ‘this’

The only issue with the login code is that the this keyword is being used.

    function loginInputHandler() {
        validate.check(this);
    }

We should get the check argument from the event object instead. We can start by telling the test to pass a replica of the event object to the handler:

login-input-email.test.js

    function loginInputHandler() {
        const inputHandler = login.eventHandler.loginInput;
        // const thisArg = $emailGroup.get(0);
        const evt = {target: $emailGroup.get(0)};
        // inputHandler.call(thisArg);
        inputHandler(evt);
    }

The same change gets made to the other login test files too, and we can easily update the login code to use the event target instead.

    // function loginInputHandler() {
    function loginInputHandler(evt) {
        // validate.check(this);
        validate.check(evt.target);
    }

Give anonymous function a name

Similar as to what we did with the registration code, we should give the login function a name:

const login = (function makeLogin() {

And that’s all of the issues found in the login code.

Clean up the change-password code

The anonymous function in change-password can be named in the same way as we have done with the login code.

// const changePassword = (function() {
const changePassword = (function makeChangePassword() {

Unused ‘passwordsMatchRule’

The linter has found a function that we used to use, that is no longer being used. We can easily remove that function. I still keep an eye on the tests as we remove the code to ensure that they are all passing, just in case something else relied on it.

change-password.js

    // function passwordsMatchRule(input) {
    //     return validate.fieldMatch("Password", input);
    // }

Improve event handlers

While updating the change-password code, I see that all of the event handlers are grouped together at the end of the code. We should group them together in the other parts of the code too.

registration.js

    // $(".input-group").on("input", registrationInputHandler);
//...
    // $(".citylist li").click(citylistClickHandler);
//...
    // $("#terms").click(termsClickHandler);
//...
    // $("#registration").on("submit", registrationSubmitHandler);
//...
    $(".input-group").on("input", registrationInputHandler);
    $(".citylist li").click(citylistClickHandler);
    $("#terms").click(termsClickHandler);
    $("#registration").on("submit", registrationSubmitHandler);
    $("#registration").on("reset", registrationResetHandler);

Group event handlers together

While we are grouping similar things together, the event handler functions should also be grouped together near the end of the code too.

    function citylistClickHandler(evt) {
        //...
    }
    function registrationInputHandler(evt) {
        //...
    }
    function termsClickHandler() {
        updateTerms();
    }
    function registrationSubmitHandler(evt) {
        validate.checkFormEmpty("#registration");
        updateTerms();
        if ($("#registration .warning").length !== 0) {
            evt.preventDefault();
        }
    }
    function registrationResetHandler() {
        document.querySelectorAll(".form-group").forEach(resetMessages);
        removeTermWarning();
    }

Simplify code

The event handlers should also be kept small and simple, to improve our ability to understand them.

    function selectAndValidate(listItem) {
        const $form = $(listItem).closest("form");
        const $cityField = $form.find("[name='Your City']");
        const $cityGroup = $cityField.closest(".form-group");
        const value = $(listItem).text().trim();
        $cityField.val(value);
        $("#citylist").collapse("hide");

        validate.check($cityGroup, {
            "Your City": [validate.fn.hasContent]
        });
    }
    function citylistClickHandler(evt) {
        // const listItem = evt.target;
        //...
        // validate.check($cityGroup, {
        //     "Your City": [validate.fn.hasContent]
        // });
        selectAndValidate(evt.target);
    }

The registration input handler is also simplified by moving the code into a validateFields function.

    function validateFields(inputGroup) {
        const validators = {
        //...
        return validate.check($formGroup, {
            //...
        });
    }
    function registrationInputHandler(evt) {
        // const validators = {
        // ...
        // return validate.check($formGroup, {
        //     ...
        // });
        validateFields(evt.target);
    }

I’m also noticing that the validateFields function contains quite a lot of configuration, that can be extracted out of the function.

    const validatorConfig = {
        lessThanTwentyChars: {
            regex: /^.{1,19}$/,
            error: "Please enter no more than 19 char"
        },
        moreThanOneChar: {
            regex: /^.{2,}$/,
            error: "Please enter 2 upper case or lower case at least"
        },
        onlyAlphaChars: {
            regex: /^([a-zA-Z]{1,})+$/,
            error: "Please enter upper case and lower case only"
        },
        isPhoneNumber: {
            regex: /^\(?([0-9]{4})\)?[\u0020.\-]?([0-9]{3})[\u0020.\-]?([0-9]{4})$/,
            error: "Please enter Phone Number correctly"
        },
        isEmail: {
            regex: /^([\w\-.]+@([\w\-]+\.)+[\w\-]{2,4})?$/,
            error: "Please enter it correctly"
        },
        postalAddress: {
            regex: /^\d+\s[A-z]+\s[A-z]+/g,
            error: "Please enter Address correctly"
        },
        postcode: {
            regex: /^[a-zA-Z]{1,2}([0-9]{1,2}|[0-9][a-zA-Z])\s*[0-9][a-zA-Z]{2}$/,
            error: "Please enter Post-code correctly"
        },
        differentThanFirstname: {
            fieldname: "First Name",
            shouldMatch: false,
            error: "Password shouldn't match first-name"
        },
        differentThanLastname: {
            fieldname: "Last Name",
            shouldMatch: false,
            error: "Password shouldn't match last-name"
        },
        differentThanCity: {
            fieldname: "Your City",
            shouldMatch: false,
            error: "Password shouldn't match city name"
        },
        matchesPassword: {
            fieldname: "Password",
            error: "Password doesn't match retyped pwd"
        }
    };
//...
        function createMatcher(validatorName) {
            const config = validatorConfig[validatorName];
            return validate.createMatcher(config);
        }

and lastly, we can extract out some of the validators to a separate object too.

        const validators = {
            //...
        };
        return validate.check($formGroup, validators);

That’s about the last of the cleaning up of the code.

Summary

We investigated a case of duplication, finding that it doesn’t need to be removed, and finished off linting the code.

The code as it stands today is found at v0.0.41 in releases

Next time we investigate the second of four sets of remaining duplication.

2 Likes

Removing duplication from when accessing the input field is what we’re up to today. We will be doing the following:

  • Make the form reset messages similar to each other
  • Investigate the cause of duplication
  • Devise a plan to remove that duplication

Duplication found when accessing the .check field

The next set of duplication is in regard to finding the input field to check in a form group.

Match - 3 instances

./registration3\input-status.js:115,119
formGroups.forEach(function resetGroup(formGroup) {
    var inputName = $(formGroup).find(".check").attr("name");
    inputStatus.errorOk(formGroup, "Your " + inputName);
    inputStatus.feedbackNone(formGroup);
});

./registration3\registration.js:115,117
function resetMessages(ignore, formGroup) {
    const name = $(formGroup).find(".check").attr("name");
    inputStatus.errorOk(formGroup, name);

./registration3\validate.js:113,115
function checkFormEmpty(form) {
    const $formGroups = $(form).find(".form-group").has(".check");
    $formGroups.each(function validateGroup(ignore, formGroup) {

Is it appropriate to have each section use a separate function to get the input to check? I don’t think so.

Another option is to have one of the sections of code have a public function to retrieve the input to check, and have the other sections of code use that function. That doesn’t seem right either.

Make messages consistent with each other

Another thing that I notice that could be useful is that the errorOk function is being given different names to show. In one case it’s being given "Your " + inputName and in another case it’s just being given name. As the registration form isn’t using “Your”, we should stay consistent with that.

This means updating the tests so that any which expect Your at the start of a message, excluding of course Your City which is a field name in its own right.

inputstatus.test.js

        it("resets one message", function () {
            $error1.find(".error").html("");
            inputStatus.resetForm($form);
            // expect($error1.html()).to.equal("Your " + $name1);
            expect($error1.html()).to.equal($name1);
        });
        it("resets another message", function () {
            $error2.find(".error").html("");
            inputStatus.resetForm($form);
            // expect($error2.html()).to.equal("Your " + $name2);
            expect($error2.html()).to.equal($name2);
        });

We can now update the input-status code so that the Your prefix is not used:

    function resetForm($form) {
        const formGroups = $form.find(".form-group").toArray();
        formGroups.forEach(function resetGroup(formGroup) {
            var inputName = $(formGroup).find(".check").attr("name");
            // inputStatus.errorOk(formGroup, "Your " + inputName);
            inputStatus.errorOk(formGroup, inputName);
            inputStatus.feedbackNone(formGroup);
        });
    }

The remaining tests that care about the Your prefix can be updated too.

login-reset.test.js

   it("uses reset event to reset the form", function () {
        $emailError.html("");
        $("#login").trigger("reset");
        // expect($emailError.html()).to.contain("Your E-mail");
        expect($emailError.html()).to.equal("E-mail");
    });
    it("shows email message", function () {
        $emailError.html("");
        loginResetHandler();
        // expect($emailError.html()).to.equal("Your E-mail");
        expect($emailError.html()).to.equal("E-mail");
    });

changepassword-reset.test.js

    it("resets email message", function () {
        const $emailGroup = $("#changepw .form-group").eq(1);
        const $emailError = $emailGroup.find(".error");
        $emailError.html("test content");
        passwordResetHandler(fakeEvt);
        // expect($emailError.html()).to.equal("Your E-mail");
        expect($emailError.html()).to.equal("E-mail");
    });

Exploring duplication removal options

There might be something that we can do to remove the duplication that was found. I’m not quite sure what that is yet, so let’s work through a few possible ideas to explore what can be done.

The errorOk function almost always receives the same type of message.

[Note: This is where my hindbrain becomes disturbed]

As the inputGroup and the message are connected, in that the input field’s name attribute is what starts the message, the tests must follow that same behaviour too.

[Note: This is where I become aware of the problem]

But that’s not all. The reset messages also use errorOk which tends to indicate that we should have a separate errorReset function too.

[Note: And here is where I understand the issue]

Another problem comes to mind too. if I add an errorReset function just so that the errorOk function can show a boiler-place success message, that causes the inputStatus code to be too specific. That isn’t where the consistant success message should come from.

Instead, the success messages should only come from the validate code itself.

It all becomes clear to me now. Each set of code needs to have clearly defined boundaries.

  1. The inputStatus code must know nothing about which error message to give.
  2. The registration code (along with login, and changepassword code) must tell something else to reset the form.
  3. The validate code is the only place where the name is retrieved from input fields.

In terms of the duplication that was found, inputStatus needs to receive a reset message, the registration code needs to tell something else (validate.js for now) to reset the form, and the validate code needs to be the only place where the name of the input field is retrieved.

Summary

We made the form reset messages similar to each other, investigate the cause of duplication, and devised a plan to remove that duplication.

The code as it stands today is found at v0.0.42 in releases

Next time we remove duplication from when accessing the input fields.

2 Likes

Removing duplication from when accessing input fields, is what we’re up to today. We will be doing the following:

  • Removing inputStatus knowledge about reset messages
  • Updating registration to use inputStatus.resetForm
  • Checking that only validate gets input field name

Deal with duplication when getting input field name

Last time we figured out a plan of attack on the duplication that was found.

  1. The inputStatus code must know nothing about which error message to give.
  2. The registration code (along with login, and changepassword code) must tell something else to reset the form.
  3. The validate code is the only place where the name is retrieved from input fields.

Let’s see how it goes.

inputStatus should know nothing about which message to give

In the inputStatus tests, the reset tests aren’t given a message of what to show. They just assume that it’s the input field name that’s wanted. Instead, they should be given the message to show.

inputstatus.test.js

        it("resets one message", function () {
            $error1.find(".error").html("");
            // inputStatus.resetForm($form);
            inputStatus.resetForm($form, "Test message");
            // expect($error1.html()).to.equal($name1);
            expect($error1.html()).to.equal("Test message");
        });
        it("resets another message", function () {
            $error2.find(".error").html("");
            inputStatus.resetForm($form);
            expect($error2.html()).to.equal($name2);
        });

That results in a those tests failing, so we can update the inputStatus code to make them pass.

input-status.js

    // function resetForm($form) {
    function resetForm($form, message) {
        const formGroups = $form.find(".form-group").toArray();
        formGroups.forEach(function resetGroup(formGroup) {
            var inputName = $(formGroup).find(".check").attr("name");
            // inputStatus.errorOk(formGroup, inputName);
            inputStatus.errorOk(formGroup, message);
            inputStatus.feedbackNone(formGroup);
        });
    }

That is going to cause problems though because it’s not the same message being given. Instead of giving a message to the resetForm function, we can give it a callback function instead. That way we can accept a formGroup reference, and from that figure out what message to give.

inputstatus.test.js

        function messageCallback(formGroup) {
            return "Test message";
        }
        it("resets one message", function () {
            $error1.find(".error").html("");
            // inputStatus.resetForm($form, "Test message");
            inputStatus.resetForm($form, messageCallback);
            expect($error1.html()).to.equal("Test message");
        });

input-status.js

    function resetForm($form, messageCallback) {
        const formGroups = $form.find(".form-group").toArray();
        formGroups.forEach(function resetGroup(formGroup) {
            // var inputName = $(formGroup).find(".check").attr("name");
            var message = messageCallback(formGroup);
            inputStatus.errorOk(formGroup, message);
            inputStatus.feedbackNone(formGroup);
        });
    }

The code that uses resetForm can now be updated:

change-password.js

    function passwordResetHandler() {
        // inputStatus.resetForm($("#changepw"));
        inputStatus.resetForm($("#changepw"), validate.fn.getName);
    }

login.js

    function loginResetHandler() {
        // inputStatus.resetForm($("#login"));
        inputStatus.resetForm($("#login"), validate.fn.getName);
    }

That helps me to realise that the registration code doesn’t yet use the resetForm, which it can do now.

Registration tells something else to reset the form

This ties in nicely what what we just noticed, about login and change-password both using inputStatus.resetForm but registration not yet doing so.

Updating the registration code to use inputStatus.resetForm is easily achieved.

registration.js

    // function resetMessages(ignore, formGroup) {
    //     const name = $(formGroup).find(".check").attr("name");
    //     inputStatus.errorOk(formGroup, name);
    //     inputStatus.feedbackNone(formGroup);
    //     inputStatus.requiredOk(formGroup);
    // }
//...
    function registrationResetHandler() {
        // $(".form-group").each(resetMessages);
        inputStatus.resetForm($("#registration"), validate.fn.getName);
        removeTermWarning();
    }

Ensure that the input field name is only retrieved by validate code

When searching all of the code for ".check" I find that nothing else uses it, and it’s only retrieved from one place in the validate code.

Summary

We have removed inputStatus knowledge about reset messages, updated registration to use inputStatus.resetForm, and checked that only the validate code gets input field name.

The code as it stands today is found at v0.0.43 in releases

Next time we examine the final sets of duplication, and perform a visual check over the code for any final issues.

3 Likes

Examining final sets of duplication and cleaning up, is what we’re up to today. We will be doing the following:

  • Investigating the remaining duplication
  • Cleaning up index.html and setup.js
  • Cleaning up registration.js, login.js, and change-password.js
  • Cleaning up validate.js

Duplication found in login and change password code

Of the three sets of duplication that have been found, the first set is that the login and change-password code is quite similar. We are okay with that.

Duplication found in return statements

The second set of duplication is as follows:

Match - 2 instances

./registration3\input-status.js:119,129
        inputStatus.feedbackNone(formGroup);
    });
}
return {
    setNone,
    setOk,
    setWarning,
    errorOk,
    errorWarning,
    feedbackNone,
    feedbackOk,

./registration3\validate.js:116,126
        checkFieldEmpty(formGroup);
    });
}
return {
    checkRx,
    fieldMatch,
    createMatcher,
    check,
    checkFieldEmpty,
    checkFormEmpty,
    fn: {

This is not a type of duplication that we should remove. It’s okay and appropriate for the code to return an object with publiclly accessible functions.

Last set of potential duplication

Match - 2 instances

./registration3\registration.js:119,125
    inputStatus.requiredOk($termsGroup);
}

function selectAndValidate(listItem) {
    const $form = $(listItem).closest("form");
    const $cityField = $form.find("[name='Your City']");
    const $cityGroup = $cityField.closest(".form-group");

./registration3\validate.js:100,106
        inputStatus.ok(inputGroup, getName(inputGroup) + msg);
    }
}
function checkFieldEmpty(formGroup) {
    const $inputField = $(formGroup).find("input, textarea");
    const name = $inputField.attr("name");
    const validations = [

I’m having trouble figuring out what duplication has been found there.

As this is the last of the duplications that were found, we are now finished with the removal of duplication.

Cleaning up the code

Now that we’re not focused on issues of duplication, this is when taking a look through the code for other issues to clean up can be of help.

Cleaning up index.html

The index.html page has several things that can initially be cleaned up.

  • commented-out code can be removed
  • HTML attributes with a trailing space can be fixed up

Cleaning up the setup code

The setup.js code has some confusing parts in there. What is the difference between #changepw and #changepw2? I can help tidy that up by making it clear that one is on the login form, and the other is on the modal dialog itself.

setup.js

const $formChangepasswordLink = $("#changepsw");
const $modalChangepasswordLink = $("#changepsw2");
// $("#changepsw").on("click", showChangePassword);
$formChangepasswordLink.on("click", showChangePassword);
// $("#changepsw2").on("click", showChangePassword);
$modalChangepasswordLink.on("click", showChangePassword);

and the same can be done in there for the login link:

// $("#login2").on("click", showLogin);
const $loginLink = $("#login2");
$loginLink.on("click", showLogin);

and the login button:

const $loginButton = $("#myBtn");
$("#myBtn").on("click", function loginClickHandler() {

Cleaning up the registration code

In the registration code, the submit handler could do with a small improvement too.

Instead of checking if the length is not zero, it is more expressive to say that we are looking for more than zero.

registration.js

        // if ($("#registration .warning").length !== 0) {
        if ($("#registration .warning").length > 0) {
            evt.preventDefault();
        }

Another confusion occurs with the citylist click handler.

    $(".citylist li").click(citylistClickHandler);

It doesn’t make much sense for the citylist click handler to trigger on a listitem. It makes more sense for it to trigger on the citylist itself, and for the clicked item to then be dealt with after that.

    // $(".citylist li").click(citylistClickHandler);
    $(".citylist").click(citylistClickHandler);

And lastly, the list of event handlers are currently out of order with the handlers used in the code. To help keep things consistent there, reordering that list is done so that the reset and submit ones are at the end of the list.

        eventHandler: {
            registrationInput: registrationInputHandler,
            // registrationSubmit: registrationSubmitHandler,
            // registrationReset: registrationResetHandler,
            citylistClick: citylistClickHandler,
            termsClick: termsClickHandler,
            registrationReset: registrationResetHandler,
            registrationSubmit: registrationSubmitHandler
        }

Cleaning up the login code

With the login code, a similar improvement to the comparison code is done:

        // if ($("#login .warning").length !== 0) {
        if ($("#login .warning").length > 0) {

and the events are reordered in terms of expected usage, before finishing with reset and submit.

    function loginInputHandler(evt) {
        //...
    }
    // function loginSubmitHandler(evt) {
    //     //...
    // }
    function loginResetHandler() {
        //...
    }
    function loginSubmitHandler(evt) {
        //...
    }
    $("#login .form-group").on("focusin focusout input", loginInputHandler);
    // $("#login").on("submit", loginSubmitHandler);
    $("#login").on("reset", loginResetHandler);
    $("#login").on("submit", loginSubmitHandler);
    return {
        eventHandler: {
            loginInput: loginInputHandler,
            // loginSubmit: loginSubmitHandler,
            loginReset: loginResetHandler,
            loginSubmit: loginSubmitHandler
        }
    };

Cleaning up the change password code

The same improvements happen to the changepassword code too.

Cleaning up the validate code

With the validate code, nothing else uses the checkRx, fieldMatch, checkFieldEmpty, getValue, and isEmail functions don’t need to be returned:

validate.js

    return {
        // checkRx,
        // fieldMatch,
        createMatcher,
        check,
        // checkFieldEmpty,
        checkFormEmpty,
        fn: {
            getName,
            // getValue,
            hasContent: createMatcher(validators.hasContent),
            // isEmail: defaultValidators["E-mail"][1],
            passwordAtLeastSix: defaultValidators.Password[1],
            passwordBelowThirteen: defaultValidators.Password[2]
        }
    };

We can remove each of those one at a time, giving us the opportunity to fix up any issues in the process.

Fixing checkRx tests

Removing the checkRx from the validate return causes a few tests to fail.

validate.js

    return {
        // checkRx,
        fieldMatch,
        //...
    };

We need to use less invasive ways to test the checkRx code. In this case, that’s through validate.check() method by giving it a regular expression matcher.

validate.test.js

        const regexMatcher = validate.createMatcher({
            regex: /^[a-z]@[a-z]$/,
            error: "Test message"
        });
        const testValidator = {
            "E-mail": [regexMatcher]
        };
        it("checks that a field matches a regex", function () {
            emailInput.value = "anemail@example.com";
            // const regex = /[a-z]@[a-z]/;
            // const result = validate.checkRx(regex, emailInput);
            const result = validate.check(emailGroup, testValidator);
            // expect(result).to.equal(true);
            expect($emailError.html()).to.contain("entered correctly");
        });

Fix fieldMatch tests

Removing fieldMatch from the validate return causes a similar problem with the tests.

validate.js

    return {
        // fieldMatch,
        createMatcher,
        //...
    };

A similar technique as was used with checkRx can be used, by giving the validator fieldname to match against.

validate.test.js

        const $retypeGroup = $(retypeField).closest(".form-group");
        const $retypeError = $retypeGroup.find(".error");
        const passwordMatcher = validate.createMatcher({
            fieldname: "Password",
            error: "Password doesn't match retyped password"
        });
        const testValidator = {
            "Password Retype": [
                passwordMatcher,
            ]
        };
        it("checks that a field matches a value", function () {
            passwordField.value = "test password";
            retypeField.value = "test password";
            // const result = validate.fieldMatch("Password", retypeField);
            const result = validate.check($retypeGroup, testValidator);
            // expect(result).to.equal(true);
            expect($retypeError.html()).to.contain("is Ok");
        });
        it("checks that a field doesn't match", function () {
            passwordField.value = "test password";
            retypeField.value = "bad password";
            // const result = validate.fieldMatch("Password", retypeField);
            const result = validate.check($retypeGroup, testValidator);
            // expect(result).to.equal(false);
            expect($retypeError.html()).to.contain("doesn't match");
        });

Fix checkFieldEmpty tests

Removing checkFieldEmpty from the validate return causes a few more tests to fail.

validate.js

    return {
        //...
        // checkFieldEmpty,
        checkFormEmpty,
        //...
    };

The tests that fail after removing checkFieldEmpty from the return are easily fixed by running checkFormEmpty instead.

validate.test.js

    describe("checks empty", function () {
        it("checks a field is empty", function () {
            emailInput.value = "";
            $emailError.removeClass("warning");
            // validate.checkFieldEmpty(emailGroup);
            validate.checkFormEmpty(emailInput.form);
            expect($emailError.attr("class")).to.contain("warning");
        });
        it("checks a field is not empty", function () {
            emailInput.value = "test@example.com";
            $emailError.removeClass("ok");
            // validate.checkFieldEmpty(emailGroup);
            validate.checkFormEmpty(emailInput.form);
            expect($emailError.attr("class")).to.contain("ok");
        });
    });

Fix getValue tests

Removing getValue from the validate return causes nothing to break. Good news.

        fn: {
            getName,
            // getValue,
            hasContent: createMatcher(validators.hasContent),
            //...
        }

Fix isEmail tests

And finally removing isEmail from the validate return causes a few more tests to fail.

validate.js

        fn: {
            //...
            hasContent: createMatcher(validators.hasContent),
            // isEmail: defaultValidators["E-mail"][1],
            passwordAtLeastSix: defaultValidators.Password[1],
            //...
        }

We don’t need those tests because other validate tests already test the email validation.

validate.test.js

            it("matches using public email matcher", function () {
                emailInput.value = "email@example.com";
                // validate.check($(emailGroup), {
                //     "E-mail": [
                //         validate.fn.isEmail
                //     ]
                // });
                validate.check(emailGroup);
                expect($emailError.html()).to.contain("is Ok");
            });
            it("doesn't match using public email matcher", function () {
                emailInput.value = "not an email";
                validate.check(emailGroup);
                expect($emailError.html()).to.contain("is Incorrect");
            });

Summary

We investigated the remaining duplication, and cleaned up most of the code we’ve been working with including the index, setup, registration, login, change-password, and validate files.

The code as it stands today is found at v0.0.44 in releases

Next time we finish cleaning up, with the inputStatus code.

2 Likes

Cleaning up the inputStatus code is what we are up to today. We will be doing the following:

  • using internal function calls instead of external methods
  • investigating returned methods
  • simplifying the terms and conditions code
  • removing unused methods

Clean up inputStatus code

Not much needs to be cleaned up in this part of the code, except for the resetForm section where it should make an internal call to errorOk and feedbackNone instead of external calls.

This is a leftover from when moving previously external code into the inputStatus code. As the function calls are all available, the external calls aren’t needed anymore.

input-status.js

    function resetForm($form, messageCallback) {
        const $formGroups = $form.find(".form-group");
        $formGroups.each(function resetGroup(ignore, formGroup) {
            var message = messageCallback(formGroup);
            // inputStatus.errorOk(formGroup, message);
            errorOk(formGroup, message);
            // inputStatus.feedbackNone(formGroup);
            feedbackNone(formGroup);
        });
    }

Remove unused return methods

We have a lot of methods being returned from inputStatus, that seem to only be needed internally within inputStatus itself.

To best figure out what needs to be done, we should do a count of how often each method is called by other code:

Method          Count   Location
------          -----   --------
setNone           0
setOk             2     Registration terms
setWarning        1     Registration terms
errorOk           0
errorWarning      0
feedbackNone      1     Registration terms
feedbackOk        0
feedbackWarning   0
requiredOk        2     Registration terms
requiredWarning   1     Registration terms
ok                1     Validate
warning           1     Validate
resetForm         3     Registration, Login, and Change-password

It’s very clear from that investigation that the terms section currently does a lot more detailed work than anything else. If the terms code didn’t need such detail, we would only need the last three methods, those being ok, warning, and resetForm.

Can we have terms use ok and warning instead? Let’s take a look at some of it.

Simplify what Terms does with inputStatus

The updateTerms function helps us to see some of the trouble:

    function updateTerms() {
        const $termsGroup = $(".form-group").has("#terms");
        const $terms = $termsGroup.find("#terms");
        if ($terms.is(":checked")) {
            inputStatus.setOk($termsGroup.find(".error2"));
            inputStatus.requiredOk($termsGroup);
        } else {
            inputStatus.setWarning($termsGroup.find(".error2"));
            inputStatus.requiredWarning($termsGroup);
        }
    }

That error2 class is an obvious issue. Why was it given a different name? Does everything still work when it’s given a class name of error instead?

Here’s how we can switch things over without causing too many things to break.

1. Add error to the error2 element

Having both error and error2 on the element helps us to bridge the transition from one name to the other.

    <!--<span id="termcheck" class="error2">I agree to the <a href="#">Terms and Conditions</a>.</span>-->
        <span id="termcheck" class="error error2">I agree to the <a href="#">Terms and Conditions</a>.</span>

2. Update error2 tests to use error instead

The terms test currently looks for error2. We can update that to have it look for error instead.

        describe("terms", function () {
            const $termsGroup = $("#terms").closest(".form-group");
            // const $termsError = $termsGroup.find(".error2");
            const $termsError = $termsGroup.find(".error");
            const $termsRequired = $termsGroup.find(".starrq");

3. Switch the code over from error2 to error

Now that things are confirmed to work with error instead of error2, we can update the code:

        if ($terms.is(":checked")) {
            // inputStatus.setOk($termsGroup.find(".error2"));
            inputStatus.setOk($termsGroup.find(".error"));
            inputStatus.requiredOk($termsGroup);
        } else {
            // inputStatus.setWarning($termsGroup.find(".error2"));
            inputStatus.setWarning($termsGroup.find(".error"));
            inputStatus.requiredWarning($termsGroup);
        }

The tests that we have in place help to reassure us that everything keeps on working while making these changes.

4. Remove error2 from the HTML code

    <!--<span id="termcheck" class="error error2">I agree to the <a href="#">Terms and Conditions</a>.</span>-->
        <span id="termcheck" class="error">I agree to the <a href="#">Terms and Conditions</a>.</span>

5. Replace setOk and setWarning code with ok and warning methods instead

We can now simplify the terms code by having it use the ok and warning methods instead.

        if ($terms.is(":checked")) {
            // inputStatus.setOk($termsGroup.find(".error"));
            // inputStatus.requiredOk($termsGroup);
            inputStatus.ok($termsGroup);
        } else {
            // inputStatus.setWarning($termsGroup.find(".error"));
            // inputStatus.requiredWarning($termsGroup);
            inputStatus.warning($termsGroup);
        }

6. Simplify other Terms usage of inputStatus

In the removeTermWarning function, we can also replace setOk and requiredOk with just a single ok method.

    function removeTermWarning() {
        const $termsGroup = $("#terms").closest(".form-group");
        inputStatus.ok($termsGroup);
        inputStatus.feedbackNone($termsGroup);
        // inputStatus.setOk($termsGroup.find("#termcheck"));
        // inputStatus.requiredOk($termsGroup);
    }

7. Reinvestigate the use of each method

How does that update the stats from before?

Method          Old Count  New Count   Location
------          ---------  ---------   --------
setNone             0           0
setOk               2           0      Registration terms
setWarning          1           0      Registration terms
errorOk             0           0
errorWarning        0           0
feedbackNone        1           1      Registration terms
feedbackOk          0           0
feedbackWarning     0           0
requiredOk          2           0      Registration terms
requiredWarning     1           0      Registration terms
ok                  1           3      Validate
warning             1           2      Validate
resetForm           3           3      Registration, Login, and Change-password

That’s very good news.

8. Remove the unused methods

Now that most of the methods aren’t being used anymore, they are easily removed:

input-status.js

    return {
        // setNone,
        // setOk,
        // setWarning,
        // errorOk,
        // errorWarning,
        feedbackNone,
        // feedbackOk,
        // feedbackWarning,
        // requiredOk,
        // requiredWarning,
        ok,
        warning,
        resetForm
    };

As the above removed methods are all covered from the tests for the ok and warning methods, we can remove the tests for the ones we removed. The other remaining tests help to ensure that everything else here keeps on working as it should.

    // describe("error", function () {
    //     ...
    // });
    // describe("feedback", function () {
    //     ...
    // });
    // describe("required", function () {
    //     ...
    // });

That brings us to a satisfying conclusion of updating the inputStatus code.

Summary

We used internal function calls instead of external methods, investigated returned methods, simplified the terms and conditions code, and removed unused methods.

The code as it stands today is found at v0.0.45 in releases

Next time we summarise what was done to remove duplication from the original code.

2 Likes

A retrospective on what we did with the original duplication, is what we’re up to today.

How we dealt with the duplication

In the original code there are 22 different sets of duplication that are found. The default threshold is 30 so I dropped it to 20 to catch a few more issues.

The command that finds that duplication is:

> npx jsinspect -t 20 registration3 --ignore lib -I -L

Each of those sets of duplication has multiple instances of it scattered throughout the code.

We removed duplication having no initial plan on how to deal with it. Where duplication was in the same area, we used a separate function to remove that duplication. When it was across different areas, we used a separate module to help bridge that gap.

What follows is an examination of each set of duplication, and what ended up happening with them.

Duplication 1: Feedback status

50 instances found.

An example of that duplication is:

    .html(name + " is Fake text: Please remove repetition ");
$(this)
    .next()
    .find(".feedback")
    .removeClass("glyphicon glyphicon-ok")
    .addClass("glyphicon glyphicon-remove")
    .removeClass("ok")
    .addClass("warning");

We moved that duplication out to an inputStatus method called inputStatus.warning, that dealt with showing the message, and setting the error, feedback, and required class names.

Duplication 2: Input field color changes

43 instances found.

An example of that duplication is:

$(".fst-name-glyph")
    .on("mouseenter", function () {
        $(".fst-name-glyph")
            .css("background-color", "darkred");
        $(".fst-name-glyph")
            .css("color", "orange");
    });

This type of code was found to be no-longer needed, as CSS is able to much more easily achieve what it does instead.

A different set of duplication that was included in this lot was for showing the login and change-password forms, which was moved out to separate setup code.

Duplication 3: Registration form status messages

21 instances found.

An example of that duplication is:

if (/^([a-zA-Z]{2,16})+$/.test(value) === true) {
    $(this)
        .next()
        .find(".error")
        .html(name + " is OK: Your data has been entered correctly");

We used inputStatus.ok and inputStatus.warning methods to show the appropriate response.

Duplication 4: Login and change password status messages

21 instances found.

An example of that duplication is:

$(this).find(".error").html(inputattr+'  is ok').removeClass("warning").addClass("ok");
$(this).find(".feedback").removeClass("glyphicon glyphicon-remove").addClass("glyphicon glyphicon-ok").removeClass("warning").addClass("ok");

We moved the code out to an inputStatus.ok method.

Duplication 5: Input field color changes

20 instances found.

An example of that duplication is:

$(".fst-name-glyph")
    .on("mouseleave", function () {
        $(".fst-name-glyph")
            .css("background-color", "darkgreen");
        $(".fst-name-glyph")
            .css("color", "yellow");
    });
$(".fst-name-glyph")
    .on("mouseenter", function () {
        $(".fst-name-field")
            .css("background-color", "pink");
    });
$(".fst-name-glyph")
    .on("mouseleave", function () {
        $(".fst-name-field")
            .css("background-color", "lightgreen");
    });

Similar to duplication 2, we removed the code to allow more effective CSS code to replace it.

Duplication 6: Input value status messages

11 instances found.

An example of that duplication is:

if (value.length > 19) {
    // condition for more than 19 char
    $(this)
        .next()
        .find(".error")
        .html(name + " is Incorrect: Please enter no more than 19 char ");
    $(this)
        .next()
        .find(".error")
        .addClass('warning')
        .removeClass('ok');
    $(this)
        .next()
        .find(".feedback")
        .removeClass("glyphicon glyphicon-ok")
        .addClass("glyphicon glyphicon-remove")
        .removeClass("ok")
        .addClass("warning");
    $("#fnameRequired")
        .removeClass("ok")
        .addClass("warning");

Similar to duplication 3, we moved the rule out to a separate matcher, and used validate.check to show the appropriate response.

Duplication 7: Login and change password email validation

3 instances found.

An example of that duplication is:

if(emailReg.test(inputstr)) {
     $(this).find(".error").html(inputattr + " is Ok : Your data has been entered correctly ");
     $(this).find(".error").removeClass('warning').addClass('ok');
     $(this).find(".feedback").removeClass("glyphicon glyphicon-remove").addClass("glyphicon glyphicon-ok").removeClass("warning").addClass("ok");
     //$(this).css("border","2px solid green");

} else {
     $(this).find(".error").html(inputattr + " is Incorrect: Please enter it correctly ").addClass('warning').removeClass('ok');

Similar to duplication 3 and 6, we moved the rule out to a separate matcher, and used validate.check to show the appropriate response.

Duplication 8: Checking for fake text

6 instances found.

An example of that duplication is:

    if (fakeReg.test(value)) {
        $(this)
            .next()
            .find(".error")
            .html(name + " is Fake text: Please remove repetition ");
        $(this)
            .next()
            .find(".feedback")
            .removeClass("glyphicon glyphicon-ok")
            .addClass("glyphicon glyphicon-remove")
            .removeClass("ok")
            .addClass("warning");
        $(this)
            .next()
            .find(".error")
            .addClass('warning')
            .removeClass('ok');
        $("#fnameRequired")
            .removeClass("ok")
            .addClass("warning");   
        //$(this).css("border","2px solid red");
    } else {

Due to too many false-positive situations where potential fake text is actually real text, this type of test was removed.

Duplication 9: Checking for empty values

5 instances found.

An example of that duplication is:

if (inputstr != "") {
       $(this).find(".error").html(inputattr+'  is ok').removeClass("warning").addClass("ok");
       $(this).find(".feedback").removeClass("glyphicon glyphicon-remove").addClass("glyphicon glyphicon-ok").removeClass("warning").addClass("ok");

We changed the check from isEmpty to a hasContent matcher, and used validate.check to check that that matcher.

Duplication 10: Message that field is empty

7 instances found.

An example of that duplication is:

if (value === "") {
    $(this)
        .next()
        .find(".error")
        .html("Your " + name + " field is Empty !")

We gave the message to a hasContent matcher, and used validate.check to show the appropriate response.

Duplication 11: Password comparison in different situations

2 instances found.

An example of that duplication is:

./registration3\validate.js:1231,1427
if (name === "Password") {
    var pswReglow = /^([a-zA-Z0-9]{6,})+$/;
    var pswRegheigh = /^([a-zA-Z0-9]{13,})+$/; //13 or more occurences
    if (value.length > 0) {
        /*  
            var inputstr = $(this).val();
            var pswstr = $(this).val();
            var pswReglow = /^([a-zA-Z0-9]{6,})+$/;
            var pswRegheigh = /^([a-zA-Z0-9]{13,})+$/;//13 or more occurences
            */
        // fake text    
        //var inputstr = $(this).val();
        //var fakeReg = /(.)\1{2,}/;
        if (fakeReg.test(value)) {
            $(this)
                .next()
                .find(".error")
                .html(name + " is Fake text: Please remove repetition ");
            $(this)
                .next()
                .find(".feedback")
                .removeClass("glyphicon glyphicon-ok")
                .addClass("glyphicon glyphicon-remove")
                .removeClass("ok")
                .addClass("warning");
            $(this)
                .next()
                .find(".error")
                .addClass('warning')
                .removeClass('ok');
            $("#pwdRequired")
                .removeClass("ok")
                .addClass("warning");   
            //$(this).css("border","2px solid red");
        } else {
            //check if password and last-name is the same
            if (value === inputs["Your City"].value) {
                $(this)
                    .next()
                    .find(".error")
                    .html(name + " is Incorrect: Password shouldn't match city name");
                $(this)
                    .next()
                    .find(".error")
                    .addClass('warning')
                    .removeClass('ok');
                $(this)
                    .next()
                    .find(".feedback")
                    .removeClass("glyphicon glyphicon-ok")
                    .addClass("glyphicon glyphicon-remove")
                    .removeClass("ok")
                    .addClass("warning");
                $("#pwdRequired")
                    .removeClass("ok")
                    .addClass("warning");   
                //$(this).css("border","2px solid red");
            } else {
                if (value === inputs["Last Name"].value) {
                    $(this)
                        .next()
                        .find(".error")
                        .html(name + " is Incorrect: Password shouldn't match last-name ");
                    $(this)
                        .next()
                        .find(".error")
                        .addClass('warning')
                        .removeClass('ok');
                    $(this)
                        .next()
                        .find(".feedback")
                        .removeClass("glyphicon glyphicon-ok")
                        .addClass("glyphicon glyphicon-remove")
                        .removeClass("ok")
                        .addClass("warning");
                    $("#pwdRequired")
                        .removeClass("ok")
                        .addClass("warning");   
                    //$(this).css("border","2px solid red");
                } else {
                    //check if password and first-name is the same
                    if (value === inputs["First Name"].value) {
                        $(this)
                            .next()
                            .find(".error")
                            .html(name + " is Incorrect: Password shouldn't match fist-name ");
                        $(this)
                            .next()
                            .find(".error")
                            .addClass('warning')
                            .removeClass('ok');
                        $(this)
                            .next()
                            .find(".feedback")
                            .removeClass("glyphicon glyphicon-ok")
                            .addClass("glyphicon glyphicon-remove")
                            .removeClass("ok")
                            .addClass("warning");
                        $("#pwdRequired")
                            .removeClass("ok")

We moved the password rules into the validate code, and used validate.check to show the appropriate response.

Summary

With the vast majority of duplication that we had here, most of it was moved out to a common set of code, those being inputStatus for showing the messages and validate for checking the input fields.

Next time we finish off the second half of the retrospective.

2 Likes

Today we examine what was done with the second half of the original duplication.

How we dealt with the duplication

Duplication 12: Login and change password submit click event

5 instances found.

An example of that duplication is:

$(".button1color1").click(function() {
      $(".inputboxmodal1").each(function() {
        var st = $(this)

Click event code was replaced with a submit handler, and we moved code that checks for empty field values out to an inputStatus.checkFormEmpty method.

Duplication 13: Input focus handler

4 instances found.

An example of that duplication is:

$(".inputboxmodal1").on("focusin focusout input", function() {
     console.log("changed");   
    
     var inputattr = $(this)
                  .find(".input-check")
                  .attr("name");

A consistent input handler was used for each form.

Duplication 14: Email and password validation on Login and Change password forms

2 instances found.

An example of that duplication is:

//$(this).find(".error").html(inputattr+'  is ok');
/* E-mail filter */
         if (inputattr === "E-mail") {   
                if(emailReg.test(inputstr)) {
                     $(this).find(".error").html(inputattr + " is Ok : Your data has been entered correctly ");
                     $(this).find(".error").removeClass('warning').addClass('ok');
                     $(this).find(".feedback").removeClass("glyphicon glyphicon-remove").addClass("glyphicon glyphicon-ok").removeClass("warning").addClass("ok");
                     //$(this).css("border","2px solid green");
                
                } else {
                     $(this).find(".error").html(inputattr + " is Incorrect: Please enter it correctly ").addClass('warning').removeClass('ok');
                     $(this).find(".feedback").removeClass("glyphicon glyphicon-ok").addClass("glyphicon glyphicon-remove").removeClass("ok").addClass("warning");
                     //$(this).find(".error");
                     //$(this).css("border","2px solid red");
                }
  /* Password filter */             
                                     }
                                     
         if (inputattr === "Password") {
             if(fakeReg.test(inputstr)) {
                 $(this).find(".error").html(inputattr + " is Fake text: Please remove repetition ");
                 $(this).find(".feedback").removeClass("glyphicon glyphicon-ok").addClass("glyphicon glyphicon-remove").removeClass("ok").addClass("warning");
                 $(this).find(".error").addClass('warning').removeClass('ok');
                 //$(this).css("border","2px solid red");
                 } else {
                     if (!pswReglow.test(inputstr)) {
                         $(this).find(".error").html(inputattr + " is Incorrect: Please enter at lest 6 character ");
                         $(this).find(".feedback").removeClass("glyphicon glyphicon-ok").addClass("glyphicon glyphicon-remove").removeClass("ok").addClass("warning");
                         $(this).find(".error").addClass('warning').removeClass('ok');
                         //$(this).css("border","2px solid red");
                         } else {
                             if (!pswRegheigh.test(inputstr)) {
                     
                              // condition to check if length is bigger than 12 char
                                 
                                 $(this).find(".error").html(inputattr+" is OK: Your data has been entered correctly");
                                 $(this).find(".error").addClass('ok').removeClass('warning');
                                 $(this).find(".feedback").removeClass("glyphicon glyphicon-remove").addClass("glyphicon glyphicon-ok").removeClass("warning").addClass("ok");
                                 //$(this).css("border","2px solid green");   
                                  } else {
                                     $(this).find(".error").html(inputattr + " is Incorrect: Please enter no more than 12 character "+inputstr);
                                     $(this).find(".error").addClass('warning').removeClass('ok');    
                                     $(this).find(".feedback").removeClass("glyphicon glyphicon-ok").addClass("glyphicon glyphicon-remove").removeClass("ok").addClass("warning");

We moved the email and password rules as matchers into the validate code, and used validate.check to show the appropriate response.

Duplication 15: HasContent validation on Login and Change password form submit

4 instances found.

An example of that duplication is:

$(".button1color1").click(function() {
      $(".inputboxmodal1").each(function() {
        var st = $(this)
          .find(".input-check")
          .attr("name");
        var st2 = $(this)
          .find(".input-check")
          .val()
          .trim();
        if (
          $(this)
            .find(".input-check")
            .val()
            .trim() != ""
        ) {
          //$($(this).nextAll(".inputstatus")).find(".fielderror").text("Your " + st + " is OK ");
          //$(this).find(".fielderror").text("Your " + st + " is OK ");
          $(this)
            //.next()
            .find(".error")

We gave the hasContent rule as a matcher to validate.check, which used inputCheck to show the appropriate message.

Duplication 16: HasContent similarities on Login and Change password forms

2 instances found.

An example of that duplication is:

    .find(".error")
    .removeClass("warning")
    .addClass("ok");
 
  //alert(st2);
} else {
  //$($(this).nextAll(".inputstatus")).find(".fielderror").text("Your " + st + " is empty");
  $(this)
    //.next()
    .find(".error")
    .html("Your " + st + " is empty ");
  $(this)
    .find(".error")
    //.css("background-color", "pink");
    .css("color", "red");

We gave the hasContent rule as a matcher to validate.check, which used inputCheck to show the appropriate message.

Duplication 17: Retype password similarities

2 instances found.

An example of that duplication is:

          /* retype password  */
          
          if (name === "Retype Password") {
              if (value.length > 0) {
                  if (inputstr !== inputs.Password.value) {
                      $(this)
                          .next()
                          .find(".error")
                          .html(name + " is Incorrect: Password doesn't match retyped pwd ");
	$(this)
                          .next()
                          .find(".errorm")
                          .html(name + " is Incorrect: Password doesn't match retyped pwd ");	
                      $(this)
                          .next()
                          .find(".error")
                          .addClass('warning')
                          .removeClass('ok');
	$(this)
                          .next()
                          .find(".errorm")
                          .addClass('warning')
                          .removeClass('ok');	
                      $(this)
                          .next()
                          .find(".feedback")
                          .removeClass("glyphicon glyphicon-ok")
                          .addClass("glyphicon glyphicon-remove")
                          .removeClass("ok")
                          .addClass("warning");
	$("#pwreRequired")
		.removeClass("ok")
		.addClass("warning");	
                      //$(this).css("border","2px solid red");
                  } else {
                      $(this)
                          .next()
                          .find(".error")
                          .html(name + " is OK: Your data has been entered correctly " + inputstr);
                      $(this)
                          .next()
                          .find(".error")
                          .addClass('ok')
                          .removeClass('warning');
                      $(this)
                          .next()
                          .find(".feedback")
                          .removeClass("glyphicon glyphicon-remove")
                          .addClass("glyphicon glyphicon-ok")
                          .removeClass("warning")
                          .addClass("ok");
	$("#pwreRequired")
		.removeClass("warning")
		.addClass("ok");	
                      //$(this).css("border","2px solid green");
                  }
                  
                  // var pswReglow = /^([a-zA-Z0-9]{6,})+$/;
                  // var pswRegheigh = /^([a-zA-Z0-9]{13,})+$/;//13 or more occurences
                  
              } else {
                  $(this)
                      .next()
                      .find(".error")
                      .html(name + " is EMPTY: Please enter data into this input");
                  $(this)
                      .next()
                      .find(".error")
                      .addClass('warning')
                      .removeClass('ok');
                  $(this)
                      .next()
                      .find(".feedback")
                      .removeClass("glyphicon glyphicon-ok")
                      .addClass("glyphicon glyphicon-remove")
                      .removeClass("ok")
                      .addClass("warning");
$("#pwreRequired")
	.removeClass("warning")
	.addClass("ok");	
                  //$(this).css("border","2px solid red");
              }

We gave the passwords-match rule as a matcher to validate.check, which used inputCheck to show the appropriate message.

Duplication 18: Warning and success similarities

3 instances found.

An example of that duplication is:

if (name === "Phone Number") {
    if (inputstr.length > 0) {
        //var inputstr = $(this).val();
        var phoneReg =/^\(?([0-9]{4})\)?([ .-]?)([0-9]{3})\2([0-9]{4})$/;
        if (!phoneReg.test(inputstr)) {
            // email
            $(this)
                .next()
                .find(".error")
                .html(name + " is Incorrect: Please enter Phone Number correctly ");
            $(this)
                .next()
                .find(".error")
                .addClass('warning')
                .removeClass('ok');
            $(this)
                .next()
                .find(".feedback")
                .removeClass("glyphicon glyphicon-ok")
                .addClass("glyphicon glyphicon-remove")
                .removeClass("ok")
                .addClass("warning");
            $("#phoneRequired")
                .removeClass("ok")
                .addClass("warning");   
            //$(this).css("border","2px solid red");
        } else {
            $(this)
                .next()
                .find(".error")
                .html(name + " is Ok : Your Phone number has been entered correctly ");
            $(this)
                .next()
                .find(".error")
                .addClass('ok')
                .removeClass('warning');
            $(this)
                .next()
                .find(".feedback")
                .removeClass("glyphicon glyphicon-remove")
                .addClass("glyphicon glyphicon-ok")
                .removeClass("warning")
                .addClass("ok");
            $("#phoneRequired")
                .removeClass("warning")
                .addClass("ok");    
            //$(this).css("border","2px solid green");

The code to show a warning or success was moved to inputStatus.warning and inputStatus.ok, with validate.check showing the appropriate result.

Duplication 19: Login and change-password use the same code

2 instances found.

An example of that duplication is:

//$($(this).nextAll(".inputstatus")).find(".fielderror").text("Your " + st + " is OK ");
//$(this).find(".fielderror").text("Your " + st + " is OK ");
$(this)
  //.next()
  .find(".error")
  .html("Your " + st + " is OK ");
$(this)
  .find(".error")
  .css("color", "green");

$(this)
  //.next()
  .find(".feedback")
  //.html("Your " + st + " is OK ")
  .removeClass("glyphicon glyphicon-remove")
  .addClass("glyphicon glyphicon-ok ok");
$(this)

The code to show a success was moved to inputStatus.ok, where validate.check shows the appropriate result.

Duplication 20: Login and change-password uses the same reset code

2 instances found.

An example of that duplication is:

    .find(".error")
    .removeClass("warning")
    .addClass("ok");
 
  //alert(st2);
} else {
  //$($(this).nextAll(".inputstatus")).find(".fielderror").text("Your " + st + " is empty");
  $(this)
    //.next()
    .find(".error")
    .html("Your " + st);
  $(this)
    .find(".error")
    //.css("background-color", "pink");
    .css("color", "green");

The reset code was moved to an inputStatus.resetForm() method.

Duplication 21: City list code to show warning or success

2 instances found.

An example of that duplication is:

    .find("#errorid")
    .addClass('ok')
    .removeClass('warning');
$(".form-group")
    //.next()
    .find("#feedbackid")
    .removeClass("glyphicon glyphicon-remove")
    .addClass("glyphicon glyphicon-ok")
    .removeClass("warning")
    .addClass("ok");
$(".form-group")
    //.next()
    .find("#cityRequired")
    .addClass('ok')
    .removeClass('warning');    

The code was moved to an event handler that selects and uses validate.check on the city field to show the appropriate result.

Duplication 22: Update the terms and conditions messages

2 instances found.

An example of that duplication is:

        if($(this).is(":checked")){
            console.log("Checkbox is checked.");
$("#termcheck").addClass('ok').removeClass('warning');
$("#termsRequired").addClass('ok').removeClass('warning');
//alert(name+'checked');

The code to validate the terms checkbox was simplified by using inputStatus.ok and inputStatus.warning methods to show the appropriate result.

Summary

We used jsInspect to find duplication, tests to ensure that existing behaviour was preserved, and combined small simple changes to make large improvements to the code. All while keeping it in working order every step of the way.

The code as it stands today is found at v0.0.45 in releases

This brings the tutorial on removing duplication to a close.
I hope that you find something of value from the process.

4 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.