Search code examples
javascripthtmlknockout.jsobservableknockout-3.0

Knockout JS writable computed value doesn't fire another computed value when writing function is used


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


Solution

  • 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