I have a dropdown list within my knockout app, markup is as follows:
View:
<select data-bind="options: $root.counties, optionsText: 'name', value: county, optionsCaption: '@PlanStrings.ChooseFromDropdownMessage'"></select>
I wish to validate this simply to ensure an option is selected. It renders in the browser like so:
<select data-bind="options: $root.counties, optionsText: 'name', value: county, optionsCaption: 'Choose...'">
<option value="">Choose...</option>
<option value="">Carlow</option>
<option value="">Cavan</option>
<option value="">Clare</option>
<option value="">Cork</option>
<option value="">Donegal</option>
<option value="">Dublin</option>
<option value="">Galway</option>
<option value="">Kerry</option>
<option value="">Kildare</option>
<option value="">Kilkenny</option>
<option value="">Laois</option>
<option value="">Leitrim</option>
<option value="">Limerick</option>
<option value="">Longford</option>
<option value="">Louth</option>
<option value="">Mayo</option>
<option value="">Meath</option>
<option value="">Monaghan</option>
<option value="">Offaly</option>
<option value="">Roscommon</option>
<option value="">Sligo</option>
<option value="">Tipperary</option>
<option value="">Waterford</option>
<option value="">Westmeath</option>
<option value="">Wexford</option>
<option value="">Wicklow</option>
</select>
The value county is a computed observable, code here:
/// <summary>Constructor function for site info view model</summary>
function SiteModel(data, parent) {
var self = this;
// some code etc
self.county = ko.computed({
read: function () {
return ko.utils.arrayFirst(
parent.counties(),
i => i.countyId() === self.countyId()
);
},
write: function (value) {
self.countyId(value === undefined ?
null : value.countyId());
}
});
}
SiteModel is called within the overall ViewModel() function.
I've implemented ko validation on a simple input text field (non computed) on the same page, so its not a more fundamental issue of setting up ko validation. I'm just doing something wrong re. validating this computed observable/dropdown.
I've tried all of the following:
self.countyId = ko.observable().extend({ required: { message: "You must select a county." } });
self.county.extend({ required: { message: "You must select a county." } });
self.countyId.extend({ required: { message: "You must select a county." } });
self.county = ko.computed({
read: function () {
return ko.utils.arrayFirst(
parent.counties(),
i => i.countyId() === self.countyId()
);
},
write: function (value) {
self.countyId(value === undefined ?
null : value.countyId());
}
}).extend({ required: { message: "You must select a county." } });
None of these seem to work. New to ko so at a loss with this. Any suggestions on how to validate this dropdown/computed observable?
I might add that every other aspect of this dropdown works correctly, in that its reads the value correctly when saving the page, and renders with the value from the model as the selected value, if the model contains a value.
##EDIT## Including the full View model code for completeness:
function ($, ko, validation, mapping, formatting, strings, global, base) {
/// <summary>Constructor function for top-level view model</summary>
function ViewModel(data) {
var self = this;
var map = {
ignore: ["RegistrationDate"],
sites: {
create: function (options) {
return new SiteModel(options.data, self);
}
}
};
mapping.fromJS(data, map, self);
self.industrySector = ko.computed({
read: function () {
return ko.utils.arrayFirst(
self.industrySectors(),
i => i.industrySectorId() === self.industrySectorId()
);
},
write: function (value) {
self.industrySectorId(value === undefined ?
null : value.industrySectorId());
}
});
self.isEditingNumberOfEmployees = ko.observable(false);
self.editNumberOfEmployees = () => self.isEditingNumberOfEmployees(true);
self.numberOfEmployeesFormatted =
ko.computed(() => {
var value = ko.unwrap(self.numberOfEmployees);
return value === null ?
strings.nullPlaceholder :
value;
});
self.isEditingTurnover = ko.observable(false);
self.editTurnover = () => self.isEditingTurnover(true);
self.turnoverFormatted =
ko.computed(() => {
var value = ko.unwrap(self.turnover);
if (value !== null)
{
var val_string = value.toString();
var value_parsed = parseFloat(val_string.replace(/[^\d\.]/g, ''));
}
return value === null ?
strings.nullPlaceholder :
formatting.formatDecimal(value_parsed);
});
self.isEditingYearEstablished = ko.observable(false);
self.editYearEstablished = () => self.isEditingYearEstablished(true);
self.yearEstablishedFormatted =
ko.computed(() => {
var value = ko.unwrap(self.yearEstablished);
return value === null ?
strings.nullPlaceholder :
value;
});
self.shouldShowLogoMessage = ko.observable(true);
var companyLogoId = data.logoFileId;
if (companyLogoId > 0)
{
self.shouldShowLogoMessage(false);
}
self.uploadLogo = function (data, event) {
var inputId = $(event.target).data("inputId");
$("#" + inputId).trigger("click");
};
self.liveSites = ko.computed(function () {
return ko.utils.arrayFilter(
self.sites(),
site => ko.unwrap(site.state) !== "Deleted"
);
});
self.addSite = function () {
var newSite = new SiteModel(
{
name: null,
address1: null,
address2: null,
address3: null,
countyId: null,
keyActivities: null,
state: "Added"
},
self
);
self.sites.push(newSite);
self.setTab(newSite.position());
}
self.setTab = function (tabIndex) {
// FIXME Investigate replacing with KO microtask when upgraded
// to KO 3.4
window.setTimeout(function () {
$("#sites_tab").tabs("option", "active", tabIndex);
}, 10);
};
self.siteCount = ko.computed(() => self.liveSites().length);
self.dirtyFlag = new ko.dirtyFlag(self, false);
}
var knockoutValidationSettings = {
insertMessages: true,
messagesOnModified: true,
};
ko.validation.init(knockoutValidationSettings, true);
ViewModel.prototype = base;
/// <summary>Constructor function for site info view model</summary>
function SiteModel(data, parent) {
var self = this;
mapping.fromJS(data, {}, self);
self.position = ko.computed(function () {
return parent.sites().indexOf(self);
});
self.hasData = function () {
return (self.name() !== "" ||
self.address1() !== "" ||
self.address2() !== "");
};
self.displayName = ko.computed({
read: function () {
if (self.name() === "" || self.name() === null) {
return strings.introductionSiteAutoName + " " + (parent.liveSites ?
parent.liveSites().indexOf(self) + 1 :
parent.sites().indexOf(self) + 1).toString();
}
return self.name();
},
write: function (value) {
self.name(value);
}
});
self.anchorRef = ko.computed(function () {
return "#tabs-" + (self.position() + 1).toString();
});
self.idRef = ko.computed(function () {
return "tabs-" + (self.position() + 1).toString();
});
self.stateModified = ko.computed(function () {
self.name();
self.address1();
self.address2();
if (self.state() === "Unchanged") {
self.state("Modified");
}
});
self.deleteSite = function () {
var pos = self.position();
// FIXME: Replace with proper dialog box
if (window.confirm(strings.introductionConfirmDeleteSiteMessage)) {
if (self.state() === "Added") {
// Site was never saved to DB so just remove it
parent.sites().splice(pos, 1);
}
self.state("Deleted");
if (pos > 0) pos--;
parent.setTab(pos);
}
}
self.county = ko.computed({
read: function () {
return ko.utils.arrayFirst(
parent.counties(),
i => i.countyId() === self.countyId()
);
},
write: function (value) {
self.countyId(value === undefined ?
null : value.countyId());
}
}).extend({ required: true });
self.countyHidden = ko.computed({
read: function () {
return ko.utils.arrayFirst(
parent.counties(),
i => i.countyId() === self.countyId()
);
},
write: function (value) {
self.countyId(value === undefined ?
null : value.countyId());
}
}).extend({ required: true });
self.name.extend({ required: { message: "You must enter a name." } });
}
return {
ViewModel: ViewModel,
};
}
countiesdefined in the top level, which is what parent is pointing to, right? - Roy J