Form calculation JavaScript component

This is created for project cost calculation in 2008.

Note that it does not use jQuery, but using $ symbol. Be careful to use it with jQuery on the same web page.

JavaScript component: he.calc.js

//TODO: validation logic
/*
This component is created specifically for Excel like calculation.
Author: Albert He
Created: 2008-12-01

usage:
He.enableTrace = true;  // default = false, if true read on developer console

default dataType: number
default precision: 2

//accept external event handler
onSelfChange        //value changed by self, arg = the cell object
onchange            //value changed by any reason, arg = the cell object

dataType:
'string'
'number'

precision: 3
validation: 100
*/


var doc = document;
var $ = function(id) { return document.getElementById(id); };

var removeLeadingZero = function(numberString) {
    return (numberString.indexOf('0') == 0) ? numberString.replace(/^[0]*/, '') : numberString;
}

var v = function(id) {
    var value = '';
    var elm = $(id);
    if (!elm) return 0;

    if (elm.cellInstance)
        value = elm.cellInstance.value;
    else {
        He.log += elm.id + ' has no instance (has not been defined yet).\n';       //this indicates using undefined cell.
        //alert(elm.id + ' value: ' + elm.value);
    }

    if (value == '') value = 0;
    return eval(value);
};

function roundTo(base, precision) {
    var n = Math.pow(10, precision);
    return Math.round(base * n) / n;
}
function numberPrecision(base, precision) {
    if (precision == 999) return base-0;
    else if (precision <= 0) return Math.round(base);

    var s = roundTo(base, precision).toString();
    var decimalIndex = s.indexOf(".");
    if (precision > 0 && decimalIndex < 0) {
        decimalIndex = s.length; s += '.';
    }
    while (decimalIndex + precision + 1 > s.length) {
        s += '0';
    }
    return s;
}

var numberSeparator = ',';
var decimalPoint = '.';

function numberFormat(number, precision) {
    number = number.toString().replace(/,/g, '');
    var a = number.split(decimalPoint);
    var x = a[0]; // decimal
    var y = a[1]; // fraction
    var z = '';

    for (i = x.length - 1; i >= 0; i--) z += x.charAt(i);
    z = z.replace(/(\d{3})/g, '$1' + numberSeparator);
    if (z.slice(-1) == numberSeparator) z = z.slice(0, -1);
    x = '';
    for (i = z.length - 1; i >= 0; i--) x += z.charAt(i);
    if (x.length == 0) x = '0';

    // add the fraction back in, if it was there
    if (precision > 0) {
        if (typeof y == 'undefined' || y == '') y = '0';
        y = numberPrecision('.' + y, precision);
        x += (y + '').slice(1);
    }

    return x;
}


function moneyFormat(number) {
    return '$' + numberFormat(number, 2);
}
//created 2008-12-08 for PWG Proejct module
Array.prototype.removeItem = function(item) {
    for (var i = 0; i < this.length; i++) {
        if (this[i] == item) {
            var rest = this.slice(i + 1);
            this.length = i;
            return this.push.apply(this, rest);
        }
    }
}
///add item to array only if the item is not exist.
Array.prototype.addItem = function(item) {
    for (var i = 0; i < this.length; i++) {
        if (this[i] == item) return;
    }
    this.push(item);
}
window.He = window.He || {};
He.error = [];
He.errorBgcolor = '#ffaaaa';
He.log = '';
He.enableTrace = false;

He.getError = function() {
    if (He.error.length == 0) return;

    var err = '';
    for (var i = 0; i < He.error.length; i++) {
 err += (He.error[i].cellId + ' ');
 }
 alert(He.error.length + 'errors:\n ' + err);
 };
 //get instance: $('id').cellInstance;
 ///Cell can be input (TextBox), select (DropdownList).
 ///dependency lists all the cell component object, not DOM element, in an array.
 ///formula contains cell DOM element id.
 He.Cell = (function() {
    function ctor(init) {
       this.type = 'input'; //default type is 'input', possible type would be 'select'.
       this.eventListeners = [];
       this.actionListeners = [];
       if (init) {
          if (init.cell) {
             this.cellId = init.cell;
             this.element = $(this.cellId);
             //document.write(init.cell);
             if (!this.element) {
                alert('Element with id of ' + this.cellId + ' is required.');
                throw 'Element with id of ' + this.cellId + ' is required.';
             }
             this.type = this.element.nodeName.toLowerCase();
             //INPUT, SELECT this.element.cellInstance = this;
             //this.element.onchange = handler;
             var existingHandler = this.element.onchange ? this.element.onchange : function() { };
             this.element.onchange = function(evt) { existingHandler(evt); handler(evt);
          }
          this.onchange = init.onchange ? init.onchange : null;
          this.onSelfChange = init.onSelfChange ? init.onSelfChange : null;
          this.eventChangable = true;
          this.dataType = init.dataType ? init.dataType : 'number';
          this.precision = init.precision >= 0 ? init.precision : 2;

                if (!init.text) init.text = '';

                ///if element already has value, use the value, or assign the value from script.
                if (this.element.value && (init.useDefaultText == undefined || init.useDefaultText == true)) {
                    this.text = this.value = this.element.value.replace(/,/g, '');
                    if (this.element.type != 'select-one' && this.dataType != 'string') this.element.value = numberFormat(this.text, this.precision);
                }
                else {
                    //if (init.text)    ?????????????????
                    this.setText(init.text);
                }

                ///formula should be quoted, in order to be evaluated dynamically. Cell name in formula is the DOM element id, not class instance name.
                this.formula = init.formula;
                if (init.width) this.element.width = init.width;
                if (init.color) this.element.style.color = init.color;

                ///subscribe, register event on all dependencies
                if (init.dependency) {
                    for (var i = 0; d = init.dependency[i]; i++) {
                        if (d.onEvent) {
                            d.onEvent(this);
                        }
                    }
                }

                if (init.actionOn) {
                    for (var i = 0; obj = init.actionOn.on[i]; i++) {
                        if (obj.registerAction) {
                            for (var k = 0; act = init.actionOn.action[k]; k++) {
                                obj.registerAction(this, act);
                            }
                        }
                    }
                }

                if (init.validation) {
                    this.validation = init.validation;
                    this.validate();
                }
            }
        }
        else throw 'cell is required.';
    }

    function handler(evt) {
        var elm = document.all ? event.srcElement : evt.target; //IE:FF
        if (He.trace) He.log += (event.srcElement.cellInstance.cellId + ' event firing (event changable is set to false) ...\n');

        if (elm.cellInstance.dataType != 'number') {
            elm.cellInstance.text = elm.cellInstance.value = elm.value;
            elm.cellInstance.validate();
            elm.cellInstance.fireEvent(elm);
            if (elm.cellInstance.onSelfChange) elm.cellInstance.onSelfChange(this);     //event delegate
            return;
        }

        //the following being executed when dataType=='number'
        if (!elm.cellInstance.validate()) return;   //if not passed validation, reset text to original after warning message

        if ((val = eval(elm.value)) == elm.cellInstance.value) {    //prevent trigger events if no actual value change
            elm.cellInstance.setText(val);
            return;
        }

        elm.cellInstance.setText(val);
        elm.cellInstance.fireEvent(elm);
        if (elm.cellInstance.onSelfChange) elm.cellInstance.onSelfChange(this);     //event delegate
        //if (elm.cellInstance.onchange) elm.cellInstance.onchange(elm);    //event delegate
        elm.cellInstance.setEventChangable(false);
        if (He.trace) $('He_Calc_Log').innerText = He.log;
    }

    ctor.prototype = {
        setText: function(text) {
            if (this.dataType == 'number' && !text) text = 0;
            this.element.value = this.text = this.value = typeof text == 'number' ? text : this.dataType == 'number' ? text.replace(/,/g, '') : text;
            if (this.element.type != 'select-one' && this.dataType == 'number') this.element.value = numberFormat(this.value, this.precision);
        },
        calculate: function() {
            if (!this.eventChangable) return;
            //He.log+=(this.cellId+': '+this.formula+'='+eval(this.formula)+'\n');
            this.validate();
            this.setText(eval(this.formula));
        },
        //TODO: use valudate object {regex,max,min,equal,...}
        validate: function() {
            if (this.dataType == 'number') {
                var result = He.Cell.Validator.validateNumber(this, this.precision);
                if (!result) return result;
            }

            if (this.validation)
                return He.Cell.Validator.processResult(this, this.text == this.validation, '');

            return true;
        },
        onEvent: function(handler) {
            this.eventListeners.push(handler);
            if (He.trace) He.log += handler.cellId + ' registered on ' + this.cellId + '\n';
        },

        fireEvent: function() {
            if (this.onchange) this.onchange(this);     //event delegate

            if (He.trace) He.log += '(' + this.cellId + ' event list length = ' + this.eventListeners.length + ')\n';
            var str = '';
            for (var i = 0; i < this.eventListeners.length; i++) { this.eventListeners[i].calculate(); this.eventListeners[i].fireEvent(); //fire event by component, element does not fire event when change make on background. if (He.trace) He.log += (this.eventListeners[i].cellId + ' event fired on ' + this.cellId + '\n'); } }, /// when eventChangable = flase, the value won't be changed when the cell, that registered by this, call back. setEventChangable: function() { if (arguments.length == 0) this.eventChangable = true; else { this.eventChangable = arguments[0]; //=false this.element.style.color = 'blue'; } }, registerAction: function(obj, action) { this.actionListeners.push(obj, action); //action='hide','readonly',... // if (He.trace) He.log += handler.cellId + ' registered on ' + this.cellId + '\n'; }, fireAction: function() { for (var i = 0; obj = actionListeners[i]; i++) { switch (actionListeners[i + 1]) { //action type case 'hide': //obj.hide(); //obj should have a function hide() = function(){this.element.style.display='none';} obj.element.style.display = 'none'; break; case 'readonly': obj.element.readonly = 'readonly'; break; } } } }; return ctor; })(); He.Label = (function() { function ctor(init) { this.type = 'label'; if (init) { this.eventListeners = []; if (init.cell) { this.cellId = init.cell; this.element = $(this.cellId); if (!this.element) { alert('Element with id of ' + this.cellId + ' is required.'); throw 'Element with id of ' + this.cellId + ' is required.'; } this.element.cellInstance = this; //this.element.onchange = handler; } this.eventChangable = true; this.dataType = init.dataType ? init.dataType : 'number'; this.precision = init.precision >= 0 ? init.precision : 2;
            this.onchange = init.onchange ? init.onchange : null;

            if (!init.text) init.text = '';

            ///if element already has value, use the value, or assign the value from script.
            if ((this.element.innerText && this.element.innerText.replace(/^[\s\xA0]+$/g, '') != '')
             && (init.useDefaultText == undefined || init.useDefaultText == true)) {
                this.text = this.value = this.element.innerText.replace(/,/g, '');
                if (this.dataType != 'string') this.element.innerText = numberFormat(this.text, this.precision);
            }
            else {
                //if (init.text) {
                this.text = this.value = init.text;
                this.setText(init.text);
                //}
            }

            this.formula = init.formula;
            if (init.color) this.element.style.color = init.color;

            ///subscribe, register event on all dependencies
            if (init.dependency) {
                for (var i = 0; d = init.dependency[i]; i++) {
                    if (d.onEvent) d.onEvent(this);
                }
            }

            if (init.validation) {
                this.validation = init.validation;
                this.validate();
            }
        }
        else throw 'cell is required.';
    }

    ctor.prototype = {
        setText: function(text) {
            if (!text) text = 0;
            this.element.innerText = this.text = this.value = typeof text == 'number' ? text : this.dataType == 'number' ? text.replace(/,/g, '') : text;
            if (this.dataType == 'number') this.element.innerText = numberFormat(this.value, this.precision);
        },
        calculate: function() {
            this.setText(eval(this.formula));
            this.validate();
        },
        validate: function() {
            if (this.validation) {
                //only validate equal
                if (this.text == this.validation) {
                    this.element.parentNode.style.backgroundColor = ''; He.error.removeItem(this);
                }
                else { this.element.parentNode.style.backgroundColor = He.errorBgcolor; He.error.addItem(this); }
                //He.getError();
            }
        },
        onEvent: function(handler) {
            this.eventListeners.push(handler);
            if (He.trace) He.log += handler.cellId + ' registered on ' + this.cellId + '\n';
            return 'registered';
        },

        fireEvent: function() {
            if (this.onchange) this.onchange(this);     //event delegate

            if (He.trace) He.log += '(' + this.cellId + ' event list length = ' + this.eventListeners.length + ')\n';
            for (var i = 0; i < this.eventListeners.length; i++) { this.eventListeners[i].calculate(); this.eventListeners[i].fireEvent(); //fire event by component, element does not fire event when change make on background. if (He.trace) He.log += (this.eventListeners[i].cellId + ' event fired on ' + this.cellId + '\n'); } }, setEventChangable: function() { if (arguments.length == 0) this.eventChangable = true; else this.eventChangable = arguments[0]; } }; return ctor; })(); // var n = doc.createElement('input'); // n.id = 'ww'; // n.type = 'button'; // n.value = text; // this.element.appendChild(n); //if (init.type=='text') this.element.type = 'text'; //if (init.type == 'button') this.element.type = 'hidden'; //not support He.Button = (function() { function ctor(init) { this.eventListeners = []; //register event action, to be eval() if (init) { if (init.button) { this.button = init.button; this.element = $(init.button); if (!this.element) { alert('Element with id of ' + this.button + ' is required.'); throw 'Element with id of ' + this.button + ' is required.'; } this.element.cellInstance = this; if (this.element.addEventListener) this.element.addEventListener('onclick', function() { if (He.trace) He.log += (event.srcElement.cellInstance.button + ' event firing ...\n'); event.target.cellInstance.fireEvent(); if (He.trace) $('He_Calc_Log').innerText = He.log; }, false) else //ie:srcElement this.element.attachEvent('onclick', function() { if (He.trace) He.log += (event.srcElement.cellInstance.button + ' event firing ...\n'); event.srcElement.cellInstance.fireEvent(); if (He.trace) $('He_Calc_Log').innerText = He.log; }, false); } else { alert('Button name is missing.'); throw 'button name is required.'; } } } ctor.prototype = { onEvent: function(eventHandler) { //eventHandler is an action this.eventListeners.push(eventHandler); }, fireEvent: function() { for (var i = 0; listener = this.eventListeners[i]; i++) { eval(listener); } } }; return ctor; })(); //Validator, added on 2009-07-25 He.Cell.Validator = function() { }; He.Cell.Validator.processResult = function(cell, passed, message) { if (passed) { cell.element.style.backgroundColor = ''; He.error.removeItem(cell); } else { if (message.length > 0) {
            cell.element.style.backgroundColor = He.errorBgcolor;
            alert(message);
            
            //auto-recover, after that do not trigger events
            cell.setText(cell.text);
            cell.element.style.backgroundColor = '';
        }
        else {
            cell.element.style.backgroundColor = He.errorBgcolor;
            He.error.addItem(cell);
        }
    }
    return passed;
}

He.Cell.Validator.validateNumber = function(numberCell, precision) {
    var num = removeLeadingZero(numberCell.element.value).replace(/,/g, '').replace(/[.]$/, '');
    if (num == '') num = 0;
    var result = He.Cell.Validator.processResult(numberCell, /^(\d+\.\d+|\d*\.\d+|\d+[.]?)$/g.test(num), 'Please enter number.');
    if (result) numberCell.element.value = num;
    return result;
};

Usage:

HTML file:

<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.css">
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<sc ript src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="./assets/js/he.calc.js"></script>
<style>
body {
}
.tire-cost {
	margin:auto;
	max-width:1200px;
}
.tire-cost h4 {
	border-bottom:solid 2px #555;
	font-size:14px;
	font-weight:bold;
}
.tire-cost .instruction {
	width:100%;
	margin:10px auto 10px auto;
}

.tire-cost .center {
	text-align:center;
}
.tire-cost table {
	font-size:11px;
	font-family:Verdana,Arial;
	border-collapse: collapse;
	border: solid 2px #551;
	width:100%;
}
.tire-cost input {
	font-size:11px;
	width:60px;
	text-align:center;
}
	
.tire-cost img{
	padding-bottom:8px;
}
.tire-cost th,.tire-cost td {
	padding:2px 10px 2px 10px;
	border: 1px solid #999;
	text-align:center !important;
}

.tire-monthly-km,
.tire-base,
.tire-opt1,
.tire-opt2 {
	max-width:400px;
}
.instruction{
	background:#eee;
	padding:10px;
	height:100px;
}
.box {
	//max-width:400px;
	margin:10px 0 10px 0;
	padding:5px;
}

.tire-results{
	//width:720px;
	//margin:10px auto 10px auto;
}

.result-label {
	width:38%;
}
.result-option-costs {
	width:38%;
}
.result-option-compare {
	width:24%;
}
.tire-cost-label {
	width:300px;
}
.tire-cost-input {
	text-align:center;
}
.tire-cost-input input {
	width:60px;
	text-align:center;
}
.tire-cost .value{
	text-align:right;
}
.empty {
	display:none;
}
@media (min-width: 768px) {
	.empty {
		display:block;
	}
}
.equal-height{
	height:347px;
}
</style>
</head>
<body>
<div class="tire-cost row">

<div class="tire-monthly-km box col-md-4">
	<table>
		<thead>
			<tr>
				<th colspan="2">
					<h4>Monthly Kilometres Driven</h4>
				</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td class="tire-cost-label">Average KM driven - Non Winter months</td>
				<td class="tire-cost-input">
					<input type="text" id="C3" name="average-non-winter" value="2">
				</td>
			</tr>
			<tr>
				<td class="tire-cost-label">Average KM driven - Winter months</td>
				<td class="tire-cost-input">
					<input type="text" id="C4" name="average-winter" value="3">
				</td>
			</tr>
			<tr>
				<td class="tire-cost-label">Annual Kilometers</td>
				<td class="tire-cost-input" style="text-align:center;">
					<input type="text" id="C5" name="annual-km" value="4">
				</td>
			</tr>
		</tbody>
	</table>
</div>

<div class="tire-monthly-km box col-md-4">
<table>
	<tbody>
		<tr>
			<td rowspan="2" class="result-label"></td>
			<td colspan="2" class="result-option-costs center">
				<div>Tire Options with Annual and Monthly Costs</div>
			</td>
		</tr>
		<tr>
			<td class="center">1 set (base)</td>
			<td class="center">2 sets without extra wheels (option1)</td>
		</tr>
		<tr>
			<td class="tire-cost-label">Average Annual Cost</td>
			<td class="value"><span id="L12">$291.00</span></td>
			<td class="value"><span id="M12">$387.84</span></td>
		</tr>
	</tbody>
</table>
</div>
</div></body>
<script src="./assets/js/tire-cost.js"></script>
</html>

JavaScript file:

// write formula and dependency in this file
C3 = txtAverageNonWinter = new He.Cell({ cell: 'C3' });
C4 = txtAverageWinter = new He.Cell({ cell: 'C4' });
C5 = txtAnnualKm = new He.Cell({ cell: 'C5' });
L12 = new He.Label({ cell: 'L12', text: '', formula: "v('C3')+v('C4')+v('C5')", dependency: [C3,C4,C5], dataType: 'string' });
M12 = new He.Label({ cell: 'M12', text: '', formula: "v('C3')*v('C4')*v('C5')", dependency: [C3,C4,C5], dataType: 'string' });

719 total views, 2 views today

Author: Albert

Leave a Reply