I have a view-model where some properties values available for selection are dictated by other properties, this is set via the requires
field:
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,
}];
The code works (you can see how clicking different options changes dependent options database
and database clustering
): http://jsfiddle.net/g18c/DTdyM/
This code is statically typed, and i am trying to convert this so that arbitrary data (and requires mappings) can be sent from the server viewmodel created with ko.mapping
.
The code to calculate available options for selection is in a helper function (example shown below for two dependent properties from my initial statically defined example):
self.availableDatabases = myutils.ko.createComputedDepdency(this.selectedOs, this.dbOptions);
self.availableClusteringOptions = myutils.ko.createComputedDepdency(this.selectedDb, this.dbClusteringOptions);
I have rewritten my serverData and the only things i need to know form my model are the selected items of the arrays passed in dynamically from the server, in this case it is the options array: selectedServerOption
, selectedOsOption
, selectedDatabaseOption
and selectedClusterOption
I am stuck handling the mapping and unsure how to work with the requiresMapping
array.
How can I handle the mapping of the option fields below?
var serverData = {
options: [serverOptions, osOptions, databaseOptions, clusterOptions],
requiresMappings: [
{target: "selectedOs", options: "dbOptions"},
{target: "selectedDb", options: "dbClusteringOptions"}
]
}
var mappingScheme = {
'options' : {
create: function(options){
console.log("creating sku: " + options.data.sku);
// 1) create dependency using requiresMappings property
// myutils.ko.createComputedDepdency(this.selectedOs, this.dbOptions);
// 2) subscribe to updates
// self.availableDatabases.subscribe(function () {self.selectedDb(self.availableDatabases()[0].sku);});
}
},
// ignore these mappings we don't want to observe them, they are used to define mappings for the creation function above
'ignore' : ["requiresMappings"]
}
var viewModel = ko.mapping.fromJS(serverData, mappingScheme);
My current fiddle is here: http://jsfiddle.net/g18c/DTdyM/5/
After reviewing Automate mapping of dependent properties in knockout answer and the code from @PW Kad, it made sense to write a simple custom mapper.
I chose to write my own mapper over ko.mapping as i couldn't work out how to reference the mapping for the requires (with ko.mapping), and in particular how to make multiple properties from the create
mapping function (as i could see i could only return a single new property, not multiple as i would need).
It works seemingly well, but would appreciate comments or an alternative using ko.mapping just to help my understanding especially if i have reinvented the wheel!
Fiddle is here: http://jsfiddle.net/g18c/DTdyM/26/
var myutils = myutils || {}; // if namespace is not defined, make it equal to an empty object
myutils.ko = (function(){
var createComputedDepdency = function(targetDependency, options){
var computedProperty = ko.computed(function(){
var targetValue = targetDependency();
if(typeof targetValue === "undefined")
return [];
return ko.utils.arrayFilter(options, function(opt){
if(typeof opt.requires === "undefined")
return true;
else
return opt.requires && opt.requires.indexOf(targetValue) > -1;
});
});
return computedProperty;
};
var createProductViewModel = function(options){
var viewModel = {};
var length = options.length;
console.log(length);
for (var i = 0; i < length; i++) {
var option = options[i];
console.log("creating property: " + option.selectedName + ", options: " + option.optionsName);
// create the property for selected value, i.e. object.firstName = ko.observable();
viewModel[option.selectedName] = ko.observable();
if(option.requires)
{
var computedOptions = createComputedDepdency(viewModel[option.requires.target],option.data);
viewModel[option.optionsName] = computedOptions;
console.log("making callback scope object for: " + option.optionsName );
var callbackScope = {
callbackName: option.optionsName,
options: computedOptions,
selectedValue: viewModel[option.selectedName]
};
// when the list of available options changes, set the selected property to the first option
computedOptions.subscribe(function () {
var scope = this;
console.log("my object: %o", scope);
scope.selectedValue(scope.options()[0].sku);
console.log("in subscribe function for..." + scope.callbackName);
},callbackScope);
}
else
{
// create the property holding values, i.e. object.nameOptions = serverData.names;
viewModel[option.optionsName] = option.data;
}
}
// now all options have been created, loop through the array one last time and set the dependent options.
// note that this should be done last, as dependent calculated properties will subscribed to these events
// and update their default first options
for (var x = 0; x < length; x++) {
var option = options[x];
// only need to do this to non-calculated values
if(!option.requires)
{
viewModel[option.selectedName](viewModel[option.optionsName][0].sku);
}
}
return viewModel;
};
return{
createProductViewModel: createProductViewModel
};
}());
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 databaseClusterOptions = [{
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,
}];
var serverData = {
options: [
{ data: serverOptions, selectedName: "selectedServer", optionsName: "serverOptions" },
{ data: osOptions, selectedName: "selectedOs", optionsName: "osOptions" },
{ data: databaseOptions, selectedName: "selectedDb", optionsName: "availableDatabases", requires: { target: "selectedOs" } },
{ data: databaseClusterOptions, selectedName: "selectedDbCluster", optionsName: "availableClusteringOptions", requires: { target: "selectedDb" } }
]
};
var viewModel = myutils.ko.createProductViewModel(serverData.options);
ko.applyBindings(viewModel);