I have an array of strings like:
questions: [
"Question 1?",
"Question 2?",
"Question 3?",
"Question 4?",
],
Then I have form fields in my data() like:
legitForm: {
name: '', // this will be equal to question string
answer: '',
description: '',
duration: '',
},
Now, the problem I'm facing is when I fill inputs for any of questions
above the same field for other questions gets same value.
Here is my template code:
<div v-for="(question, index) in questions" :key="index">
<form #submit.prevent="legitStore(question)" method="post">
<div class="row">
<div class="col-md-12">
<p>
<strong>{{question}}</strong>
</p>
</div>
<div class="col-md-6">
<label for="answer">Answer</label>
<select class="mb-1 field" v-model="legitForm.answer" name="answer" id="answer">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<div class="col-md-6">
<label for="duration">Duration</label>
<input class="field" v-model="legitForm.duration" type="text">
</div>
<div class="col-md-12">
<label for="description">Description</label>
<textarea style="height: 190px;" type="text" cols="5" rows="10" id="address" class="mb-1 field" v-model="legitForm.description"></textarea>
</div>
</div>
<button type="submit" class="saveButtonExperience float-right btn btn--custom">
<span class="text">Save</span>
</button>
</form>
</div>
And this is my post method that sends data to backend:
legitStore(question) {
this.legitForm.name = question; // set "name" in `legitForm` to value of `question` string
axios.post('/api/auth/userLegitsStore', this.legitForm, {
headers: {
Authorization: localStorage.getItem('access_token')
}
})
.then(res => {
// reset my data after success
this.legitForm = {
name: '',
answer: '',
description: '',
duration: '',
};
})
.catch(error => {
var errors = error.response.data;
let errorsHtml = '<ol>';
$.each(errors.errors,function (k,v) {
errorsHtml += '<li>'+ v + '</li>';
});
errorsHtml += '</ol>';
console.log(errorsHtml);
})
},
Here is issue screenshot:
Note: I've tried to
change legitForm to array like legitForm: [], and legitForm: [{....}]
add index to my inputs v-model
but I've got errors so i wasn't sure what I'm doing wrong, that's why
I'm asking here.
If you think of your questions as questions and answers, you can do something like this:
questions: [
{
question: 'Question 1',
answer: null,
description: null,
duration: null,
},
{
question: 'Question 2',
answer: null,
description: null,
duration: null,
},
]
Then when looping through your form, it would be more like this:
<div v-for="(question, index) in questions" :key="index">
<form #submit.prevent="legitStore(question)" method="post">
...
<select class="mb-1 field" v-model="question.answer" name="answer" id="answer">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
...
</form>
</div>
And in the storing function you could send the data in the question instead of this.legitForm
Like you said you tried here:
change legitForm to array like legitForm: [], and legitForm: [{....}]
add index to my inputs v-model
You are supposed to be doing that.
I would change legitForm to:
//create legitForms equal to the length of questions
const legitForms = [];
for (i in questions) legitForms.push(
{
name: '', // this will be equal to question string
answer: '',
description: '',
duration: '',
}
);
and in template:
<div v-for="(question, index) in questions" :key="index">
<form #submit.prevent="legitStore(question)" method="post">
<div class="row">
<div class="col-md-12">
<p>
<strong>{{question}}</strong>
</p>
</div>
<div class="col-md-6">
<label for="answer">Answer</label>
<select class="mb-1 field" v-model="legitForms[index].answer" name="answer" id="answer">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<div class="col-md-6">
<label for="duration">Duration</label>
<input class="field" v-model="legitForms[index].duration" type="text">
</div>
<div class="col-md-12">
<label for="description">Description</label>
<textarea style="height: 190px;" type="text" cols="5" rows="10" id="address" class="mb-1 field" v-model="legitForms[index].description"></textarea>
</div>
</div>
<button type="submit" class="saveButtonExperience float-right btn btn--custom">
<span class="text">Save</span>
</button>
</form>
</div>
<div v-for="(question, index) in questions" :key="index">
In your template you iterate through questions and within this tag render object legitForm it all questions will refer to the same 1 object that's why all question have the same data.
You should have had create an array of question contains it own question's content like
<template>
<div v-for="(question, index) in questions" :key="index">
<form #submit.prevent="legitStore(question)" method="post">
...
<div class="col-md-12">
<p>
<strong>{{question.id}}</strong>
</p>
</div>
...
<select class="mb-1 field" v-model="question.answer" name="answer" id="answer">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
...
</form>
</div>
</template>
<script>
class QuestionForm {
// pass default value if you want
name = ''
answer = ''
description = ''
duration = ''
id = ''
constructor(form) {
Object.assign(this, form)
}
}
function initQuestions(num) {
// can generate a Set object cause question are unique
return Array.from({ length: num }, (v, i) => i).map(_j => new QuestionForm())
}
export default {
data() {
return {
questions: initQuestions(5), // pass number of question you want to generate
}
}
}
</script>
I am trying to use inifinite-scroll directive for angularJS. The examples show usage of div inside the div, but in my case I'm trying to use it in a table. Here is my html:
<div class="scrolling-table-body">
<table class="table table-bordered table-hover table-list">
<thead search-table-header data-table="duplicatesTable"
data-search="sort(column)"
data-show-row-selector="true"
data-hide-sorting-indicator="true"
data-row-selector-click="selectAllRows(allSelected)"
data-column="column">
</thead>
<tbody infinite-scroll="loadMore()">
<tr ng-repeat="row in duplicatesArray"
ng-click="selectedDuplicateIndex=$index;"
ng-class="{selected: $index === selectedDuplicateIndex}">
<td style="text-align:center;">
<input type="checkbox"
name="checkRow"
ng-model="row.isSelected"
ng-change="selectRow(row, $index);" />
</td>
<td>
<span ng-if="row.barcode>0">{{row.barcode}}</span>
<span>{{$index}}</span>
<span class="pull-right">
<i class="fa fa-trash"
style="color:red;"
ng-click="removeRow($index)"
title="#Labels.delete"></i>
</span>
</td>
<td>
<div class="col-xs-12">
<input type="text"
name="assetNo"
id="assetNo"
ng-model="row.assetNo"
class="form-control"
ng-change="checkAssetNo(row)"
ng-maxlength="100"
sm-duplicate-validator
validate-duplicates="true"
error-message="row.errorMessage"
api-method="api/rentalEquipments/checkForDuplicate"
primary-key-value="row.equipmentId"
ng-model-options="{ debounce: { default : 500, blur: 0 }}" />
</div>
</td>
<td>
<input type="text"
name="serialNo1"
id="serialNo1"
ng-model="row.serialNo1"
class="form-control"
ng-maxlength="100" />
</td>
The above is used inside the modal form (bootstrap modal).
I initially load 10 rows into my duplicatesArray and I have the following code for loadMore function:
$scope.loadMore = function () {
const last = $scope.duplicatesArray.length;
if (last < $scope.numberOfDuplicates) {
for (let i = 1; i <= 10; i++) {
self.logInfo("Loading more duplicates...");
const newEquipment = {
equipmentId: (last + i) * -1,
descrip: self.model.descrip,
homeShopId: self.model.homeShopId,
ruleId: self.model.ruleId,
manufacturerId: self.model.manufacturerId,
modelId: self.model.modelId,
typeId: self.model.typeId,
levelId: self.model.levelId,
equipSize: self.model.equipSize,
bootMm: self.model.bootMm,
bindingManufacturerId: self.model.bindingManufacturerId,
bindingModelId: self.model.bindingModelId,
cost: self.model.cost,
bindingCost: self.model.bindingCost,
unitCost: self.model.unitCost,
errorMessage: "",
duplicateForm: true,
duplicatedId: self.model.equipmentId,
isDuplicate: true,
barcode: 0,
assetNo: "",
serialNo1: "", serialNo2: "", serialNo3: "", serialNo4: "",
isSelected: false
};
$scope.duplicatesArray.push(newEquipment);
}
}
};
There is currently an issue in this js code (I moved check for last < numberOfDuplicates before the loop thinking it may be the issue).
When I open my modal I see 20 items in the list and when I scroll I don't see more items.
Do you see what am I doing wrong?
Also, does it matter that I have the following markup for the modal:
<ng-form name="equipmentDuplicatesForm">
<div class="modal-body">
<div id="fixed-header-table">
<div class="fixed-header-bg">
</div>
<div class="scrolling-table-body">
table goes here
</div>
<div class="modal-footer hundred-percent padTop padBottom">
<button type="button" class="btn btn-warning"
data-dismiss="modal" aria-hidden="true"
ng-click="$dismiss()">
#Labels.cancel
</button>
</div>
</ng-form>
I'm trying to add textfield dynamically with angular 2 and it works as expected,but couldn't managed to get this json object :
exemple of json object which i want to get when i clic the submit button :
{fullname:"toto",etapes:[{etape:"sometext"},{etape:"sometext"}]}
heres the HTML CODE:
<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm)">
<div style="padding:15px" class="container" style="width: 1027px !important;">
<div class="itineraire">
<div class="panel panel-default" style="width:100%">
<div class="panel-heading panelcolor"><span style="font-size: 15px;font-weight: bold">Itinéraire</span></div>
<div class="panel-body panelcolor">
<input type="text" formControlName="fullname" class="form-control" placeholder="fullename"
name="Location" >
<div formArrayName="myArray">
<div *ngFor="let myGroup of myForm.controls.myArray.controls; let i=index">
<div [formGroupName]="i">
<span *ngIf="myForm.controls.myArray.controls.length > 1" (click)="removeDataKey(i)" class="glyphicon glyphicon-remove pull-right"
style="z-index:33;cursor: pointer">
</span>
<!--[formGroupName]="myGroupName[i]"-->
<div [formGroupName]="myGroupName[i]">
<div class="inner-addon left-addon ">
<i class="glyphicon marker" style="border: 5px solid #FED141"></i>
<input type="text" style="width:50% !important" formControlName="etape" class="form-control" placeholder="Exemple : Maarif, Grand Casablanca"
name="Location" (setAddress)="getAddressOnChange($event,LocationCtrl)"><br/>
</div>
</div>
<!--[formGroupName]="myGroupName[i]"-->
</div>
<!--[formGroupName]="i" -->
</div>
</div>
<br/>
<a (click)="addArray()" style="cursor: pointer">+ Ajouter une ville étape</a>
<input type="text" style="width:30%" #newName id="newName" [hidden]="true">
</div>
</div>
<button type="submit" (click)="save()">save</button>
</form>
Component.ts :
initArray(nameObj:any) {
return this._fb.group({
[nameObj]: this._fb.group({
etape: [''],
gmtDate:[''],
})
});
}
addArray(newName:string) {
const control = <FormArray>this.myForm.controls['myArray'];
this.myGroupName.push(newName);
control.push(this.initArray(newName));
}
ngOnInit() {
this.myForm = this._fb.group({
myArray: this._fb.array([
this._fb.group({
test: this._fb.group({
etape: [''],
gmtDate:['']
})
}),
])
});
}
save(){
console.log(myObject);
}
so, what are the changes that i have to do in my code to get the like the Json object above, when i click the submit button, please help i got stuck on this.
If you want:
{fullname:"toto",etapes:[{etape:"sometext"},{etape:"sometext"}]}
your form should looks like: (we dont need the formGroupName )
this.myForm = this._fb.group({
fullname: ['', Validators.required ],
etapes: this._fb.array([
this._fb.group({
etape: ['']
},
this._fb.group({
etape: ['']
},
...
)
])
});
So, to have it dynamically, your initArray()should be like:
initArray() {
return this._fb.group({
etape: ['']
});
}
and adding an array should looks like:
addArray(newName:string) {
const control = <FormArray>this.myForm.controls['etapes']
control.push(this.initArray());
}
NB: You dont need the (click)="save()" anymore in the submit button, since you already call it when submit.
a working plunker: http://plnkr.co/edit/Nq4jrC5g0tfxE0NCTfoQ?p=preview
HI can you try like this
save(){
console.log(this.myForm.value);
}
#imsi imsi
you have
<div [formGroupName]="myGroupName[i]"> //WRONG
<div [formGroupName]="myGroup[i]"> //<--it's myGroup
I am using below code.
HTML code
<form name="profInfoForm" ng-submit="saveInfo(profInfoForm)" novalidate>
<div class="wrapper">
<div class="container">
<section class="main-ctr">
<div class="error-msg" ng-show="showErrorMsg">
<div class="text">Please fix the validation error(s) below and try again.<br />{{serviceErrorMsg}}</div>
</div>
<div ng-include="'views/header.html'"></div>
<main class="sm-main">
<section class="fix-section">
<h1>Professional information</h1>
<div class="lg-form">
<div class="form-group">
<label class="text-label lg-label">Salutation</label>
<div class="select-wrap lg-select">
<select class="form-input select" name="salutation" required ng-init="profInfo.salutation = item[0]"
ng-options="item.name as item.name for item in salutations"
ng-model="profInfo.salutation">
</select>
<div class="error" ng-show="profInfoForm.$submitted || profInfoForm.salutation.$touched">
<span ng-show="profInfoForm.salutation.$error.required">Required Field</span>
</div>
</div>
</div>
</div>
</section>
<div class="clear"></div>
<div class="button-ctr">
<button class="button" ng-class="profInfoForm.$valid ? 'active' : 'disable'">Next</button>
</div>
</main>
</section>
</div>
</div>
<div id="loading" ng-if="showLoader">
<img src="images/loader.gif" id="loading-image">
</div>
</form>
My constant.js
.constant('APP_CONSTANTS', {
SALUTATIONS: [{ id: 0, name: 'Select' }, { id: 1, name: 'Mr.' }, { id: 2, name: 'Mrs.' }, { id: 3, name: 'Miss' }, { id: 4, name: 'Dr.' }, { id: 5, name: 'Ms'}]
})
Controller code is given below
.controller('professionalInfoCtrl', ['$rootScope', '$scope', '$state', 'globalService', 'APP_CONSTANTS', 'dataServices', function ($rootScope, $scope, $state, globalService, APP_CONSTANTS, dataServices) {
$scope.showLoader = false;
$scope.profInfo = userData;
$scope.salutations = APP_CONSTANTS.SALUTATIONS;
}])
I want to set default value of Salutation drop down list.
For this I am using ng-init but this is not working. I did not to find out the problem.
Please consider using ui-select of AngularUI:
<ui-select ng-model="$parent.company">
<!-- using $parent - https://github.com/angular-ui/ui-select/issues/18 -->
<ui-select-match>{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="company in companies>{{company.name}}</ui-select-choices>
</ui-select>
I resolved my issue by using below code
<select class="form-input select" ng-model="profInfo.salutation" required
ng-options="item.name as item.name for item in salutations">
<option value="">Select salutation...</option>
</select>
<div class="error" ng-show="profInfoForm.$submitted || profInfoForm.salutation.$touched">
<span ng-show="profInfoForm.salutation.$error.required">Required Field</span>
</div>
As simple I am using
<option value="">Select salutation...</option>
Thanks everyone.
Objective
I have the desire to create a Claims Form. This claims form must support the following:
Add(create) a Claim Line
Store(read) all Claim Lines
Edit(update) a Claim Line
Delete(destroy) a Claim Line
Display a variable number of fields, based on user selection
Requirement #5 was handled here
Retrieve boolean value from selected array object
Issues
The current code has a broken edit and update process, I know I have an issue in binding my data to the appropriate select list, but I cannot see it.
Desired Result
The answer from the above SO question has to remain intact, if possible.
When the user add a claim line the form should revert to its state onLoad.
When the user edits a claim line, it should rebuild the form to accommodate the data
When the user updates the claim line it should update the line in the saved claim list
Javascript
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 a few issues here.
Your select arrays are declared as self.planTypes = .... self is a variable inside the constructor of myViewModel. You should be getting an exception that this point but something has declared a self variable to equal window.
Your selected... observables are also all on window scope, and not enclosed in myViewModel.
When you add a new claim line, I'm getting a javascript errors depending on what you select, like if expenseType is null.
Solution
I have created a top level namespace call Models and attached everything to that.
I have created an explicit class for the edit claim line. This allows you to added various help functions and observables without polluting the claimlines themselves.
I have changed all the options bindings to remove the Id parameter. I find it a lot easier to work with the object instead of constantly looking up the array member.
I have implemented the Add and Update functions.
I also removed the datapicker jQuery call, as you need to do more work to update the observable when using this plugin. DatePicker plugin and Knockout do not work side-by-side without assistance ( ie custom binding ).
I also added the letters E and R ( Edit and Remove ) in your claim line's buttons as I wasn't getting 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>
Javascript
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();
});