Knockout JS - Binding select list to data object
objective
I wish to create a claim form. The claim form must support the following:
- Add (create) claim line
- Store (read) all claim lines
- Edit (update) claim line
- Delete (destroy) claim lines
- Display a variable number of fields based on user selection
Requirement 5 is handled here
, retrieving boolean value from selected array object
question
The editing and updating process for the current code is broken, I know there is a problem with binding the data to the appropriate select list, but I can't see it.
desired result
- If possible, the answers to the above SO questions must remain the same.
- When the user adds a claim row, the form should revert to its state onLoad.
- When a user edits a claim row, the form should be rebuilt to accommodate the data
- When the user updates a claim row, the row in the saved claims list should be updated
Java script
var myViewModel = window["myViewModel"] = {};
(function () {
myViewModel = function () {
var self = this;
self.claimLines = ko.observableArray(ko.utils.arrayMap(claimLines, function (claimLine) {
return new ClaimLine(new ClaimLine("","","","","",""));
}));
// Changed newClaimLine to observable with empty ClaimLine
self.newClaimLine = ko.observable(new ClaimLine("","","","","",""));
self.editClaimLine = function (claimLineItem) {
var editable = new ClaimLine(claimLineItem.serviceStartDate(), claimLineItem.serviceEndDate(), claimLineItem.planType(), claimLineItem.expenseType(), claimLineItem.amount(), claimLineItem.provider());
claimLineBeingEdited = claimLineItem;
self.newClaimLine(editable);
var test = 'test';
};
// The only thing the update method does is emptying the editor form
self.updateClaimLine = function (claimLineBeingUpdated) {
var test = 'test';
claimLineBeingEdited.serviceStartDate(claimLineBeingUpdated.serviceStartDate());
claimLineBeingEdited.serviceEndDate(claimLineBeingUpdated.serviceEndDate());
claimLineBeingEdited.planType(claimLineBeingUpdated.planType());
claimLineBeingEdited.expenseType(claimLineBeingUpdated.expenseType());
claimLineBeingEdited.amount(claimLineBeingUpdated.amount());
claimLineBeingEdited.provider(claimLineBeingUpdated.provider());
self.newClaimLine(new ClaimLine("","","","","",""));
isClaimFor = false;
isExpenseType = false;
};
// This method can only be used for adding new items, not updating existing items
self.addClaimLine = function (claimLineBeingAdded) {
self.claimLines.push(new ClaimLine(claimLineBeingAdded.serviceStartDate(), claimLineBeingAdded.serviceEndDate(), claimLineBeingAdded.planType(), claimLineBeingAdded.expenseType(), claimLineBeingAdded.amount(), claimLineBeingAdded.provider()));
self.newClaimLine(new ClaimLine("","","","","",""));
};
//remove an existing claim line
self.removeClaimLine = function (claimLine) {
self.claimLines.remove(claimLine);
}
//aggregate claim amounts
self.grandTotal = ko.computed(function() {
var total = 0;
$.each(self.claimLines(), function() {
total += parseFloat(this.amount())
});
return "$" + total.toFixed(2);
});
};
function ClaimLine(serviceStartDate, serviceEndDate, planType, expenseType, amount, provider) {
var line = this;
line.serviceStartDate = ko.observable(ko.utils.unwrapObservable(serviceStartDate));
line.serviceEndDate = ko.observable(ko.utils.unwrapObservable(serviceEndDate));
line.planType = ko.observable(ko.utils.unwrapObservable(selectedPlanTypeId));
line.expenseType = ko.observable(ko.utils.unwrapObservable(selectedExpenseTypeId));
line.amount = ko.observable(ko.utils.unwrapObservable(amount));
line.provider = ko.observable(ko.utils.unwrapObservable(provider));
line.expenseTypeName = ko.computed(function() {
return ko.utils.arrayFirst(self.expenseTypes, function (expenseTypeSomething) {
return expenseTypeSomething.id == line.expenseType();
});
});
line.planTypeName = ko.computed(function() {
return ko.utils.arrayFirst(self.planTypes, function (planTypeSomething) {
return planTypeSomething.id == line.planType();
});
});
}
var claimLines = [
];
self.planTypes = [
{ id: 1, name: 'The EBC HRA - Deductible', hasClaimFor: true, hasExpenseType: false },
{ id: 2, name: 'FSA - Health Care FSA', hasClaimFor: false, hasExpenseType: true },
{ id: 3, name: 'FSA - Dependent Care FSA', hasClaimFor: false, hasExpenseType: true }
];
self.claimForWhom = [
{ id: 1, name: "Self"},
{ id: 2, name: "Boston Allen (Dependent)"},
{ id: 3, name: "Bishop Allen (Dependent)"},
{ id: 4, name: "Billy Allen Jr (Dependent)"},
{ id: 5, name: "Billy Allen Sr (Dependent)"},
{ id: 6, name: "Name not listed"}
];
self.expenseTypes = [
{ id: 1, name: "Chiropractic"},
{ id: 2, name: "Dental"},
{ id: 3, name: "Massage Therapy"},
{ id: 4, name: "Medical"},
{ id: 5, name: "Medical Mileage"},
{ id: 6, name: "Office Visit"},
{ id: 7, name: "Optical"},
{ id: 8, name: "Orthodontic"},
{ id: 9, name: "OTC"},
{ id: 10, name: "Prescription"},
{ id: 11, name: "Supplement/Vitamin"},
{ id: 12, name: "Therapy"}
];
self.providers = [
"Dean",
"Mercy Health",
"UW Health",
"Aurora"
];
self.selectedPlanTypeId = ko.observable();
self.selectedExpenseTypeId = ko.observable();
self.selectedClaimForWhomId = ko.observable();
self.selectedPlanType = ko.computed(function () {
var selectedPlanTypeId = self.selectedPlanTypeId();
return ko.utils.arrayFirst(self.planTypes, function (planType) {
return planType.id == selectedPlanTypeId;
});
});
self.selectedExpenseType = ko.computed(function () {
var selectedExpenseTypeId = self.selectedExpenseTypeId();
return ko.utils.arrayFirst(self.expenseTypes, function (expenseType) {
return expenseType.id == selectedExpenseTypeId;
});
});
self.isClaimFor = ko.computed(function(){
var selectedPlanType = self.selectedPlanType();
return selectedPlanType && !!selectedPlanType.hasClaimFor;
});
self.isExpenseType = ko.computed(function(){
var selectedPlanType = self.selectedPlanType();
return selectedPlanType && !!selectedPlanType.hasExpenseType;
});
})();
$(document).ready(function(){
myViewModel = new myViewModel();
ko.applyBindings(myViewModel);
$('.datepicker').datepicker();
});
HTML
<h3 class="body">Enter Claim Lines</h3>
<form class="form-horizontal col-xs-12 col-sm-12 col-md-12 col-lg-12" role="form" data-bind="with: newClaimLine">
<div class="form-group">
<label for="serviceStartDate" class="col-sm-4 control-label">Service Start Date</label>
<div class="col-sm-4">
<input id="serviceStartDate" type="date" class="form-control datepicker" data-bind="value: serviceStartDate" placeholder="mm/dd/yyyy" />
</div>
</div>
<div class="form-group">
<label for="serviceEndDate" class="col-sm-4 control-label">Service End Date</label>
<div class="col-sm-4">
<input id="serviceEndDate" type="date" class="form-control datepicker" data-bind="value: serviceEndDate" placeholder="mm/dd/yyyy" />
</div>
</div>
<div class="form-group">
<label for="planType" class="col-sm-4 control-label">Plan Type</label>
<div class="col-sm-4">
<select id="planType" class="form-control" data-bind="options: planTypes, optionsText: 'name', optionsCaption: 'Choose Plan Type', optionsValue: 'id', value: selectedPlanTypeId">
</select>
</div>
</div>
<div data-bind="if: isClaimFor">
<div class="form-group">
<label for="claimForWhom" class="col-sm-4 control-label">Claim For</label>
<div class="col-sm-4">
<select id="claimForWhom" class="form-control" data-bind="options: claimForWhom, optionsText : 'name', optionsCaption: 'Select Dependent', optionsValue: 'id', value: selectedClaimForWhomId"></select>
</div>
</div>
</div>
<div data-bind="if: isExpenseType">
<div class="form-group">
<label for="expenseType" class="col-sm-4 control-label">Expense Type</label>
<div class="col-sm-4">
<select id="expenseType" class="form-control" data-bind="options: expenseTypes, optionsText : 'name', optionsCaption: 'Select Expense Type', optionsValue: 'id', value: selectedExpenseTypeId"></select>
</div>
</div>
</div>
<div class="form-group">
<label for="amount" class="col-sm-4 control-label">Amount</label>
<div class="col-sm-4">
<input id="amount" type="date" class="form-control" data-bind="value: amount" placeholder="Enter Amount" />
</div>
</div>
<div class="form-group">
<label for="provider" class="col-sm-4 control-label">Provider</label>
<div class="col-sm-4">
<select id="provider" class="form-control" data-bind="options: providers, optionsCaption: 'Choose Provider Type', value: provider">
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-6 col-sm-offset-4">
<button class="btn btn-default" data-bind="click: $root.addClaimLine"><i class="fa fa-plus fa-lg fa-fw"></i> Add Claim Line</button>
<button class="btn btn-default" data-bind="click: $root.updateClaimLine"><i class="fa fa-refresh fa-lg fa-fw"></i> Update Claim Line</button>
</div>
</div>
</form>
<!-- Desktop saved claim lines -->
<table class="hidden-xs table table-responsive table-condensed" data-bind="visible: claimLines().length > 0">
<thead>
<tr>
<th colspan="2">Saved Claim Lines
<span class="pull-right">Claim Total = <span data-bind="text: grandTotal()"></span></span>
</th>
</tr>
</thead>
<tbody data-bind="foreach: claimLines">
<tr>
<td>
<p><strong><span data-bind="text: planTypeName().name"></span> - <span data-bind="text: expenseTypeName().name"></span><br /></strong><strong data-bind="text: $root.grandTotal()"></strong> claim incurred between <strong data-bind="text: serviceStartDate"></strong> and <strong data-bind="text: serviceEndDate"></strong>.</p>
</td>
<td class="text-right">
<button data-bind="click: $root.editClaimLine" class="btn btn-link">
<i class="fa fa-edit fa-2x"></i>
</button>
<button data-bind="click: $root.removeClaimLine" class="btn btn-link">
<i class="fa fa-times fa-2x"></i>
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-right" colspan="2">
<span>Claim Total = <span data-bind="text: grandTotal()"></span></span>
</th>
</tr>
</tfoot>
</table>
There are some problems here.
The array of your choice is declared as self.planTypes = ...
. self is a variable in the myViewModel constructor. You should get an exception though, something has declared an argument equal to window.
Your choice...observables are also all in window scope and not contained in myViewModel.
When you add a new claim line, depending on what you choose, I get javascript errors like expenseType is null.
solution
I created a top-level namespace calling model and attached everything to that model. I created an explicit class for editing declaration lines. This allows you to add various helper functions and observables without polluting the declaration line itself. I have changed all option bindings to remove the Id parameter. I find it much easier to use the object instead of constantly looking up the array members.
I have implemented add and update functionality.
I also removed the datapicker jQuery call because with this plugin you need to do more work to update the observable. The DatePicker plugin and Knockout do not work side-by-side without help (i.e. custom bindings).
I also added the letters E and R ("edit and delete") to the button on the claim row as I don't have any UI (missing CSS in your fiddle?)
HTML
<section class="row top10">
<div class="col-xs-12 col-sm-12 col-md-8 col-lg-8 col-md-offset-2 col-lg-offset-2">
<form class="form-horizontal col-xs-12 col-sm-12 col-md-12 col-lg-12" role="form" data-bind="with: newClaimLine">
<div class="form-group">
<label for="serviceStartDate" class="col-sm-4 control-label">Service Start Date</label>
<div class="col-sm-4">
<input id="serviceStartDate" type="date" class="form-control datepicker" data-bind="value: serviceStartDate" placeholder="mm/dd/yyyy" />
</div>
</div>
<div class="form-group">
<label for="serviceEndDate" class="col-sm-4 control-label">Service End Date</label>
<div class="col-sm-4">
<input id="serviceEndDate" type="date" class="form-control datepicker" data-bind="value: serviceEndDate" placeholder="mm/dd/yyyy" />
</div>
</div>
<div class="form-group">
<label for="planType" class="col-sm-4 control-label">Plan Type</label>
<div class="col-sm-4">
<select id="planType" class="form-control" data-bind="options: Models.planTypes, optionsText: 'name', optionsCaption: 'Choose Plan Type', value: planType"></select>
</div>
</div>
<div data-bind="if: isClaimFor">
<div class="form-group">
<label for="claimForWhom" class="col-sm-4 control-label">Claim For</label>
<div class="col-sm-4">
<select id="claimForWhom" class="form-control" data-bind="options: Models.claimForWhom, optionsText : 'name', optionsCaption: 'Select Dependent', value: claimFor"></select>
</div>
</div>
</div>
<div data-bind="if: isExpenseType">
<div class="form-group">
<label for="expenseType" class="col-sm-4 control-label">Expense Type</label>
<div class="col-sm-4">
<select id="expenseType" class="form-control" data-bind="options: Models.expenseTypes, optionsText : 'name', optionsCaption: 'Select Expense Type', value: expenseType"></select>
</div>
</div>
</div>
<div class="form-group">
<label for="amount" class="col-sm-4 control-label">Amount</label>
<div class="col-sm-4">
<input id="amount" type="number" class="form-control" data-bind="value: amount" placeholder="Enter Amount" />
</div>
</div>
<div class="form-group">
<label for="provider" class="col-sm-4 control-label">Provider</label>
<div class="col-sm-4">
<select id="provider" class="form-control" data-bind="options: Models.providers, optionsCaption: 'Choose Provider Type', value: provider"></select>
</div>
</div>
<div class="form-group">
<div class="col-sm-6 col-sm-offset-4">
<button class="btn btn-default" data-bind="click: $root.addClaimLine, enable: !claimId()"><i class="fa fa-plus fa-lg fa-fw"></i> Add Claim Line</button>
<button class="btn btn-default" data-bind="click: $root.updateClaimLine, enable: claimId"><i class="fa fa-refresh fa-lg fa-fw"></i> Update Claim Line</button>
</div>
</div>
</form>
<!-- Desktop saved claim lines -->
<table class="hidden-xs table table-responsive table-condensed" data-bind="visible: claimLines().length > 0">
<thead>
<tr>
<th colspan="2">Saved Claim Lines <span class="pull-right">Claim Total = <span data-bind="text: grandTotal()"></span></span>
</th>
</tr>
</thead>
<tbody data-bind="foreach: claimLines">
<tr>
<td>
<p><strong><span data-bind="text: planTypeName().name"></span> - <span data-bind="text: expenseTypeName().name"></span><br /></strong><strong data-bind="text: $root.grandTotal()"></strong> claim incurred between <strong data-bind="text: serviceStartDate"></strong> and <strong data-bind="text: serviceEndDate"></strong>.</p>
</td>
<td class="text-right">
<button data-bind="click: $root.editClaimLine" class="btn btn-link"> <i class="fa fa-edit fa-2x">E</i>
</button>
<button data-bind="click: $root.removeClaimLine" class="btn btn-link"> <i class="fa fa-times fa-2x">R</i>
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-right" colspan="2"> <span>Claim Total = <span data-bind="text: grandTotal()"></span></span>
</th>
</tr>
</tfoot>
</table>
<!-- Mobile saved claim lines -->
<div class="hidden-sm hidden-md hidden-lg" data-bind="visible: claimLines().length > 0">
<h3 class="body">Saved Claim Lines</h3>
<div data-bind="foreach: claimLines">
<div>
<p>Your <strong data-bind="text: planTypeName().name"></strong> incurred a <strong data-bind="text: expenseTypeName().name"></strong> claim for <strong data-bind="text: $root.grandTotal()"></strong> between <strong data-bind="text: serviceStartDate"></strong> - <strong data-bind="text: serviceEndDate"></strong>.</p>
<p>
<button data-bind="click: $root.editClaimLine" class="btn btn-default"> <i class="fa fa-edit fa-lg"></i> Edit Claim Line</button>
<button data-bind="click: $root.removeClaimLine" class="btn btn-default"> <i class="fa fa-times fa-lg"></i> Delete Claim Line</button>
</p>
</div>
</div>
</div>
<h3 class="body">Attach Supporting Documentation</h3>
<button class="btn btn-default btn-lg" role="button"> <i class="fa fa-cloud-upload fa-3x fa-fw pull-left"></i>
<span class="pull-left text-left">Upload<br />Documentation</span>
</button>
<hr />
<div class="pull-right">
<button class="btn btn-link btn-lg">Cancel</button>
<button class="btn btn-default btn-lg" role="button"> <i class="fa fa-check fa-fw"></i>Verify Claim</button>
</div>
</div>
</section>
Java script
var Models = window["Models"] = {};
(function () {
Models.ViewModel = function () {
var self = this;
var newClaimId = 0;
self.claimLines = ko.observableArray(ko.utils.arrayMap(claimLines, function (claimLine) {
return new Models.ClaimLine("","","","","","", "");
}));
// Changed newClaimLine to observable with empty ClaimLine
self.newClaimLine = new Models.EditClaimLine();
self.editClaimLine = function(claimLineItem) {
self.newClaimLine.edit(claimLineItem);
};
/*
self.editClaimLine = function (claimLineItem) {
var editable = new ClaimLine(claimLineItem.serviceStartDate(), claimLineItem.serviceEndDate(), claimLineItem.planType(), claimLineItem.expenseType(), claimLineItem.amount(), claimLineItem.provider());
claimLineBeingEdited = claimLineItem;
self.newClaimLine(editable);
var test = 'test';
};
*/
// The only thing the update method does is emptying the editor form
self.updateClaimLine = function (claimLineBeingUpdated) {
var foundClaim = ko.utils.arrayFirst( self.claimLines(), function(item) { return item.claimId() == claimLineBeingUpdated.claimId(); } );
var test = 'test';
foundClaim.serviceStartDate(claimLineBeingUpdated.serviceStartDate());
foundClaim.serviceEndDate(claimLineBeingUpdated.serviceEndDate());
foundClaim.planType(claimLineBeingUpdated.planType());
foundClaim.expenseType(claimLineBeingUpdated.expenseType());
foundClaim.amount(claimLineBeingUpdated.amount());
foundClaim.provider(claimLineBeingUpdated.provider());
foundClaim.claimFor(claimLineBeingUpdated.claimFor());
self.newClaimLine.reset(); //(new ClaimLine("","","","","",""));
};
// This method can only be used for adding new items, not updating existing items
self.addClaimLine = function (claimLineBeingAdded) {
var newClaim = new Models.ClaimLine(claimLineBeingAdded.serviceStartDate, claimLineBeingAdded.serviceEndDate, claimLineBeingAdded.planType, claimLineBeingAdded.expenseType, claimLineBeingAdded.amount, claimLineBeingAdded.provider, claimLineBeingAdded.claimFor);
newClaim.claimId(++newClaimId);
self.claimLines.push(newClaim);
self.newClaimLine.reset(); //(new ClaimLine("","","","","",""));
};
//remove an existing claim line
self.removeClaimLine = function (claimLine) {
self.claimLines.remove(claimLine);
}
//aggregate claim amounts
self.grandTotal = ko.computed(function() {
var total = 0;
$.each(self.claimLines(), function() {
total += parseFloat(this.amount())
});
return "$" + total.toFixed(2);
});
};
Models.EditClaimLine = function() {
var self = this;
self.claimId = ko.observable();
self.serviceStartDate = ko.observable();
self.serviceEndDate = ko.observable();
self.planType = ko.observable();
self.claimFor = ko.observable();
self.expenseType = ko.observable();
self.amount = ko.observable();
self.provider = ko.observable();
self.isClaimFor = ko.computed(function(){
var selectedPlanType = self.planType();
return selectedPlanType && !!selectedPlanType.hasClaimFor;
});
self.isExpenseType = ko.computed(function(){
var selectedPlanType = self.planType();
return selectedPlanType && !!selectedPlanType.hasExpenseType;
});
self.reset = function(){
self.claimId(undefined);
self.serviceStartDate(undefined);
self.serviceEndDate(undefined);
self.planType(undefined);
self.claimFor(undefined);
self.expenseType(undefined);
self.amount(undefined);
self.provider(undefined);
};
self.edit = function(claim) {
self.claimId(claim.claimId());
self.serviceStartDate(claim.serviceStartDate());
self.serviceEndDate(claim.serviceEndDate());
self.planType(claim.planType());
self.claimFor(claim.claimFor());
self.expenseType(claim.expenseType());
self.amount(claim.amount());
self.provider(claim.provider());
};
self.reset();
}
Models.ClaimLine = function(serviceStartDate, serviceEndDate, planType, expenseType, amount, provider, claimFor) {
var line = this;
var getName = function(value){
return (ko.unwrap(value) || { name: '' }).name;
};
line.claimId = ko.observable();
line.serviceStartDate = ko.observable(ko.unwrap(serviceStartDate));
line.serviceEndDate = ko.observable(ko.unwrap(serviceEndDate));
line.planType = ko.observable(ko.unwrap(planType));
line.expenseType = ko.observable(ko.unwrap(expenseType));
line.amount = ko.observable(ko.unwrap(amount));
line.provider = ko.observable(ko.unwrap(provider));
line.claimFor = ko.observable(ko.unwrap(claimFor));
line.expenseTypeName = ko.computed(function() {
return getName(line.expenseType);
});
line.planTypeName = ko.computed(function() {
return getName(line.planType);
});
}
var claimLines = [
];
Models.planTypes = [
{ id: 1, name: 'The EBC HRA - Deductible', hasClaimFor: true, hasExpenseType: false },
{ id: 2, name: 'FSA - Health Care FSA', hasClaimFor: false, hasExpenseType: true },
{ id: 3, name: 'FSA - Dependent Care FSA', hasClaimFor: false, hasExpenseType: true }
];
Models.claimForWhom = [
{ id: 1, name: "Self"},
{ id: 2, name: "Boston Allen (Dependent)"},
{ id: 3, name: "Bishop Allen (Dependent)"},
{ id: 4, name: "Billy Allen Jr (Dependent)"},
{ id: 5, name: "Billy Allen Sr (Dependent)"},
{ id: 6, name: "Name not listed"}
];
Models.expenseTypes = [
{ id: 1, name: "Chiropractic"},
{ id: 2, name: "Dental"},
{ id: 3, name: "Massage Therapy"},
{ id: 4, name: "Medical"},
{ id: 5, name: "Medical Mileage"},
{ id: 6, name: "Office Visit"},
{ id: 7, name: "Optical"},
{ id: 8, name: "Orthodontic"},
{ id: 9, name: "OTC"},
{ id: 10, name: "Prescription"},
{ id: 11, name: "Supplement/Vitamin"},
{ id: 12, name: "Therapy"}
];
Models.providers = [
"Dean",
"Mercy Health",
"UW Health",
"Aurora"
];
})();
$(document).ready(function(){
var myViewModel = new Models.ViewModel();
ko.applyBindings(myViewModel);
//$('.datepicker').datepicker();
});