Question

Have an observable array of bank transactions which contain amounts among other things.

I'm trying to keep a running balance as a computable but I seem to get stuck in infinite loops and all sorts of badness.

Here's the simplest fiddle I could come up with - http://jsfiddle.net/Nnyxx/2/

JS:

var transactions = [{Amount: -100}, {Amount: 125}, {Amount: 10}, {Amount: 25}, {Amount: -125}, {Amount: 400}];

var ViewModel = function() {
    this.OpeningBalance = ko.observable(1000);
    this.RunningBalance = ko.observable(this.OpeningBalance());
    this.ClosingBalance = ko.observable(this.RunningBalance());

    this.UpdateRunningBalance = ko.computed({
        read: function() {
            return this.RunningBalance();
        },
        write: function(amount) {
            this.RunningBalance(this.RunningBalance() + amount);

            return amount;
        }
    }, this);

    this.Transactions = ko.observableArray(ko.mapping.fromJS(transactions)());
}

var model = new ViewModel();

ko.applyBindings(model);

HTML:

<table>
    <thead>
        <tr>
            <th width="150"></th>
            <th width="150">Money Out</th>
            <th width="150">Money In</th>
            <th>Balance</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td colspan="3"><strong>Opening Balance</strong></td>
            <th>
                <span data-bind="text: OpeningBalance"></span>
            </th>
        </tr>
        <!-- ko foreach: Transactions -->
        <tr>
            <td></td>
            <!-- ko if: Amount() < 0 -->
            <td data-bind="text: Amount"></td>
            <td></td>
            <!-- /ko -->
            <!-- ko if: Amount() > 0 -->
            <td></td>
            <td data-bind="text: Amount"></td>
            <!-- /ko -->
            <td>
                <span data-bind="text: $root.UpdateRunningBalance(Amount())"></span>
            </td>
        </tr>
        <!-- /ko -->
        <tr>
            <td colspan="3"><strong>Closing Balance</strong></td>
            <th>
                <span data-bind="text: ClosingBalance"></span>
            </th>
        </tr>
    </tbody>
</table>

It's still incomplete as I end up going around in circles on how to get the opening balance to display and recalculate.

The running balance needs to be an observable so if the opening balance or the transactions change it will recalculate.

Also the closing balance needs to be the final running balance.

Was it helpful?

Solution

I think you are missing something essential here.

There is no such thing as a running balance in Knockout. Every single data point that is shown on the screen must bind to a live value in your view model.

There is no value that "only exists while the current row is built", akin to what, say, PHP would do when building HTML during a loop. If you need 10 "running value" rows, have 10 distinct values in your view model.

Compare this:

function Transaction(amount, previousBalance) {
    this.amount = amount;
    this.balance = previousBalance + amount;
}

function Account(openingBalance, transactions) {
    var self = this;

    // properties
    self.openingBalance = openingBalance;
    self.transactions = ko.observableArray();
    self.closingBalance = ko.computed(function () {
        var transactions = self.transactions(),
            lastTransaction = transactions[transactions.length - 1];
        return lastTransaction ? lastTransaction.balance : openingBalance;
    });

    // methods
    self.addTransaction = function (amount) {
        var previousBalance  = self.closingBalance();
        self.transactions.push(new Transaction(amount, previousBalance));
    };

    // init    
    ko.utils.arrayForEach(transactions, function (t) {
        self.addTransaction(t.Amount);
    });
}

var transactions = [{Amount: -100}, {Amount: 125}, {Amount: 10}, 
                    {Amount: 25}, {Amount: -125}, {Amount: 400}];

ko.applyBindings(new Account(1000, transactions));

(see it live over here http://jsfiddle.net/Nnyxx/5/)

Note how every table row has a respective value of the current running value that actually exists in the view model.

Knockout is not a client side page processing library. It is a data binding library. It can only show data that really exists during the lifetime of the page.

Also, not every value in Knockout needs to be wrapped in an observable, only values that you expect to change during the lifetime of the page. Account.openingBalance or Tansaction.amount are properties that are not likely to change, they can be left unwrapped.

Unrelated, but try to follow the convention for JS casing: Use PascalCaseNames for constructors and constructors only, all other variables (member functions, properties, local variables) will get camelCaseNames.

One more thing - if you work with monetary values, beware of the pitfalls of IEEE 754 floating point arithmetic. All operations should be properly rounded before you use them anywhere else. If you drag along a running value without any intermediate rounding (like in the above code sample) you might end up with values that are off due to internal number representation issues.

OTHER TIPS

You can also do this by using a plain function instead of a computed observable.

See this fiddle

JS:

var transactions = [{
    Amount: -100
}, {
    Amount: 125
}, {
    Amount: 10
}, {
    Amount: 25
}, {
    Amount: -125
}, {
    Amount: 400
}];

var ViewModel = function () {
    var self = this;
    self.Transactions = ko.observableArray(ko.mapping.fromJS(transactions)());
    self.OpeningBalance = ko.observable(1000);
    self.runningBalance = self.OpeningBalance();

    self.RunningBalance = function (index) {
        return ko.computed(function () {
            var total = parseInt(self.OpeningBalance());
            for (i = 0; i <= index; i++) {
                total += parseInt(self.Transactions()[i].Amount());
            }
            return total;
        });
    };
    self.ClosingBalance = ko.computed(function () {
        var total = parseInt(self.OpeningBalance());
        ko.utils.arrayForEach(self.Transactions(), function (item) {
            total += parseInt(item.Amount());
        });
        return total
    });

}

var model = new ViewModel();

ko.applyBindings(model);

HTML

Opening balance
<input data-bind="value: OpeningBalance" />
<table>
    <thead>
        <tr>
            <th width="150"></th>
            <th width="150">Money Out</th>
            <th width="150">Money In</th>
            <th>Balance</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td colspan="3"><strong>Opening Balance</strong>

            </td>
            <th>    <span data-bind="text: OpeningBalance"></span>

            </th>
        </tr>
        <!-- ko foreach: Transactions -->
        <tr>
            <td></td>
            <!-- ko if: Amount() < 0 -->
            <td data-bind="text: Amount"></td>
            <td></td>
            <!-- /ko -->
            <!-- ko if: Amount()> 0 -->
            <td></td>
            <td data-bind="text: Amount"></td>
            <!-- /ko -->
            <td>    <span data-bind="text: $root.RunningBalance($index())"></span>

            </td>
        </tr>
        <!-- /ko -->
        <tr>
            <td colspan="3"><strong>Closing Balance</strong>

            </td>
            <th>    <span data-bind="text: ClosingBalance"></span>

            </th>
        </tr>
    </tbody>
</table>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top