qtime67
qtime67

Reputation: 317

Knockoutjs mapping and validation with observable arrays

I am trying to create a KnockoutJS viewmodel, that models a customer order and many order items. I want to load initial data, and have validation of the data.

So far I can load data using knockoutjs.mapping, validate data added using the mapping.

// data to load into viewmodel
var modeldata = {
  "OrderID":1,
  "ReturnString":null,
  "CustomerName":"First Customer",
  "OrderDate":"2013-09-16T19:41:40.1639709+01:00",
  "OrderItems": [
     {"ItemID":0,
      "ItemName":"Name_0",
      "ItemPrice":0.0,
      "_destroy":false
     },
     {"ItemID":1,
      "ItemName":"Name_1",
      "ItemPrice":10.0,
      "_destroy":false
     },
     {"ItemID":2,
      "ItemName":"Name_2",
      "ItemPrice":20.0,
      "_destroy":false
     }
   ]
};


// setup defaults for validation
var validationOptions = {
    insertMessages: true,
    decorateElement: true,
    errorElementClass: 'errorCSS',
    messagesOnModified: true,
    debug: true,
    grouping: {
        deep: true,
        observable: false //Needed so added objects AFTER the initial setup get included
    },
};

ko.validation.init(validationOptions);


// define array model
var Item = function () {
    var self = this;
    ItemID = ko.observable();
    ItemName = ko.observable().extend({
      required: { message: '* item name needed' }
    });
    ItemPrice = ko.observable();
    _destroy = false;
}

// define view model
var ViewModel = function (data) {
    var self = this;
    self.OrderID = ko.observable();
    self.ReturnString = ko.observable();
    self.CustomerName = ko.observable().extend({
      required: { message: '* customer name needed' }
    });
    self.OrderDate = ko.observable();
    self.OrderItems = ko.observableArray([]);  // to be array of "Item"

    // create validation group
    self.orderErrors = ko.validation.group(self);
    self.orderItemErrors = ko.validation.group(
      self.OrderItems, { deep: true }
    );

    self.lineItemsValid = function () {
        var LValid = false;
        if (self.orderItemErrors().length > 0) {
            if (self.orderItemErrors()[0] != null) // important to test for null
                LValid = false;
            else 
                LValid = true;
            }
        else  LValid = true 
        if (LValid) {
            return true
            }
        else
            {
            self.orderItemErrors.showAllMessages();
            return false;
            }
    }

    self.orderValid = function () {
        var LValid = false;
        if (self.orderErrors().length > 0) {
            if (self.orderErrors()[0] != null) // important to test for null
                LValid = false;
            else
                LValid = true;
        }
        else LValid = true
        if (LValid) {
            return true
        }
        else {
            self.orderErrors.showAllMessages();
            return false;
        }
    }

    self.isValid = function () {
        if(self.orderValid() & self.lineItemsValid()){
        alert('All ok!')
        }
        else{
        alert('Errors!');}
    }


    // operations
    self.addLineItem = function () {
        self.OrderItems.unshift(new Item());
    }

    self.removeLineItem = function (item) {
        self.OrderItems.destroy(item);
    }

    // load data into model
    self.loadData = function () {
        ko.mapping.fromJS(modeldata, {}, self);
    }


}

$(document).ready(function () {
    var viewModel = new ViewModel()
    ko.applyBindings(viewModel);
});

Problems:

(1) I can also add order-items using a click function, but the data from these does not seem to get updated in the observable array. However, when I call a "remove" item function, the array item gets marked as deleted.

(2) When I load items with mapping, and test the validation (required = true), it works only for items loaded via mapping, not order-items I add after the mapping is complete

(3) When I update an order item brought in by mapping, the change is reflected immediately in the observable array, when I update an order item I added after the mapping, there is no update in the array.

I have a JSFiddle here:

http://jsfiddle.net/devops/ZsDjh/40/

I am sure it is something to do with how I am adding to the observable array but cannot see anything obvious - I am obviously missing something basic ... If anyone has any ideas?

thanks

Upvotes: 1

Views: 4111

Answers (1)

Roger Spurrell
Roger Spurrell

Reputation: 106

There are just a few minor oversights is all.

First, let's jump to (3)

(3) When I update an order item brought in by mapping, the change is reflected immediately in the observable array, when I update an order item I added after the mapping, there is no update in the array.

In your Item function the properties must be bound to this (self).

var Item = function () {
    var self = this;
    self._destroy = false;
    self.ItemID = ko.observable();
    self.ItemName = ko.observable().extend({ required: { message: '* item name needed' } });
    self.ItemPrice = ko.observable();
}

Now for (2),

(2) When I load items with mapping, and test the validation (required = true), it works only for items loaded via mapping, not order-items I add after the mapping is complete

This is because of the manner in which you are using the mapping plugin and with how you are adding new items to your OrderItems observableArray.

// load data into model
self.loadData = function () {
    ko.mapping.fromJS(modeldata, {}, self);
}

I couldn't replicate this from your fiddle, but I have a very good idea of what is likely happening.

Since the mapping pluging is simply creating observable properties from your JSON and respectively assigning (or reassigning in your case) properties on self it has no idea that each OrderItems object in the array is to be a new Item() object. It's just creating anonymous observable objects and placing them in a new observableArray and then is assigning that to self.OrderItems.

You must instruct the mapper how to process your array.

// outside your viewmodel
var itemMapping = {
    create: function (options) {
        return new Item(options.data);
    }
};

// load data into model
self.loadData = function () {
    ko.mapping.fromJS(modeldata, { OrderItems: itemMapping }, self);
}

Now we are passing each item in the JSON array into the create function of the itemMapping. However, we now have a problem. The Item function does not take any parameters. So lets fix that. And, while were at it lets have the mapper help us out again with creating the observable properties for our Item.

var Item = function (data) {
    var self = this;
    self._destroy = false;
    //self.ItemID = ko.observable(data.ItemID);
    //self.ItemName = ko.observable(data.ItemName).extend({ required: true });
    //self.ItemPrice = ko.observable(data.ItemPrice);
    ko.mapping.fromJS(data, {}, self);
    self.ItemName.extend({ required: { message: '* item name needed' } });
}

Since we've modified your Item function we need to update the addLineItem method.

// operations
self.addLineItem = function () {
    // set the default values
    self.OrderItems.unshift(new Item({ ItemId: null, ItemName: "", ItemPrice: 0 }));
}

Now, get rid of your self.lineItemsValid and self.orderValid methods and remove your self.orderItemErrors property.

Rename and update your isValid method.

self.checkValid = function () {
    if(self.isValid()){
      alert('All ok!');
    }
    else{
      self.orderErrors.showAllMessages();
    }
}

self.isValid() is a method created by the validation plugin. You were overwriting it.

And, update your html.

<a href="#" data-bind="click: checkValid">Check is valid</a>

Finally (1),

(1) I can also add order-items using a click function, but the data from these does not seem to get updated in the observable array.

This is fixed with the modifications we made to the Item function.

However, when I call a "remove" item function, the array item gets marked as deleted.

I'm not sure if you want it to be complete removed or simply labeled as _destroyed.

If you just want it removed update your removeLineItem method.

self.removeLineItem = function (item) {
    self.OrderItems.remove(item);
}

Please refer to the knockout documentation on Observable Arrays and read about the remove and destroy methods to determine what best suits you.

Here is the modified jsFiddle.

Cheers!

P.S. Please +1 since this answer greased your wheels! :)

Upvotes: 9

Related Questions