0
votes

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,
    };
}
1
@RoyJ ive applied that option in the select data-bind in the view and it now returns the following error: Uncaught TypeError: parent.counties is not a function This occurs in the read within the ko.Computed function. This only seems to occur when I try to apply a .extend to this computed.Terry Delahunt
It doesn't look like there is a counties defined in the top level, which is what parent is pointing to, right?Roy J
@RoyJ parent refers to self from the ViewModel function. I've debugged it and parent.counties() does seem to contain data. In ViewModel(data) there is an array called counties that contains the data used to populate the dropdown options. The dropdown works perfectly in the sense that it is populated with all required options and the recorded option is selected upon page load. All I want is that it recognise when the 'Choose...' is selected and indicate that an option must be chosen. Seems like it should be very simple but its driving me nuts!Terry Delahunt
Still no luck resolving this with the select dropdown, so I went with adding a hidden field and binding it to the countyId value used in the computed county object. So when the dropdown changes, the value of the hidden field changes with it, and that field has a required extender on it. So to all intents and purposes, the validation looks and acts like it belongs to the dropdown. Its a hack but gets me round the problem. Thks for your help @RoyJ :)Terry Delahunt

1 Answers

0
votes

you need to use select binding in different way. (with optionsValue)

update selected value into countyId instead of county then you can get county as computed observable depends on countyId

var data = { counties: [{ countyId: 1, name: 'Carlow' },{ countyId: 2, name: 'Donegal' },{ countyId: 3, name: 'Galway' },{ countyId: 4, name: 'Kildare' },{ countyId: 5, name: 'Laois' }], countyId: 2 };

function vm(data) {
  var self = this;
  self.counties = ko.observable(data.counties);
  self.countyId = ko.observable(data.countyId).extend({ required: true });
  self.contry = ko.computed(function() {
    return ko.utils.arrayFirst(this.counties(), i => i.countyId == this.countyId());
  }, self);
}

ko.applyBindings(new vm(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js"></script>
<select data-bind="options: $root.counties, optionsText: 'name', optionsValue: 'countyId', value: countyId, optionsCaption: 'choose..'"></select>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>