3
\$\begingroup\$

Since the repetition mode didn't make to HTML5, I was wondering how to succinctly implement it in Javascript. I've already spent a few hours on this jsbin. Be great to get a review.

  <button onclick="addRow();">Add row</button>

  <label>Pence per kiloWatt hour
    <input id="price" required type="number" step="0.1" value="12.2" />
  </label>

  <label><button onclick="grandTotal()">TOTAL COST</button>
    <output id=grandtotal>
  </label>

  <form id=template onsubmit="return false" oninput="doCalc(this)">
    <input name="quantity" min=0 type="number" value="1">
    <input type="text" value="Washing machine">
    <input name="wattage" min=0 type="number" value="1000">
    <input name="minutes" min=0 type="number" value="60">
    <label>
      <output name="total"></output>
      GBP</label>
  </form>

  <div id=repeatafterme></div>

Javascript:

function doCalc(el) {
  price = document.getElementById("price").value;
  console.log("price", price);
  el.total.value = (price * el.quantity.valueAsNumber * el.wattage.valueAsNumber / 1000 * (el.minutes.valueAsNumber / 60) / 100).toFixed(2);
}

function addRow() {

  var itm = document.getElementById("template");
  var cln = itm.cloneNode(true);
  document.getElementById("repeatafterme").appendChild(cln);

}

function grandTotal() {
  t = document.getElementsByName("total");
  gt = 0.0;
  for (i = 0; i < t.length; i++) {
    console.log(t[i].value);
    gt = parseFloat(gt) + parseFloat(t[i].value);
  }
  console.log("Grand total:", gt);
  document.getElementById("grandtotal").value = gt.toFixed(2);

}

Outstanding issues I don't know how to solve:

  • I use a form since that's easier to get the values in vanilla Javascript IIUC. I had issues with a table row.
  • Currently only calculates on input change. Would be nice if I could trigger this somehow on startup
  • Furthermore when any form changes, would be nice if the grand total could update. I am not sure what event I should latch onto.
  • I cloneNode by id, but that also clones the id. Not sure what the best practice is to avoid id duplication
\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$
function doCalc(el) {
  price = document.getElementById("price").value;
  console.log("price", price);
  el.total.value = (price * el.quantity.valueAsNumber * el.wattage.valueAsNumber / 1000 * (el.minutes.valueAsNumber / 60) / 100).toFixed(2);
}
  • Don't forget to use var when declaring variables. Not doing so will cause JS to declare it in the global namespace, which anyone can clobber. There may even be a price global existing already, and you just replaced its value.

  • Use the browser debugger and plant breakpoints in the source code. It's better than peppering your code with console functions.

  • Notice that your HTML is littered with JS. Use addEventListener instead of inlining your event handlers. Keep JS just JS, and HTML just HTML.


function addRow() {

  var itm = document.getElementById("template");
  var cln = itm.cloneNode(true);
  document.getElementById("repeatafterme").appendChild(cln);

}

The problem I see with cloneNode is that it carries over some state from the cloned nodes, which can be a problem.

<script type="text/template id="template">
  <button onclick="addRow();">Add row</button>
  ...
</script>

var container = document.createElement('div');
var template = document.getElementById('template').innerHTML;

container.innerHTML = template;
container.getElementByClassName('some-input')[0].value = '';
somewhere.appendChild(container);

One solution would be to create an empty <div> using document.createElement, and populate it using innerHTML with a string form of the template. Look for the elements whose values need replacing, then append that div to the DOM.


function grandTotal() {
  t = document.getElementsByName("total");
  gt = 0.0;
  for (i = 0; i < t.length; i++) {
    console.log(t[i].value);
    gt = parseFloat(gt) + parseFloat(t[i].value);
  }
  console.log("Grand total:", gt);
  document.getElementById("grandtotal").value = gt.toFixed(2);
}
  • Same as before, don't forget var.
  • Same as before, don't use console.
  • When parseFloat receives an input that isn't starting with a number, it will return NaN. Further math with NaN will make the results NaN. You should always check the result of string-to-number conversion functions.

In the real world, nobody uses vanilla JS for this. I suggest you start looking into frameworks like Angular to do this repetitive task for you. With most frameworks, you only have to worry about operating with data and templates, and the frameworks do the heavy lifting.

// You worry only here. Angular binds the data so changes in the input
// automatically reflect in the object made as the model.
$scope.rows = [{ id: 1, name: 'washington', value: 19.99 }, ...];
$scope.grandTotal= function(){
  return $scope.rows.reduce(function(partial, value){
    return partial + value;
  }, 0);
}

// You don't have to muck around with DOM
<label>Grand Total</label><span>{{ grandTotal() }}</span>
<table ng-repeat="row in rows">
  <tr>
    <td><input type="text" model="row.id"></td>
    <td><input type="text" model="row.name"></td>
    <td><input type="text" model="row.value"></td>
  </tr>
</table>

Moving down closer to lower-level code. In PHP, you can represent a nested, repetitive structure with some trickery in the form name.

<input type="text" name="foo[]" value="1">
<input type="text" name="foo[]" value="2">
<input type="text" name="foo[]" value="3">
// Becomes ["1", "2", "3"]

<input type="text" name="foo[a]" value="1">
<input type="text" name="foo[b]" value="2">
<input type="text" name="foo[c]" value="3">
// Becomes ["a" => "1", "b" => "2", "c" => "3"]

<input type="text" name="foo[0][a]" value="1">
<input type="text" name="foo[0][b]" value="2">
<input type="text" name="foo[1][a]" value="3">
<input type="text" name="foo[1][b]" value="4">
// Becomes [["a" => "1", "b" => "2"], ["a" => "3", "b" => "4"]]

We can do the same thing in JS with a few tools. First, we can use Mustache to render the template in the same manner. To get the values in their nested form, you can use the serializeObject plugin.

// var rows = [{ id: 1, name: 'washington', value: 19.99 }, ...];
// Mustache.render(template, { rows: rows });

<form name="item-form">
  <table>
    {{# rows }}
    <tr>
      <td><input type="text" name="items[{{ id }}][id]"/></td>
      <td><input type="text" name="items[{{ id }}][name]"/></td>
      <td><input type="text" name="items[{{ id }}][value]"/></td>
    </tr>
    {{/ rows }}
  </table>
</form>

// var data = $('form[name="item-form"]').serializeObject();
// { items: [{ id: 1, name: 'washington', value: 19.99 }, ...], }
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I think I will try re-implement in ractivejs.org Angular looks way too heavy heavyweight. Otherwise I know about the PHP option and I don't think it's worth scrutinizing on console/var details that could be picked up by a linting tool. ;) Wish I know which eventlistener to bind to.. \$\endgroup\$
    – Kai Hendry
    Commented Oct 7, 2015 at 3:59
  • 1
    \$\begingroup\$ @KaiHendry Actually, I would recommend Ractive (I use it myself). But for the general public, Angular is better known which is why I use it in my examples. \$\endgroup\$
    – Joseph
    Commented Oct 7, 2015 at 5:20

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