8
\$\begingroup\$

My code consists of two input fields and a submit button. The business rules require the following rules:

  1. At least one field must be filled out. Both can be filled out but both cannot be blank.
  2. If the first field is empty or 0, then the second field is required.
  3. If the second field is empty or 0, then the first field is required.
  4. The fields should not break if non-numeric values are input.
  5. If a field is not 0 or empty, then their respective minimum amount rules apply.

I've implemented the jQuery to retrieve the minimum amounts from data-rule- in each field in the HTML. These values will ultimately be managed by a CMS. The same goes for the validation messages.

My code behaves exactly as I want it and passes my unit tests, but I am wondering if the code could be simplified. I see that the code where foo and bar are assigned is repeated throughout the script. I tried to move them to variables outside of the validator logic, but it affected functionality and no longer behaved as needed. My guess is that the variables aren't updated at the same times that the validator code is executed. So I'm not sure where else I could put these variables so that the code isn't repeated.

Is this possible?

$.validator.addMethod('groterdan', function(value, element, param) {
  return (value >= param);
});

/*
var rauwOorspronkelijkeBedrag = $.trim($('#oorspronkelijkeBedrag').val());
var oorspronkelijkeBedrag = rauwOorspronkelijkeBedrag.length > 0 ? parseInt(rauwOorspronkelijkeBedrag, 10) : 0;
var rauwAanvullendBedrag = $.trim($('#aanvullendBedrag').val());
var aanvullendBedrag = rauwAanvullendBedrag.length > 0 ? parseInt(rauwAanvullendBedrag, 10) : 0;
*/

$('#beleggingsForm').validate({
  rules: {
    oorspronkelijkeBedrag: {
      required: function(element) {
        var rauwAanvullendBedrag = $.trim($('#aanvullendBedrag').val());
        var aanvullendBedrag = rauwAanvullendBedrag.length > 0 ? parseInt(rauwAanvullendBedrag, 10) : 0;
        return aanvullendBedrag === 0;
      },
      groterdan: {
        param: $('#oorspronkelijkeBedrag').data('rule-groterdan'),
        depends: function(element) {
          var rauwOorspronkelijkeBedrag = $.trim($('#oorspronkelijkeBedrag').val());
          var oorspronkelijkeBedrag = rauwOorspronkelijkeBedrag.length > 0 ? parseInt(rauwOorspronkelijkeBedrag, 10) : 0;
          var rauwAanvullendBedrag = $.trim($('#aanvullendBedrag').val());
          var aanvullendBedrag = rauwAanvullendBedrag.length > 0 ? parseInt(rauwAanvullendBedrag, 10) : 0;
          return oorspronkelijkeBedrag !== 0 || aanvullendBedrag === 0;
        }
      }
    },
    aanvullendBedrag: {
      required: function(element) {
        var rauwOorspronkelijkeBedrag = $.trim($('#oorspronkelijkeBedrag').val());
        var oorspronkelijkeBedrag = rauwOorspronkelijkeBedrag.length > 0 ? parseInt(rauwOorspronkelijkeBedrag, 10) : 0;
        return oorspronkelijkeBedrag === 0;
      },
      groterdan: {
        param: $('#aanvullendBedrag').data('rule-groterdan'),
        depends: function(element) {
          var rauwOorspronkelijkeBedrag = $.trim($('#oorspronkelijkeBedrag').val());
          var oorspronkelijkeBedrag = rauwOorspronkelijkeBedrag.length > 0 ? parseInt(rauwOorspronkelijkeBedrag, 10) : 0;
          var rauwAanvullendBedrag = $.trim($('#aanvullendBedrag').val());
          var aanvullendBedrag = rauwAanvullendBedrag.length > 0 ? parseInt(rauwAanvullendBedrag, 10) : 0;
          return oorspronkelijkeBedrag === 0 || aanvullendBedrag !== 0;
        }
      }
    },
    submitHandler: function() {
      //Update(); /* function that contains ajax code for the actual functionality but is not relevant to this post. Using "return false" for demonstration purposes instead */
      return false;
    }
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/jquery.validate.min.js"></script>
<form id="beleggingsForm">
  <input id="oorspronkelijkeBedrag" name="oorspronkelijkeBedrag" type="text" data-rule-groterdan="500" data-msg-groterdan="Oorspronkelijke bedrag moet >= 500 zijn" data-msg-required="Oorspronkelijke bedrag is verplicht" />
  <br/>
  <input id="aanvullendBedrag" name="aanvullendBedrag" type="text" data-rule-groterdan="50" data-msg-groterdan="Aanvullend bedrag moet >= 50 zijn" data-msg-required="Aanvullend bedrag is verplicht" />
  <br/>
  <input type="submit" />
</form>

\$\endgroup\$
7
  • \$\begingroup\$ Can you show REAL code, with real variables names instead of foo and bar everywhere? \$\endgroup\$
    – Mike Brant
    Feb 14, 2018 at 4:29
  • \$\begingroup\$ While those real variables are better (since they are the real code and thus contain no more complexity hidden behind example names and structures), this is an English-speaking website. Not many people here speak Dutch, so it would help reviewers understand your code a lot better, and help you getting better answers, if you could translate the variable names to their English equivalents (or at least close enough). \$\endgroup\$
    – Graipher
    Feb 14, 2018 at 10:40
  • 1
    \$\begingroup\$ @Graipher The only requirement is for the question text to be in English. Please do not pressure people to translate their code. Improperly translated code creates problems that are much worse than code with foreign identifiers. Furthermore, the choice of language is a valid subject for review, just like whitespace is. \$\endgroup\$ Feb 14, 2018 at 14:36
  • \$\begingroup\$ @200_success Fair enough. Even though that might lead to "Use English variable names" being the only answer (not the case here, though). But I guess in that case the OP can always ask a follow-up question. \$\endgroup\$
    – Graipher
    Feb 14, 2018 at 14:39
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$
    – Heslacher
    Feb 16, 2018 at 9:41

2 Answers 2

5
\$\begingroup\$

Some suggestions

Use element instead of new Jquery element selection

Why not use the element parameter, it may be the input you are selecting via jquery anyway. One less hardcoded variable you should worry about.

Minimize code duplication

The depends,required functions are both similar. What happens if you add another rule? Now there would be 6 of the same functions. Either make the function reusable from everywhere or simplify the expression.

Reduce variable dependancies

depends and required are hard to outsource because you depends on variables for the selector and boolean expression for the return. Maybe put the variables as function parameters?

function simplified(element, otherelement) {
    // Your job
}

...
    depends: simplified(element, otherelement)

Flexible parameter/variable count

If you add more variables later, use the new spread operator ... or arguments to retrieve any number of arguments inside your function.

function simplified(element) {
    for (let argument in arguments){
        //.val(), measure length, etc.
    }
}

function simplified(element, ...args) {
    for (let argument in args){
        //.val(), measure length, etc.
    }
}

Try using let/const instead of var

Var brings no benefit in your case, and since we are in a modern ES6 age you might as well use const in your case, since you are not reassigning the variables.

These are some (not testet) suggestions that should hopefully give you some inspirations.

EDIT # 1

As @RoToRa correctly pointed out values are calculated multiple times the same way. This is what I came up with a first cleanup of the code. Please don't see this as a full solution but rather a proof of concept how you can actually minify/cleanup your code when you think about it. It's far from perfect and doesn't count in when you are depending on more than 2 different input fields.

$.validator.addMethod('groterdan', function(value, element, param) {
  return (value >= param);
});

/*
var rauwOorspronkelijkeBedrag = $.trim($('#oorspronkelijkeBedrag').val());
var oorspronkelijkeBedrag = rauwOorspronkelijkeBedrag.length > 0 ? parseInt(rauwOorspronkelijkeBedrag, 10) : 0;
var rauwAanvullendBedrag = $.trim($('#aanvullendBedrag').val());
var aanvullendBedrag = rauwAanvullendBedrag.length > 0 ? parseInt(rauwAanvullendBedrag, 10) : 0;
*/
  
const getValue = function(target){
    const value = $.trim($('#' + target).val())
    return value.length > 0 ? parseInt(value, 10) : 0
}

const dependsCheck = function(element){
  return getValue(element.id) === 0;
};

const requiredCheck = function(element) {
  const otherTarget = element.id === "oorspronkelijkeBedrag" ? "aanvullendBedrag" : "oorspronkelijkeBedrag";
  
  return getValue(otherTarget) === 0;
};

$('#beleggingsForm').validate({
  rules: {
    oorspronkelijkeBedrag: {
      required: requiredCheck,
      groterdan: {
        param: $(this).data('rule-groterdan'),
        depends: dependsCheck
      }
    },
    aanvullendBedrag: {
      required: requiredCheck,
      groterdan: {
        param: $(this).data('rule-groterdan'),
        depends: dependsCheck
      }
    },
    submitHandler: function() {
      //Update(); /* function that contains ajax code for the actual functionality but is not relevant to this post. Using "return false" for demonstration purposes instead */
      return false;
    }
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/jquery.validate.min.js"></script>
<form id="beleggingsForm">
  <input id="oorspronkelijkeBedrag" name="oorspronkelijkeBedrag" type="text" data-rule-groterdan="500" data-msg-groterdan="Oorspronkelijke bedrag moet >= 500 zijn" data-msg-required="Oorspronkelijke bedrag is verplicht" />
  <br/>
  <input id="aanvullendBedrag" name="aanvullendBedrag" type="text" data-rule-groterdan="50" data-msg-groterdan="Aanvullend bedrag moet >= 50 zijn" data-msg-required="Aanvullend bedrag is verplicht" />
  <br/>
  <input type="submit" />
</form>

While adding this example i notice some more areas to improve:

Naming / language

I myself personally struggle with this regularly. One thing i got used to is writing all my code in english and not my native tongue. This gives you a good practice and other developers not familiar with your language can lookup more easily what you are actually doing in the code. Code is kind of documentation itself. When you can't read it, documentation is needed.

As an excercise for you... which two functions look similar and which two objects can be mapped together easily now?

\$\endgroup\$
6
  • \$\begingroup\$ Thank you for the reply! Could you please provide an example for me on how I could simplify or reduce code duplication? I attempted this on my own already, but wasn't able to get it to work. \$\endgroup\$
    – Iceape
    Feb 14, 2018 at 10:18
  • \$\begingroup\$ I will update it later if you don't mind, since right now i'm quite busy. Please take in mind that this is a code review and you should consider improving your code one step at a time. While I'm away try to improve the rest of the logic for now. \$\endgroup\$
    – Tom Siwik
    Feb 14, 2018 at 10:19
  • \$\begingroup\$ Added example, just reminding you about it. \$\endgroup\$
    – Tom Siwik
    Feb 14, 2018 at 12:54
  • \$\begingroup\$ This is very useful and I appreciate it. I didn't know about 'let' and hadn't considered using a 'const' variables. I will look into adapting this for an unknown number of arguments as that could be useful in the future. \$\endgroup\$
    – Iceape
    Feb 15, 2018 at 10:58
  • 1
    \$\begingroup\$ Yes, thanks again. The only thing I wasn't able to adapt from your example was using "$(this)" in the param section of the rules, so I had to leave the hard-coded IDs for now. I guess it's how validation works internally, but $(this) seemed to pass the other element, which messed with the validation logic. I could probably improve the this logic is handled to get $(this) to work as needed \$\endgroup\$
    – Iceape
    Feb 16, 2018 at 9:45
2
\$\begingroup\$

First off, two questions:

1) Is it correct that you are handling an blank field the same as a field containing zero? It seems to contradict the requirements.

2) There are no requirements defined if both fields are filled, not even that they need to be numbers. Is that correct?


Repeated code

You have a lot of repeated code, especially:

var tempbar = $.trim($('#bar').val());
var bar = tempbar.length > 0 ? parseInt(tempbar, 10) : 0;

This belongs in a function:

function parseIntegerValue(selector) {
  var value = $.trim($(selector).val());
  return value.length > 0 ? parseInt(value, 10) : 0;
}

Also the second line can be simplified by using the unitary + operator which converts a string to a number and returns 0 for an empty string:

function parseIntegerValue(selector) {
  var value = $.trim($(selector).val());
  return +value;
}

Ignoring invalid numbers

You are ignoring the case of invalid numbers. parseInt returns NaN if the string doesn't contain valid number. And since NaN === 0 returns false, a field wont be compared to its minimum number, if the other filed contains an invalid number.


Depends function

In the depends function you are unnecessarily checking the current field. If the other field is not empty, then it doesn't matter what the current field contains.

So for foo it can be simplified to

 depends: function(element) {
      var tempbar = $.trim($('#bar').val());
      var bar = tempbar.length > 0 ? parseInt(tempbar, 10) : 0;
      return bar === 0;
    }
\$\endgroup\$
2
  • \$\begingroup\$ 1) yes, it's correct that a blank field should be handled the same as 0. 2) both fields can be filled, each field still needs to be greater than the minimum required amount and both inputs must only accept positive numbers. I will clarify this in my question. Thanks for pointing it out! \$\endgroup\$
    – Iceape
    Feb 14, 2018 at 10:33
  • \$\begingroup\$ Thank you for the feedback. Moving the code into a function like parseIntegerVaue is exactly what I was hoping to learn how to do. It is very useful. I will look into checking for invalid numbers. \$\endgroup\$
    – Iceape
    Feb 15, 2018 at 10:29

Your Answer

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

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