Hicki
Hicki

Reputation: 167

Knockout bound select dropdown with default text and two way value binding

I have a select control bound to a Typescript viewmodel, the select has a default option ("Select...") that should be displayed on page load. If the viewmodel is passed an initial period, this should then override the default as the selected option.

I can pass an initial period to the model and via the callback use that on the parent model to save an entry on the form. However, the value binding fails to work with the control and the "Select..." text remains displayed in the control as opposed to the initial period label being the selected option.

View

<div id="customPeriodSelection" data-bind="with: customPeriodSelection">
<select id="periodSelect" class="form-control" data-bind="value: selectedOption, event: { change: onChange }">
    <option>@Labels.SelectDD</option>
    <!--ko foreach: {data: customGroups}-->
    <optgroup data-bind="attr: { label: label}">
        <!--ko foreach: {data: customPeriods}-->
        <option data-bind="text: $data.label, value: $data"></option>
        <!-- /ko -->
    </optgroup>
    <!-- /ko -->
</select>

Typescript

export class CustomPeriod {
    constructor(
        public start: Iso8601DateString,
        public end: Iso8601DateString,
        public label: string) { }

    identifier = () => {
        return this.start + "-" + this.end;
    }

    static mapCustomPeriod = (period: any) => {
        return new CustomPeriod(
            period.startDate,
            period.endDate,
            period.label
        )
    }
}

export class CustomPeriodSelection {
    customFrequencyId: number;
    customPeriods: KnockoutObservableArray<CustomPeriod> = ko.observableArray([]);
    selectedOption: KnockoutObservable<any> = ko.observable();

    constructor(
        customFrequencyId: number,
        initialPeriod: any,
        public callback: (customPeriod: CustomPeriod) => void
    ) {
        var base = this;
        this.customFrequencyId = customFrequencyId;

        Verco.Services.GetData("GetPeriods", {
            frequency: customFrequencyId
        },
            function (result: any[]) {
                if (result.length > 0) {
                    _.each(result, function (customPeriod: any) {
                        base.customPeriods.push(CustomPeriod.mapCustomPeriod(customPeriod));
                    })
                }
            });

        if (initialPeriod !== null) {
            var mappedPeriod = CustomPeriod.mapCustomPeriod(initialPeriod);
            this.selectedOption(mappedPeriod);
            this.callback(this.selectedOption());
        }
    }

    customGroups: KnockoutComputed<any[]> = ko.computed(() => {

        var groupedList = _.groupBy(this.customPeriods(), function (customPeriod: CustomPeriod) {
            return Verco.Format.FormatDate(customPeriod.end, DateFormats.Year);
        });

        return _.map(groupedList, function (customPeriod: any) {
            return {
                label: Verco.Format.FormatDate(customPeriod[0].end, DateFormats.Year),
                customPeriods: customPeriod
            }
        });
    });

    onChange = () => {
        if (this.callback !== null) {
            this.callback(this.selectedOption());
        }
    };
}

The initialPeriod passed in

{customFrequencyId: 1008, startDate: "2017-01-01T00:00:00Z", endDate: "2017-01-28T00:00:00Z", label: "P1 2017"}

Mapped to...

CustomPeriod {start: "2017-01-01T00:00:00Z", end: "2017-01-28T00:00:00Z", label: "P1 2017"}

So my question is.. How when passing in a period, mapping it to the type above and assigning it to the "selectedOption" observable, do I make the control display the correct selection? Do all of the individual properties on the CustomPeriod need to be observable?

I've had a look at THIS from Ryan Niemeyer as he manipulates the Options as observables, do I need to replace the Computed with a similar way of building the overarching array? I've tried, but without success..

Upvotes: 0

Views: 1475

Answers (2)

Jason Spake
Jason Spake

Reputation: 4304

I think the problem you're having is that the default value you're trying to select isn't in the list of available options. Every time you call mapCustomPeriod you're creating a new object. Even if that object's contents are identical to the contents of one in your list the objects themselves are two completely different object instances and are not equivalent.

Try setting your default after the options list has been populated, and setting it by looping through the existing options to find the one that matches whatever criteria you want to use to define "equal".

Verco.Services.GetData("GetPeriods", {
        frequency: customFrequencyId
    },
        function (result: any[]) {
            if (result.length > 0) {
                _.each(result, function (customPeriod: any) {
                    base.customPeriods.push(CustomPeriod.mapCustomPeriod(customPeriod));
                });
                base.setValueByLabel(initialPeriod.label); //set default via label
            }
        });
...

this.setValueByLabel = function(label){
    for(var i=0; i<base.customGroups().length; i++){
        var group = base.customGroups()[i];
        for(var j=0; j<group.customPeriods.length; j++){
            var item = group.customPeriods[j];
          if(item.label === label){
                base.selectedOption(item);
              return;
          }
      }
  }
}

Upvotes: 0

4imble
4imble

Reputation: 14416

Handling an empty state should be as simple as setting the optionsCaption on the options binding.

<select data-bind="options: availableCountries,
                   optionsText: 'countryName',
                   value: selectedCountry,
        >>-------> optionsCaption: 'Choose...'"></select>

http://knockoutjs.com/documentation/options-binding.html

Upvotes: 0

Related Questions