0
votes

I am running into an issue using the knockout mapping plugin. I have an array of ShippingOptions that I data-bind to radio buttons using the checked value with the checked attribute set to another object on my view model called ShippingChoice.

I have a computed observable Total which is the sum of a my SubTotal and ShippingChoice.Price

When one of the radio options is selected it correctly sets ShippingChoice to the selected shipping option, but it causes the ShippingChoice to no longer be an observable and breaks my computed total

The console shows the error "Type error: Number is not a function" because I am accessing ShippingChoice().Price inside my computed. Once the value is set it seems to overwrite my observable with a plain object.

I attempted to use a custom mapping to set Shipping choice as an observable but that didn't help.

Jsfiddle demo at: http://jsfiddle.net/on3al/2dv33vor/9/

Any insight or suggestions would be greatly appreaciated. I've been banging my head at this one too long.

HTML

      <!-- ko foreach: ShippingOptions -->
        <div class="radio">
            <input type="radio" name="optionsRadios" 
                  data-bind="checkedValue: $data ,checked: $root.ShippingChoice" /> 
            <span data-bind="text: Carrier"></span>
            <span data-bind="text: Service"></span>
            <span data-bind="money: Price"></span> 
        </div>
        <!-- /ko -->
    </div>
    <div class="col-xs-4">
         <h5 class="text-right">Subtotal <strong data-bind="money: SubTotal"></strong></h5>
         <h5 class="text-right" data-bind="money: ShippingChoice().Price">Shipping </h5>
         <h4 class="text-right">Total <strong data-bind="money: Total"></strong></h4>
    </div>

Javascript

   var mapping = {
         'ShippingChoice': {
             create: function (options) {
                 return ko.observable(options.data);
             }
         }
     };

     CartViewModel = function (data) {
         var self = this;
         ko.mapping.fromJS(data, mapping, self);

         self.SubTotal = ko.computed(function () {
             var subTotal = 0;
             for (var i = 0; i < self.Items().length; i++) {
                 var item = self.Items()[i];
                 subTotal += item.Quantity() * item.Product.Price();
             }
             return subTotal;
         }, self);

         self.Total = ko.computed(function () {
             return self.SubTotal()+ self.ShippingChoice().Price;
         }, self);

         self.decreaseQty = function (item) {
             var currQty = item.Quantity();
             if (currQty > 0) {
                 item.Quantity(currQty - 1);
             }
             self.updateQty(item);
         };

         self.increaseQty = function (item) {
             var currQty = item.Quantity();
             item.Quantity(currQty + 1);
             self.updateQty(item);
         };
     };

     ko.bindingHandlers.money = {
         update: function (element, valueAccessor, allBindingsAccessor) {
             var value = ko.utils.unwrapObservable(valueAccessor());
             var positions = 2;
             var formattedValue = value.toFixed(positions);
             var finalFormatted = '$' + formattedValue;

             ko.bindingHandlers.text.update(element, function () {
                 return finalFormatted;
             });
         },
         defaultPositions: 2,
     };

     var cartViewModel = new CartViewModel({
         "$id": "1",
             "Id": "540eb73cff622605c4f45b39",
             "Items": [{
             "$id": "2",
                 "Product": {
                 "$id": "3",
                     "Status": "In Stock",
                     "Price": 19.99,
                     "QuantityInStock": 12,
                     "Brand": "Dummy Brand",
                     "Weight": 8.0,
                     "Width": 12.0,
                     "Length": 14.0,
                     "Height": 3.0,
                     "UrlSlug": "dummy-slug",
                     "Title": "Dummy Product",
             },
                 "Quantity": 7,
         }],
             "Country": "CA",
             "PostalCode": null,
             "ShippingOptions": [{
             "$id": "4",
                 "Carrier": "USPS",
                 "Price": 22.1,
                 "Service": "FirstClassPackageInternationalService"
         }, {
             "$id": "5",
                 "Carrier": "USPS",
                 "Price": 32.85,
                 "Service": "PriorityMailInternational"
         }, {
             "$id": "6",
                 "Carrier": "USPS",
                 "Price": 46.96,
                 "Service": "ExpressMailInternational"
         }],
             "ShippingChoice": {
             "$id": "7",
                 "Carrier": "",
                 "Price": 8.0,
                 "Service": ""
         }
     });
     ko.applyBindings(cartViewModel);
3
BTW, your fiddle is missing the updateQty function. - Tomalak

3 Answers

3
votes

You may also solve your problem just by "copying" your ShippingOptions in mapping options:

var mapping = {
  'ShippingChoice': {
    create: function (options) {
      return ko.observable(options.data);
    }
  },
  copy: [ 'ShippingOptions' ]
};

It will result in plain object array and prices will be Numbers rather than observables.

http://jsfiddle.net/2dv33vor/12/

2
votes

You almost had it. Your mapping object should be changed just a little bit:

var mapping = {
    'ShippingChoice': {
        create: function (options) {
            return ko.observable(ko.mapping.fromJS(options.data));
        }
    }
};

And in your view model:

...
self.Total = ko.computed(function () {
    return self.SubTotal()+ self.ShippingChoice().Price(); // I've added () to the Price, since it's an observable now.
}, self);

The thing is when your ShippingOptions array was mapped, all it's properties were made observables. So you should make your ShippingChoice's properties observables too.

Working demo.

1
votes

Change your Total computed from:

 self.Total = ko.computed(function () {
    return self.SubTotal() + self.ShippingChoice().Price;
 }, self);

to:

 self.Total = ko.computed(function () {
    return ko.unwrap(self.SubTotal) + ko.unwrap(self.ShippingChoice().Price);
 }, self);

When you add a console.log(value) to the money binding handler in your original code, you see this as soon as you make a shipping choice:

"307.65000000000003function d(){if(0<
arguments.length)return d.Pa(c,arguments[0])&&(d.X(),c=arguments[0],d.W()),this;a.k.Jb(d);return c}"

This indicates an observable was concatenated to a non-observable with the + operator.

Your Total function was the only place where this could happen.