0

I use Knockout Js for a few months. But I stuck on a problem. I have 2 view models. One is the whole bill view model who has the subtotal of the bill and the taxes with the grand total. A part of the main view model of the bill is:

    function BillViewModel() {
         var self = this;
         self.timesheets = ko.observableArray([]);
         self.items = ko.observableArray([]);
         self.amountdummy = ko.observable();
         self.subtotal = ko.computed(function(){
             self.amountdummy();
             var total = 0;
             for(var i = 0; i < this.timesheets().length; i++)
             {
                 var totalLine = this.timesheets()[i].amount();
                 total += parseFloat((totalLine != '' && totalLine !== null && !isNaN(totalLine) ? totalLine: 0));

             for(var i = 0; i < this.items().length; i++)
             {
                 var totalLine = this.items()[i].amount();
                 total += parseFloat((totalLine != '' && totalLine !== null && !isNaN(totalLine) ? totalLine : 0));
            
             }
             return total;
         }, self);

    };

Each line of the bill is represented by 2 others view models who are:

    function BillItemViewModel(item) {
         var self = this;
         self.parent = item.parent;
         self.quantity = ko.observable(item.quantity);
         self.price = ko.observable(item.price);
         self.amount = ko.computed({
             read: function(){
                 var quantity = getNumberFromFormattedValue(self.quantity());
                 if (quantity !== null) { quantity = parseFloat(quantity); }
                 var price = getNumberFromFormattedValue(self.price());
                 if (price !== null) { price = parseFloat(price); }
                 if (!(isNaN(quantity) || isNaN(price) || quantity === null || price === null || quantity === '' || price === '')) {
                     var newValue = quantity * price;
                     return newValue;
                 }
                 // Don't change the value
             },
             write: function(value){
                 var newValue = getNumberFromFormattedValue(value);
                 if (newValue !== null) { newValue = parseFloat(newValue); }
                 var quantity = getNumberFromFormattedValue(self.quantity());
                 if (quantity !== null) { parseFloat(quantity); }
                 if (!(isNaN(newValue) || isNaN(quantity) || newValue === null || quantity === null || newValue === '' || quantity === '')) {
                     self.price( newValue / quantity );
                 }
                 self.parent.amountdummy.notifySubscribers();
             },
             owner: self
         });
         self.quantity.subscribe(function(){
            if (self.price() === '' || self.price() === null) {
                self.amount(0);
            } 
         });
         self.amount(item.amount);

    };

The 2nd view model is pretty like this one. Except that it is used to enter time and rate and calculate an amount who is added to the subtotal.

The HTML code is:

       <table class="table item" data-bind="visible: items().length > 0">
            <tbody data-bind="foreach: items">
                 <tr>
                       <td class="qty"><input type="text" data-bind="value: quantity, name: function(data, event) { return 'qty_'+ $index(); }()"></td>
                       <td class="price"><input type="text" data-bind="value: price, name: function(data, event) { return 'price_'+ $index(); }()"></td>
                       <td class="amount"><input type="text" data-bind="value: amount, name: function(data, event) { return 'amount_'+ $index(); }()"></td>
                 </tr>
           </tbody>
       </table> 
       
       <div>
           <label for="subtotal">Subtotal</label>
           <input id="subtotal" data-bind="value: subtotal" type="text" name="subtotal" readonly>
       </div>

The behaviour of the page is that when user enters quantity and price, the amount of the line is automatically calculated. But if the user can not enter a quantity and a price, he can enter directly the amount.

see JsFiddle for complete example

Everything works fine. But when the user just enter an amount on the line, the subtotal is not updated.

Edit:

I removed everything about taxes. I followed the hint given by Josh by this link . But it is doesn't work again.

See this JsFiddle

Any help will be appreciated

  • try changing the pureComputed to computed. – Nathan Fisher Mar 22 '21 at 04:04
  • also try avoiding mix of `this` and `self`, when i try the fiddle and enter only amount and nothing on qty and amount, the computed returns NaN for the inputs, so i guess its working? – john Smith Mar 24 '21 at 12:41
  • @Nathan - This is not working – Sylvain Racine Mar 25 '21 at 00:53
  • @johnSmith - This is not working too. NaN is displayed because I simplified the code: I didn't take time to manage errors – Sylvain Racine Mar 25 '21 at 00:54
  • yes, but i mean that it show NaN is the prove that the subtotal is updated / the computed is triggered? – john Smith Mar 25 '21 at 10:01
  • @john That is right that NaN is displayed in subtotal when I remove the quantity and ... still NaN when I enter 100 as amount on the same line. This is not the behaviour that I expect. Subtotal should display 100+the amount of the 2nd line, not NaN. See my 2nd JsFiddle - https://jsfiddle.net/yvLd83hr/25/ – Sylvain Racine Mar 26 '21 at 00:05

1 Answers1

0

I found how to keep calculated the subtotal. The secret: put a separated observable beside the amount of each line and update this value when amount is updated. Next, the subtotal should calculate the sum of those hidden observables, not the computed amounts.

Thanks to Josh who by his post showed me the way how to solve it.

HTML code:

<html>
     <body>
         <form id="billForm">
             <table class="table timesheet">
                 <thead>
                     <tr>
                         <th>Time</th>
                         <th>Rate</th>
                         <th>Amount</th>
                     </tr>
                 </thead>
                 <tbody data-bind="foreach: timesheets">
                     <tr>
                         <td class="time"><input type="text" data-bind="value: time, name: function(data, event) { return 'time_'+ $index(); }()"></td>
                         <td class="rate"><input type="text" data-bind="value: rate, name: function(data, event) { return 'rate_'+ $index(); }()"></td>
                         <td class="amount"><input type="text" data-bind="value: amount, name: function(data, event) { return 'amount_'+ $index(); }()"></td>
                     </tr>
                 </tbody>
                 <tfoot>
                     <button type="button" data-bind="$root.addTimesheet">Add timesheet</button>
                 </tfoot>
            </table> 
   

            <table class="table item">
                <thead>
                    <tr>
                        <th>Qty</th>
                        <th>Price</th>
                        <th>Amount</th>
                    </tr>
                </thead>
                <tbody data-bind="foreach: items">
                    <tr>
                        <td class="qty"><input type="text" data-bind="value: quantity, name: function(data, event) { return 'qty_'+ $index(); }()"></td>
                        <td class="price"><input type="text" data-bind="value: price, name: function(data, event) { return 'price_'+ $index(); }()"></td>
                        <td class="amount"><input type="text" data-bind="value: amount, name: function(data, event) { return 'amount_'+ $index(); }()"></td>
                    </tr>
                </tbody>
                <tfoot>
                    <button type="button" data-bind="$root.addItem">Add item</button>
                </tfoot>
            </table> 
   
            <div>
               <label for="subtotal">Subtotal</label>
               <input id="subtotal" data-bind="value: subtotal" type="text" name="subtotal" readonly>
            </div>
   
        </form>
    </body>
</html>

JS code:

function getNumberFromFormattedValue(value) {
    if (value != '' && value !== null) {
        return value.toString().replace(/[^0-9.]*/g,'');

    }
    return value;
}

function BillTimesheetViewModel(item) {
     var self = this;
     self.time = ko.observable(item.time);
     self.rate = ko.observable(item.rate);
     self.total = ko.observable(item.amount);
     self.amount = ko.computed({
         read: function(){
             var time = getNumberFromFormattedValue(self.time());
             if (time !== null) { time = parseFloat(time); }
             var rate = getNumberFromFormattedValue(self.rate());
             if (rate !== null) { rate = parseFloat(rate); }
             if (!(isNaN(time) || isNaN(rate) || time === null || rate === null || time === '' || rate === '')) {
                 var newValue = time * rate;
                 self.total(newValue);
                 return newValue;
             }
             // Don't change the value
         },
         write: function(value){
             var newValue = getNumberFromFormattedValue(value);
             if (newValue !== null) { newValue = parseFloat(newValue); }
             var time = getNumberFromFormattedValue(self.time());
             if (time !== null) { parseFloat(time); }
             if (!(isNaN(newValue) || isNaN(time) || newValue === null || time === null || newValue === '' || time === '')) {
                 self.rate( newValue / time );
             }
             self.total(value);
         },
         owner: self
     });
     self.time.subscribe(function(){
        if (self.time() === '' || self.time() === null || self.rate() === '' || self.rate() === null) {
            self.total('');
            self.amount('');
        } else {
                var time = self.time();
                var rate = self.rate();
            if (time !== '' && time !== null && rate !== '' && rate !== null) {
                    var total = time * rate; 
                    self.amount(total);
                self.total(total);
            }
        } 
     });
         
     self.amount(item.amount);
}



function BillItemViewModel(item) {
     var self = this;
     self.quantity = ko.observable(item.quantity);
     self.price = ko.observable(item.price);
         self.total = ko.observable(item.amount);
     self.amount = ko.computed({
         read: function(){
             var quantity = getNumberFromFormattedValue(self.quantity());
             if (quantity !== null) { quantity = parseFloat(quantity); }
             var price = getNumberFromFormattedValue(self.price());
             if (price !== null) { price = parseFloat(price); }
             if (!(isNaN(quantity) || isNaN(price) || quantity === null || price === null || quantity === '' || price === '')) {
                 var newValue = quantity * price;
                 self.total(newValue);
                 return newValue;
             }
             // Don't change the value
         },
         write: function(value){
             var newValue = getNumberFromFormattedValue(value);
             if (newValue !== null) { newValue = parseFloat(newValue); }
             var quantity = getNumberFromFormattedValue(self.quantity());
             if (quantity !== null) { parseFloat(quantity); }
             if (!(isNaN(newValue) || isNaN(quantity) || newValue === null || quantity === null || newValue === '' || quantity === '')) {
                 self.price( newValue / quantity );
             }
             self.total(value);
         },
         owner: self
     });
     self.quantity.subscribe(function(){
        if (self.quantity() === '' || self.quantity() === null || self.price() === '' || self.price() === null) {
            self.total('');
            self.amount('');
        } else {

           var quantity = self.quantity();
           var price = self.price();
           if (quantity !== '' && quantity !== null && price !== '' && price !== null) {
             var total = quantity * price; 
             self.amount(total);
             self.total(total);
           }
       }
     });

     self.amount(item.amount);
}

function BillViewModel() {
     var self = this;
     self.timesheets = ko.observableArray([]);
     self.items = ko.observableArray([]);
     self.subtotal = ko.computed(function(){
         var total = 0;
         for(var i = 0; i < this.timesheets().length; i++)
         {
             var totalLine = this.timesheets()[i].total();
             total += parseFloat((totalLine != '' && totalLine !== null && !isNaN(totalLine) ? totalLine: 0));

        
         }

                     for(var i = 0; i < this.items().length; i++)
         {
             var totalLine = this.items()[i].total();
             total += parseFloat((totalLine != '' && totalLine !== null && !isNaN(totalLine) ? totalLine : 0));
        
         }
         return total;
     }, self);

     
     self.addTimesheet = function(item) {
        var timesheet = new BillTimesheetViewModel({
            time: item.time,
            rate: item.rate,
            amount: item.amount,
        });

        self.timesheets.push(timesheet);

             };
     
     self.addItem = function(item){

        var item = new BillItemViewModel({
            quantity: item.quantity,
            price: item.price,
            amount: item.amount,
        });
        
        self.items.push(item);
           };
     
        self.addTimesheet({
        time: 2,
      rate: 50,
      amount: 100
    });     
    
    self.addItem({
        quantity: 3,
      price: 75,
      amount: 125
    });
}    

ko.applyBindings(new BillViewModel(), document.getElementById("billForm"));

See JsFiddle