Search code examples
knockout.jsknockout-mapping-plugin

Automate mapping of dependent properties in knockout


I have a list of product options each with an sku identifier that is received as JSON from the server. I then have other options which depend on prerequisite values being selected, this is defined by the requires array property of the product-option arrays:

var serverOptions = [{
    name: "DELL R210",
    price: 100,
    sku: 1001,
},{
    name: "DELL R710",
    price: 200,
    sku: 1002,
},{
    name: "DELL R720 Dual CPU",
    price: 300,
    sku: 1003,
}];

var osOptions = [{
    name: "Windows Standard",
    sku: "201",
    price: 1,
}, {
    name: "Windows Enterprise",
    sku: "202",
    price: 2,
}, {
    name: "CentOS",
    sku: "203",
    price: 0,
}, {
    name: "Debian",
    sku: "204",
    price: 4,
}];

var databaseOptions = [{
    name: "None",
    sku: "0",
    price: 0,
}, {
    name: "SQL Express",
    sku: "401",
    requires: ["201", "202"],
    price: 10,
}, {
    name: "SQL Standard",
    sku: "402",
    requires: ["202"],
    price: 5,
}, {
    name: "MySQL",
    sku: "MySQL1",
    requires: ["201", "202", "203"],
    price: 11,
}, {
    name: "RavenDb",
    sku: "403",
    requires: ["203"],
    price: 12,
}, {
    name: "MongoDB",
    sku: "404",
    requires: ["204"],
    price: 13,
}];

var clusterOptions = [{
    name: "None",
    sku: "0",
    price: 0,
}, {
    name: "Standard MySQL Cluster",
    sku: "4101",
    requires: ["MySQL1"],
    price: 10,
}, {
    name: "Enterprise MS SQL Cluster",
    sku: "4102",
    requires: ["402"],
    price: 5,
}, {
    name: "NoSQL Sharding",
    sku: "4103",
    requires: ["403","404"],
    price: 10,
}];

In my viewmodel, I then filter the values available for selection with the following code (generic for the most part, variable references change depending on what is being used to query for the requires checking):

self.availableClusteringOptions = ko.computed(function () {
        var selectedDbSku = this.selectedDb();

        if (typeof selectedDbSku === "undefined")
            return [];

        return ko.utils.arrayFilter(this.dbClusteringOptions, function (dbCluster) {
            if (typeof dbCluster.requires === "undefined")
                return true;
            else
                return dbCluster.requires && dbCluster.requires.indexOf(selectedDbSku) > -1;
        }, this);
    }, this);

Whilst my code works, it is statically typed in advance manually by me and as I add new fields I am doing a lot of copy paste as the code syntax is identical just the variables change (case in point self.availableDatabases and self.availableClusteringOptions).

In the future we may add in (from the server database) a completely new option object, that would need to be handled, mapped and relationships created, all dynamically. A future product option that could be added could be for example:

var managementOptions = [{
    name: "Self managed",
    sku: "0",
    price: "0"
},{
    name: "Windows Management",
    sku: "WindowsManagement",
    price: 1,
    requires: ["201", "202"],
}, {
    name: "Linux Management",
    sku: "LinxManagement",
    requires: ["203", "204"],
    price: 2,
}, {
    name: "Basic Management",
    sku: "ManageAll",
    price: 0,
    requires: ["201", "202","203","204"],
}]; 

This is crying out for automation particularly as this data will be fed from a database, but i don't know where to start with this.

I have seen knockout mapping plugin that will create the viewmodel from json, but from the documentation i am not sure how this would tie in with my data structures as my JSON is a lot more complex than the examples.

How can i automate this code to allow for additional dependent 'requires' prerequisite values to be set dynamically? Can knockout mapping help in this instance or do i need to look at an alternative avenue?

Fiddle is here: http://jsfiddle.net/g18c/E54YC/7/

var serverConfig = function () {
    var self = this;

    self.osOptions = osOptions;
    self.dbOptions = databaseOptions;
    self.dbClusteringOptions = clusterOptions;
    self.serverOptions = serverOptions;

    self.selectedServer = ko.observable();
    self.selectedOs = ko.observable();
    self.selectedDb = ko.observable();
    self.selectedDbCluster = ko.observable();

    self.lookupItemForSku = function (lookup, values) {
        if ((typeof lookup != "undefined") && (lookup != "0"))
            return ko.utils.arrayFirst(values, function (item) { return item.sku == lookup; }, this);
        else
            return null;
    };

    self.availableDatabases = ko.computed(function () {
        var selectedOsSku = this.selectedOs();

        if (typeof selectedOsSku === "undefined")
            return [];

        return ko.utils.arrayFilter(this.dbOptions, function (db) {
            if (typeof db.requires === "undefined")
                return true;
            else
                return db.requires && db.requires.indexOf(selectedOsSku) > -1;
        }, this);
    }, this);

    self.availableClusteringOptions = ko.computed(function () {
        var selectedDbSku = this.selectedDb();

        if (typeof selectedDbSku === "undefined")
            return [];

        return ko.utils.arrayFilter(this.dbClusteringOptions, function (dbCluster) {
            if (typeof dbCluster.requires === "undefined")
                return true;
            else
                return dbCluster.requires && dbCluster.requires.indexOf(selectedDbSku) > -1;
        }, this);
    }, this);

    self.availableDatabases.subscribe(function () {
        self.selectedDb(self.availableDatabases()[0].sku);
    });

    self.availableClusteringOptions.subscribe(function () {
        self.selectedDbCluster(self.availableClusteringOptions()[0].sku);
    });

    self.selectedServer(self.serverOptions[0].sku);
    self.selectedOs(self.osOptions[0].sku);

    return self;
};

var configModel = new serverConfig();

ko.applyBindings(configModel);

Solution

  • Here is a quick update to your fiddle showing how you could create your own models to map data - http://jsfiddle.net/E54YC/9/

    If you wanted to get more intense and have relationship mapping you have many choices, but the top two I would consider -

    1. Write your own custom code to handle requirements and whether you can select this or that depending on it's requirements

    2. Use a client side data library (ORM) to handle it like Breeze.js where relationship mapping is handled for you and you just need to put the logic in your view model.

    Example of a model -

    function serverModel (server) {
        var self = this;
        self.Name = ko.observable(server.name);
        self.Price = ko.observable(server.price);
        self.SKU = ko.observable(server.sku);
    }