I have a form on which a user can input questions via a textarea. By clicking a button, he can dynamically generate more text fields to input questions into. I'm using Knockout with a client-side viewmodel to which the questions are bound.
My simplified view (Razor) is as follows:
<div data-bind="foreach: openQuestions">
<div class="form-group">
<input type="hidden" data-bind="value: currentIndex" name="openQuestions.Index" />
<textarea class="form-control" rows="3" data-bind="value: question, attr: { name: 'openQuestions[' + $data.currentIndex + '].Question' }"/>
</div>
</div>
@Html.ValidationMessageFor(m => m.OpenQuestions)
Part of the Knockout VM:
function OpenQuestion() {
var self = this;
self.currentIndex = GetRandomIndex();
self.question = ko.observable();
}
function QuestionViewModel() {
var self = this;
self.openQuestions = ko.observableArray();
self.addOpenQuestion = function () {
self.openQuestions.push(new OpenQuestion());
}
}
There are two validations that should occur:
I'm having difficulty hooking up the jQuery validator to validate the minimum length of the array. The problem, I think, is when the page is loaded, there is no element with a name of 'OpenQuestions' since there aren't questions yet. If you would submit at this point, the validator can't find any elements to validate and simply assumes everything is valid, which it is not.
I tried creating a custom "minimum length array required" validation attribute, but I ran into the same problem - which element do you hook it up to?
Basically this method checks if it can find any elements like the provided name (e.g. 'openquestions', but 'openquestions[123456]' will work too) and compare that length to the minimum required length.
$.validator.addMethod("minarraylength",
function (value, element, parameters) {
var minLength = parameters["minlength"];
if (element) {
var elementName = $(element).attr("name").toLowerCase();
var actualLength = $("input").filter(function () {
var currentName = $(this).attr("name");
return currentName != undefined && currentName.toLowerCase().indexOf(elementName);
}).length;
return actualLength >= minLength;
}
return true;
}
);
Combining this with a hidden field like so
@Html.HiddenFor(m => m.OpenQuestions)
will trigger validation, but will cause the model binder to fail on submit since for some reason it takes the value out of the hidden field and ignores everything else.
So the question is, how can I hook up the jQuery validator into my dynamically generated list, ensure the model binding works on postback, but prevent empty lists from being submitted?
Thanks in advance!
You can simply extend your KO objects, see below:
Step 1: Create a custom KO rule for array at least 1 requirement
ko.validation.rules["atLeastOne"] = {
validator: function(value, validate) {
if (validate && Array.isArray(value)) {
return !!value.length;
}
return true;
},
message: "Please add at least one."
};
Step 2 Update your code:
function OpenQuestion() {
var self = this;
self.currentIndex = GetRandomIndex();
self.question = ko.observable().extend({
required: true,
minLength: 1
});
}
function QuestionViewModel() {
var self = this;
self.openQuestions = ko.observableArray().extend({
atLeastOne: {
message: "Please add at least one."
}
});
self.addOpenQuestion = function () {
self.openQuestions.push(new OpenQuestion());
}
}