We have a view using Razor and Knockout.js that displays a form. Part of the form asks the user to enter a list of values, and we're using a ko.observablearray
to keep track of them. This list is represented as a bunch of text boxes, one per value, with a "Delete" button next to each box and a single "Add" button underneath all of them. It works similarly to the demo project at http://learn.knockoutjs.com/#/?tutorial=collections.
Our form is acting unexpectedly in two ways:
- When a delete button is clicked, it removes all values from the
ko.observablearray
, not just the one corresponding to what was clicked. - When the "Submit" button for the overall form is clicked, it adds a new element to the
ko.observablearray
instead of submitting the form to our server.
Why are we seeing this behavior? (I know that these are two separate issues, but I'm not sure if they're caused by the same underlying problem or not, which is why I'm posting them in one question.)
Here is our Razor view:
@model OurProject.Models.Input.InputModel
@{
ViewBag.Title = "Input";
}
<h2>Inputs</h2>
<div id="inputForm">
<!-- snip - lots of input elements to fill in that are bound to KO -->
<div>
@Html.LabelFor(model => model.POSTransactionCodes)
</div>
<div>
<span class="help-block">Separate values by commas.</span>
</div>
<div>
<ul data-bind="foreach: POSTransactionCodes">
<li><input data-bind="value: $data" /> <a href="#" data-bind="click: $root.removePOSTransactionCode">Delete</a></li>
</ul>
<button data-bind="click: addPOSTransactionCode">Add another POS Transaction Code</button>
@Html.ValidationMessageFor(model => model.POSTransactionCodes, null, new { @class = "help-inline" })
</div>
<!-- snip - more input elements -->
<button data-bind="click: save">Submit</button>
</div>
<script type="text/javascript" src='~/Scripts/jquery-1.8.2.min.js'></script>
<script type="text/javascript" src='~/Scripts/knockout-2.1.0.js'></script>
<script type="text/javascript" src='~/Scripts/OP/OP.js'></script>
<script type="text/javascript" src='~/Scripts/OP/Input/OP.Input.Input.Form.js'></script>
<script type="text/javascript" src='~/Scripts/OP/Input/OP.Input.Input.Data.js'></script>
<script type="text/javascript">
var elementToBindTo = $("#inputForm")[0];
OP.Input.Input.Form.init(elementToBindTo);
</script>
Here is our main piece of Knockout code, OP.Input.Input.Form.js:
extend(OP, 'OP.Input.Input.Form');
OP.Input.Input.Form = function (jQuery) {
//The ViewModel for the page
var ViewModel = function () {
var self = this;
//Fields
/* snip - lots of ko.observables() */
self.POSTransactionCodes = ko.observableArray([]); //is a list of transaction codes
/* snip - lots of ko.observables() */
//Set up with initial data
self.initialize = function () {
var c = function (data, status, response) {
if (status === "success") {
/* snip - lots of ko.observables() */
ko.utils.arrayPushAll(self.POSTransactionCodes, data.POSTransactionCodes);
self.POSTransactionCodes.valueHasMutated();
/* snip - lots of ko.observables() */
} else {
}
};
OP.Input.Input.Data.GetInput(c);
}
//When saving, submit data to server
self.save = function (model) {
var c = function (data, status, response) {
if (status === "success") {
//After succesfully submitting input data, go to /Input/Submitted
//in order to let MVC determine where to send the user next
window.location.href = "~/Input/Submitted";
} else {
}
};
OP.Input.Input.Data.SaveInput(model, c);
}
//Modifying POSTransactionCodes array
self.removePOSTransactionCode = function (POScode) {
self.POSTransactionCodes.remove(POScode)
}
self.addPOSTransactionCode = function () {
self.POSTransactionCodes.push("");
}
};
//Connect KO form to HTML
return {
init: function (elToBind) {
var model = new ViewModel();
ko.applyBindings(model, elToBind);
model.initialize();
}
};
} ($);
Here is OP.Input.Input.Data.js:
extend(OP, 'OP.Input.Input.Data');
OP.Input.Input.Data = {
GetInput: function (callback) {
$.get("/API/Input/InputAPI/GetInputModel", callback);
},
SaveInput: function (input, callback) {
$.ajax({
url: "/API/Input/InputAPI/SaveInput",
type: "post",
data: input,
complete: callback
});
}
};