3
\$\begingroup\$

I would like to create a simple interface to allow a user to make a few choices via select boxes and drive a resulting equation.

Here's a screenshot:

Screenshot

This is a gross simplification and I am looking for guidance/suggestions throughout my existing code.

Some areas of concern:

  • Should I be looping through the inputs?
  • When/how should I be calling the update() function?
  • Where is the proper place to put my scripts -- that is head vs. body vs. external .js file?
  • Is using onchange a good approach? Is there a better approach?
  • Should I be assigning the values, e.g. A = 1, B = 2, and C = 3 in the HTML option attributes or is this better handled in the script?

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Using Select Boxes to Drive Equation Output</title>
      <!-- Latest compiled and minified CSS -->
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <!-- Latest compiled and minified JavaScript -->
    
      <script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"></script>
    </head>
    <body>
      <div class="container">
        <div class="row">
          <div class="col-md-12">
            <h1>Awesome Model to Predict Cool Stuff</h1>
            <p>This is a test. Make some selections below.</p>
          </div>
        </div>
        <hr>
        <div class="row">
          <div class="col-md-3">
            <label for="sel1">Input #1</label>
            <select class="form-control" id="sel1" onchange="update()">
              <option>1</option>
              <option>2</option>
              <option>3</option>
              <option>4</option>
            </select>
          </div>
          <div class="col-md-3">
            <label for="sel2">Input #2</label>
            <select class="form-control" id="sel2" onchange="update()">
              <option>4</option>
              <option>5</option>
              <option>6</option>
              <option>7</option>
            </select>
          </div>
          <div class="col-md-3">
          <label for="sel3">Input #3</label>
          <select class="form-control" id="sel3" onchange="update()">
            <option value=1>A</option>
            <option value=2>B</option>
            <option value=3>C</option>
          </select>
          </div>
          <div class="col-md-3">
            <label for="sel2">Input #4</label>
            <select class="form-control" id="sel4" onchange="update()">
              <option>5</option>
              <option>1</option>
              <option>2</option>
              <option>3</option>
            </select>
          </div>
        </div> <!-- End select input rows -->
    
        <div class="row">
          <div class="col-md-12">
            <br>
            <p class="text-center"><em>Note the underlying super complicated modeling equation is ` = 2 * Input_1 + 3 * Input_2 + 5 * Input_3 + 1.2 * Input_4` where `A = 1, B = 2,` and `C = 3`</em></p>
          </div>
        </div>
        <hr>
        <div class="row">
          <div class="col-md-6 col-md-offset-3">
            <h4 class="text-center">After putting this through the model, your results are...</h4>
            <div id="result" class="alert alert-info text-center"></div>
          </div>
        </div>
      </div>
    
      <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
      <!-- Include all compiled plugins (below), or include individual files as needed -->
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
      <script>
        var input_1 = $('#sel1');
        var input_2 = $('#sel2');
        var input_3 = $('#sel3');
        var input_4 = $('#sel4');
        update();
    
        function update() {
          var result = 2 * input_1.val() + 3 * input_2.val() + 5 * input_3.val() + 1.2 * input_4.val();
          $('#result').text(result + " widgets");
        }
    
      </script>
    </body>
    </html>

\$\endgroup\$

2 Answers 2

3
+50
\$\begingroup\$

Added furtherly.

Inspired by the idea you might have to do this work for both heavier and multiple cases, I created a solution that goes widely beyond what your precise question.

It's a function whose argument is a description of the equation factors, in a very compact syntax derived from the one I suggested at the end of this post. Once invoked, not only it binds the needed computation when input changes, but also:

  • creates the HTML inputs for the factors
  • builds the equation expression

I don't even know if this may match your needs, but it was fun to do... :)

Here it is, with a few number of different equations as a demonstrator:

function equation(factors) {
  // prepare factors for computing
  var matches,
      sign,
      values,
      valTypes = {'[': ']', '{': '}'};
  factors = factors.map(function(factor) {
    matches = factor.match(/^([+-])?([\d.]+)([*\/^])([[{][^\]}]+[\]}])$/);
    if (!!matches) {
      values = matches[4];
      if (values.substr(-1) == valTypes[values[0]]) {
        sign = matches[1] ? matches[1] : '+';
        return {
          coef: sign + matches[2], calc: matches[3], values: JSON.parse(values)
        };
      }
    }
    alert('Syntax error in factor:\n' + factor);
  });
  
  // build HTML variable part
  var $factors = $('#factors'),
      $select,
      $input,
      equation,
      coef,
      where = [];
  $factors.empty();
  equation = factors.reduce(function(result, factor, index) {
    // BTW create <select>
    $input = $('\
<div class="col-md-3">\
  <label for="sel' + (index + 1) + '">Input #' + (index + 1) + '</label>\
  <select class="form-control" id="sel' + (index + 1) + '">\
  </select>\
</div>\
      ').appendTo($factors);
    $select = $('select', $input);
    values = factor.values;
    for (var key in values) {
      // create <option>
      $select.append('\
<option value="' + values[key] + '">\
  ' + ($.isArray(values) ? values[key] : key) + '\
</option>\
      ');
      // populate "where" if needed
      if (!$.isArray(values)) {
        where.push('`' + key + '=' + values[key] + '`');
      }
    }
    // build equation expression
    coef = factor.coef;
    sign = (index > 0 || coef[0] == '-') ? coef[0] : '';
    return result +
      sign + ' ' + coef.substr(1) + ' ' + factor.calc + 'Input_' + (index + 1) + ' ';
  }, '');
  if (where.length > 1) {
    where[where.length - 1] = 'and ' + where[where.length - 1];
  }
  $('#equation').text(
    '` =' + equation + '`' + (where.length ? (' where ' + where.join(', ')) : '')
  );

  // bind evaluation when some input changes
  $('.form-control').change(function() {
    $('#result').text(factors.reduce(function(result, factor, index) {
      var value = $('#sel' + (index + 1)).val();
      switch (factor.calc) {
        case '*': return result + factor.coef * value;
        case '/': return result + factor.coef / value;
        case '^': return result + Math.pow(factor.coef, value);
      }
    }, 0) + ' widgets');
  });

  // force 1st evaluation
  $select.change();
}

// build equations set
var equations = [
  [
  '2*[1, 2, 3]',
  '3*[7, 8, 9]'
  ],
  [
  '2*[1, 2, 3, 4]',
  '3*[4, 5, 6, 7]',
  '5*{"A": 1, "B": 2, "C": 3}', // WARNING: syntax must be JSON-compliant
  '1.2*[5, 1, 2, 3]'
  ],
  [
  '2*[1, 2, 3, 4]',
  '-3*[4, 5, 6, 7]',
  '5/{"A": 1, "B": 2, "C": 3}', // WARNING: syntax must be JSON-compliant
  '1.2^[5, 1, 2, 3]'
  ]
];

// build equations set
var $set = $('#equations-set');
for (let i in equations) {
  let equation  = JSON.stringify(equations[i]);
  equation = equation.substr(1, equation.length - 2)
    .replace(/","/g, '"\n"').replace(/(^"|"$)/gm, '').replace(/\\"/g, '&quot;');
  $set.append('\
<option value="' + i + '" title="' + equation + '">\
  ' + equation.replace(/\n/g, ' -- ').replace(/&quot;/g, '"').substr(0,50) + '\
</option>\
  ');
}

// bind equation choice and use first one by default
$set.change(function() {
  equation(equations[this.value]);
}).change();
  <head>
    <meta charset="UTF-8">
    <title>Using Select Boxes to Drive Equation Output</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    <script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"></script>
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <h1>Awesome Model to Predict Cool Stuff</h1>
          <p>Choose a source: <select id="equations-set"></select>
            <br /><mark id="source"></mark>
          </p>
          <p>This is a test. Make some selections below.</p>
        </div>
      </div>
      <hr>
      <div class="row" id="factors">
        <!-- here come the equation factors -->
      </div>
      <div class="row">
        <div class="col-md-12">
          <br>
          <p class="text-center"><em>Note the underlying super complicated modeling equation is <span id="equation"></span></em></p>
        </div>
      </div>
      <hr>
      <div class="row">
        <div class="col-md-6 col-md-offset-3">
          <h4 class="text-center">After putting this through the model, your results are...</h4>
          <div id="result" class="alert alert-info text-center"></div>
        </div>
      </div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>

Note. Since I'm not used to MathJax, I didn't try to fix this bug: after using a first equation, when switching to another one, the expression mathematical style is not refreshed.


Initial answer.

To be honest I must confess I don't see why you look for possible improvements for something which looks so easy and light.
Maybe because, as you said that "This is a gross simplification", you plan to apply this to a huge number of factors?

Anyway here is what I think about each of your questions.

Should I be looping through the inputs?

I can't figure out which way you imagine this would be possible, at least in the current form of expressing the equation (but look also at my suggestion at the end of this post).

In the other hand, if you're concerned by performance aspect, you might choose to directly get values rather than using jQuery.
So instead of:

var input_1 = $('#sel1');
...
var result = 2 * input_1.val() + ...

you might write:

var result = 2 * sel1.value + ...

When/how should I be calling the update() function?

Clearly here, in order to conform to current best practices, you should not use HTML onchange attributes to do it.
Instead of:

<select class="form-control" id="sel1" onchange="update()">

you should write this in HTML part:

<select class="form-control" id="sel1">

then in the <script> part:

$('.form-control').change(function() {
  var result = ...
  $('#result').text(result + " widgets");
});

Where is the proper place to put my scripts -- that is head vs. body vs. external .js file?

The way you used is tending to be the preferred one, and I agree.
Locating scripts at the end of the <body> (obviously assumed you place the exernal libraries first) has the advantage of avoiding to use body.onload or $(document).ready().

Last on this point: yes, your own script could be contained in an external .js, but in the current case it's so light that it's not worth it, IMO.

Is using onchange a good approach? Is there a better approach?

I remembered there was an issue with Firefox regarding onchange event not firing before loosing focus if keyboard was used (see this bug). But actually I checked it works now, so you might be concerned only if you want to be compatible with some old browsers versions.
In such a case you might choose a setInterval() approach...

Should I be assigning the values, e.g. A = 1, B = 2, and C = 3 in the HTML option attributes or is this better handled in the script?

Your current way of assigning values in the HTML seems the best. It takes advantage of what HTML <option value> attribute is made for: directly having a "real" value while the shown value on page is whatever else.

Beyond your questions, a suggestion

Inspired by your question about loops, and again because you presented your current example as a "gross simplification", here is a suggestion that might be of interest if you have to manage equations with a wide number of factors.

First based on the simple case of your example, where factors are only added together and each factor looks like coef * some_input.value, here is how you might proceed:

var coefs = [2, 3, 5, 1.2];
$('.form-control').change(function() {
  $('#result').text(coefs.reduce(function(result, coef, index) {
    return result + coef * document.getElementById('sel' + (index + 1)).value; 
  }, 0) + " widgets");
});

The advantage is that you only have to populate the factors array, rather of writing the complete expression of the equation.
Note that, in the other hand, this will have a performance impact!

Now imagine we have a more complex equation, where factors are not always added together, and/or coefs not always act as multiplicator. E. g.:

2 * Input1 - 3 * Input2 + 5 / Input3 + 1.2 ^ Input4

Then we may adapt the process like this:
(edit: this version has been simplified since first post)

var factors = [
  {coef: 2, calc: '*'},
  {coef: -3, calc: '*'},
  {coef: 5, calc: '/'},
  {coef: 1.2, calc: '^'}
];
$('.form-control').change(function() {
  $('#result').text(factors.reduce(function(result, factor, index) {
    var value = document.getElementById('sel' + (index + 1)).value;
    switch (factor.calc) {
      case '*': return result + factor.coef * value;
      case '/': return result + factor.coef / value;
      case '^': return result + Math.pow(factor.coef, value);
    }
  }, 0) + " widgets");
});

If needed, we may also consider more improvements, like adding an ord property to define how coef affects value.
So for instance applied to the last factor above:

  • {coef: 1.2, calc: '^', ord: '>'} -> Math.pow(factor.coef, value)
  • {coef: 1.2, calc: '^', ord: '<'} -> Math.powe(value, factor.coef)

And so on...
But obviously this may be worth only if its own complexity is counterbalanced by the one of very huge and complex cases.
Otherwise it's only for fun :)

\$\endgroup\$
1
\$\begingroup\$

I suggest an approach like the one in this jsfiddle:

https://jsfiddle.net/1z8sLt50/

To answer your questions:

Should I be looping through the inputs?

Four inputs is not that many, so, doesn't really matter. But, if it grows or if you might have to change it often, probably better to loop through the inputs as I have in the jsfiddle.

When/how should I be calling the update() function?

The update() function should be called to initialize the values and it can be used as the event listener for the select inputs.

Where is the proper place to put my scripts -- that is head vs. body vs. external .js file?

The JS files should go at the end of the <body>. Usually, you want all JS in external files. In practice, I find the only exception to be when the server is injecting data, for instance, if data about the current user needs to be available to JS it would go directly in the page and not in an external file.

Is using onchange a good approach? Is there a better approach?

Generally, attaching event listeners in markup should be avoided. A couple reasons off the top of my head:

  • The listener needs to be in the global scope (this is extremely bad for any significantly sized project)
  • There can be only one listener attached to that element (not usually a big deal)
  • Fewer options for attaching the listener

Main thing is the first issue, the listener has to be in the global scope.

I recommend either element.addEventListener('input', ...) or the jQuery .on('input', ...) to listen for the input event on the select elements.

Should I be assigning the values, e.g. A = 1, B = 2, and C = 3 in the HTML option attributes or is this better handled in the script?

Yes, as @cFreed mentioned, this is what the value attribute is for.


Some notes about the implementation in the jsfiddle:

The typical select looks like this:

<div class="col-md-3">
  <label for="sel1">Input #1</label>
  <select id="sel1" class="form-control js-input" data-scale="2">
    <option>1</option>
    <option>2</option>
    <option>3</option>
    <option>4</option>
  </select>
</div>

And the JS code is as follows:

var $result = $('#result'),
    $inputs = $('.js-input');

function update() {
    var total = 0,
        i = 0,
        elm;
    for (; i < $inputs.length; i++) {
        elm = $inputs[i];
        total += +elm.value * +elm.dataset.scale;
    }
    $result.text(total + ' widgets');
}

$inputs.on('input', update);

update();
  • The select inputs have the CSS class js-input added to them, this is used as the hook to work with them in JavaScript
    • When using CSS class names as hooks for accessing elements in JS, I like to prepend the class names with js- so it is clear the class name is being used in JS. Also, I never style these CSS classes.
  • The multiplicative factor, or whatever it would be called, associated with each select is embedded into the select's markup via the data-factor="N" data attribute. This means it is closer to the HTML elements that it is associated with. This also makes it easier to add new select inputs as it doesn't require changes to the JavaScript.
  • The select inputs are not worked with individually, instead they are worked with as a group. This has the benefit of allowing you to add new select inputs to the markup without having to make changes to the JavaScript

In total += +elm.value * +elm.dataset.scale;, the leading + unary operator (+elm.value) is used to convert the string values to numbers. Just a side note.

\$\endgroup\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.