patternjavascriptMinor
Simply implementing a repeating HTML form with a total
Viewed 0 times
totalwithrepeatingformsimplyimplementinghtml
Problem
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.
Javascript:
}
Outstanding issues I don't know how to solve:
Add row
Pence per kiloWatt hour
TOTAL COST
GBP
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
cloneNodeby id, but that also clones the id. Not sure what the best practice is to avoid id duplication
Solution
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.
Add row
...
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 `
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
Grand Total{{ grandTotal() }}
Moving down closer to lower-level code. In PHP, you can represent a nested, repetitive structure with some trickery in the form name.
// Becomes ["1", "2", "3"]
// Becomes ["a" => "1", "b" => "2", "c" => "3"]
// 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 });
{{# rows }}
{{/ rows }}
// var data = $('form[name="item-form"]').serializeObject();
// { items: [{ id: 1, name: 'washington', value: 19.99 }, ...], }Code Snippets
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);
}<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);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);
}// 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>Context
StackExchange Code Review Q#106717, answer score: 2
Revisions (0)
No revisions yet.