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
1 Answer 1
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 aprice
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 returnNaN
. Further math withNaN
will make the resultsNaN
. 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 }, ...], }
-
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– Kai Hendry2015年10月07日 03:59:12 +00:00Commented 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– Joseph2015年10月07日 05:20:34 +00:00Commented Oct 7, 2015 at 5:20