Knockout.js - Subscription to observable selected value in cascading select list firing twice

31 Views Asked by At

I have two templates for cascading select lists implemented with Knockout.Js, named office and department. Each of the lists has a subscription to the selected Value. There are two ways the list and value are populated:

  1. During the page load, the view model gets the data from backend (done is asp.net). The current office, office list, current department and department list are sent as input, ajax call is made.

  2. When an office is selected from the office list, the subscription is triggered, which makes an ajax call to retrieve the list of departments for the newly selected office. Program then populates the department list and the selected department value with the corresponding response data.

I have put together the following code using knockout tutorials, and similar posts here. The functionality seems to work perfectly. However during debugging, I found the subscription to the department select value to be triggering twice when the value is updated due to change in office. I want to understand what's causing this, cause even though it's not causing any issue now, there's no telling it won't in the future.

I know that arrayPushAll to observable arrays can trigger the subscription multiple times, but I'm not using it. The script only sets the observable array, and the subscription is to the selected value, not the array.

office Html:

<select class="w-100" data-bind="options: $data.List,
    optionsValue: 'Value',
    optionsText: 'Text',
    value: $data.Value ">
</select>

department Html:

<select id = "departmentList" class="w-100" data-bind="options: $data.List,
    optionsValue: 'Value',
    optionsText: 'Text',
    value: $data.Value ">
</select>

script:

FillProperties is called from another script when the page first loads and a number of scripts are executed to render the data in UI. This is where the initial list and value is populated during page load. This function is not used when user selects any value from the list

function OfficeList() {
    var root = this;
    this.Value = ko.observable(null);
    this.FirstLoad = false;
    this.List = new Array();

    root.Value.subscribe(function (items) {
        if (!root.FirstLoad) { // don't do ajax call on first load
            $.ajax({
                url: _utilities.getAbsoluteURL("/Copy/GetDataForOfficeSelection"),
                data: { officeId: root.Value() },
                type: "GET",
                contentType: "application/json, UTF-8",
                dataType: "json",
                cache: false,
                async: false,
                success: function (response) {
            // response format:
            // {
            //     DefaultDepartmentId: 2,
            //     "DepartmentList":
            //        [
            //            { "Value": 1, "Text": "Dep 1" },
            //            { "Value": 2, "Text": "Dep 2" },
            //            { "Value": 3, "Text": "Dep 3" }
            //        ]
            // };

                    //insert default option for department list with specific message   
                    response.DepartmentList.unshift({
                        Value: null,
                        Text: "Select a department"
                    });
                    
                    //get the element by id, as the list templates are rendered from another script
                    var el = $("#departmentList");
                    ko.dataFor(el[0]).UpdatedByOffice = true;
                    ko.dataFor(el[0]).List(response.DepartmentList);
                    ko.dataFor(el[0]).Value(response.DefaultDepartmentId);
                },
                error: function () {
                    window._toast.error("Error retrieving data for selected office")
                },
                complete: function () { }
            });
        }

        root.FirstLoad = false; // first time load is complete
        root.Text(root.List.find(o => o.Value === root.Value()).Text);
        // some more stuff
    }, this);

    this.FillProperties = function (val) {
        root.OriginalValue = val.Value;
        root.OriginalText = val.Text;
        root.FirstLoad = true; // this function is called during page load, so flagging it

        // val format:
        // {
        //     "Text": "Office 1",
        //     "Value": 1,
        //     "List": [
        //         { "Text": "Office 1", "Value": 1 },  
        //         { "Text": "Office 2", "Value": 2 }, 
        //         { "Text": "Office 3", "Value": 3 }     
        //     ]
        // }

        if (val.List) {
            for (let i = 0; i < val.List.length; i++) {
                root.List.push({
                    Value: val.List[i].Value,
                    Text: val.List[i].Text
                });
            }
        }

        root.Value(val.Value || '');
    }
}

function DepartmentList() {
    var root = this;

    this.FirstLoad = false;
    this.DefaultDurationValue = null;
    this.TeamList = new Array();
    this.UpdatedByOffice = false;
    this.Text = ko.observable('');
    this.Value = ko.observable(null);
    this.List = ko.observableArray(new Array({ Value: null, Text: "Select a department" }));

    // this subscription gets triggered twice when selecting an office
    // once the item value is the new department value
    // next the item value is previous new department value

    root.Value.subscribe(function (item) {
        // console.log("DepartmentList value subscribe: ", item);  ---> this gets printed twice

        // don't do ajax call on first load
        if (!root.FirstLoad) { 
            // don't do ajax call if 
            // department is updated by changing office from office list 
            // or if default option is selected from department list 
            if (!root.UpdatedByOffice && root.Value()) { 
                $.ajax({
                    url: _utilities.getAbsoluteURL("/Copy/GetDataForDepartmentSelection"),
                    data: { departmentId: root.Value() },
                    type: "GET",
                    contentType: "application/json, UTF-8",
                    dataType: "json",
                    cache: false,
                    async: false,
                    success: function (response) {
                // response format:
                // {
                //     DefaultDurationValue: 10,
                //     "TeamList":
                //        [
                //            { "Value": 1, "Text": "Team 1" },
                //            { "Value": 2, "Text": "Team 2" },
                //            { "Value": 3, "Text": "Team 3" }
                //        ]
                // };

                        root.TeamList  = response.TeamList;
                        root.DefaultDurationValue = response.DefaultDurationValue;
                    },
                    error: function () {
                        window._toast.error("Error retrieving data for selected department")
                    },
                    complete: function () { }
                });
            }
            root.UpdatedByOffice = false;
            root.TeamList = root.Value() ? root.TeamList : new Array();
            root.DefaultDurationValue = root.Value() ? root.DefaultDurationValue : null;
        }

        root.FirstLoad = false;
        root.Text(root.List().find(l => l.Value === root.Value()).Text);
        // some more stuff
    }, this);

    this.FillProperties = function (val) {
        root.FirstLoad = true;
        
        // val format is same as before
        if (val.List) {
            for (let i = 0; i < val.List.length; i++) {
                root.List.push({
                    Value: val.List[i].Value,
                    Text: val.List[i].Text
                });
            }
        }

        root.Value(val.Value || '');
    }
}
1

There are 1 best solutions below

0
Kevin UI On

This happens to me all the time.

It may be a number to string conversion. In the HTML tag:

<option value='1'>

'1' can only be interpreted as a string because there is no such thing as:

<option value=1>

So when you plug in the number to the tag, the DOM will then automatically convert it to a string. Triggering a change event in your subscription.

When you load the values into the array, you are loading numbers. Try loading "1" "2" "3" as your values instead of 1 2 3. I bet your double fire stops.