Search code examples
knockout.jswebpackjquery-chosencommonjs

Knockout observable is not updating when Chosen dropdown value changes


We have been using Chosen library with RequireJs and KnockOut. Everything was working fine until we switched from RequireJS to commonjs and now using webpack to bundle. The issue is that the knockout observable does not get updated when we change the value in chosen dropdown.

Here's the javascript code that was working using RequireJs.

define(['knockout', 'text!./employee-setup.html', 'utils', 'panel-section', 'toastr', 'jquery', 'knockout-postbox', 'knockout-projections', 'chosen', 'jsteps'], function (ko, template, utils, PanelSection, toastr, $, _, _, _, jsteps) {
function EmployeeSetup(params) {
    var self = this;
    this.agentTypes = ko.observableArray();
    this.agentType = ko.observable();


    this.loadAgentTypes = function () {
        $.ajax({
            url: '/Employee/GetAgentTypes',
            method: 'POST',
            dataType: 'json',
            success: function (result) {
                if (utils.handleAjaxResult(result) && result.Data) {
                    self.agentTypes([]);

                    var agentType = [{ ID: "", Name: "" }];

                    $.each(result.Data, function (i, item) {
                        agentType.push({ID: item.ID, Name: item.Name});
                    });
                    self.agentTypes(agentType);
                    $('#agentType').chosen({ allow_single_deselect: true, width: '310px' });
                    $('#agentType').trigger("chosen:updated");
                } else {
                }

            },
            error: function () {
                toastr.error('Could not load agent types');
            }
        });
    };
    self.loadAgentTypes();
    };
 return { template: template, viewModel: EmployeeSetup };
});

The html for that component:

<div class="input-container" data-bind="">
     <select data-bind="value: agentType, options: agentTypes, optionsText: 'Name'" data-placeholder="Select Agent Type..." id="agentType" class="chosen-select sp-uin-dropdown" tabindex="2"> </select>
</div>

Here's the code using commonjs

var ko = require('knockout'),
    utils = require('utils'),
    PanelSection = require('panel-section'),
    toastr = require('toastr'),
    $ = require('jquery');
require('knockout-postbox');

function ViewModel(params) {
   var self = this;
   this.agentTypes = ko.observableArray();
   this.agentType = ko.observable();

   this.loadAgentTypes = function () {
   $.ajax({
       url: '/Employee/GetAgentTypes',
       method: 'POST',
       dataType: 'json',
       success: function (result) {
       if (utils.handleAjaxResult(result) && result.Data) {
              self.agentTypes([]);

              var agentType = [{ ID: "", Name: "" }];

              $.each(result.Data, function (i, item) {
                  agentType.push({ID: item.ID, Name: item.Name});
              });
              self.agentTypes(agentType);
              $('#agentType').chosen({ allow_single_deselect: true, width: '310px' });
              $('#agentType').trigger("chosen:updated");
            } else {
           }
        },
        error: function () {
           toastr.error('Could not load agent types');
        }
    });
  };
  self.loadAgentTypes();
}
module.exports = { viewModel: ViewModel, template: require('./template.html')      };

And it's using the same html file as above.

In the webpack.config.js we define the path to jquery and chosen.

It loads the chosen dropdown correctly. However, when I subscribe to observable it doesn't update value when dropdown changes. I only see the value from console once on initial load.

self.agentType.subscribe(function (value) {
    console.log('value', value);
}, this)

Few posts here in SO suggested to use bindingHandlers. I have tried this working code from JSFiddle in my application, but I only get the value from initial load.

Any suggestion on how to resolve this issue or what is causing this?


Solution

  • The issue was caused by webpack. In order to resolve the issue, my colleague wrote a custom bindingHandler.

    HTML code:

    <div class="input-container">
     <select data-bind="
                    value: agentType,
                    options: agentTypes, 
                    optionsText: 'Name',
                    dropdown: {
                        width: '310px',
                        allow_single_deselect: true
                    } "
                    data-placeholder="Select Agent Type..." id="agentType">
    </select>
    

    Custom bindingHandler:

    // a dropdown handler, which currently utilizes the Chosen library
    
    ko.bindingHandlers.dropdown = {
    
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext){
    
            // get chosen element
    
            var $element = $(element);
    
            // get options (if any) to pass to chosen when creating
    
            var options = ko.unwrap(valueAccessor());
    
    
    
            // NOTE: when using Chosen w/ webpack, the knockout bindings no longer
    
            // fired. This event handler is to remedy that. It watches the change
    
            // event for the underlying <select> (which chosen updates), and
    
            // updates the corresponding observables mapped to value and selectedOptions.
    
            // Only one should be bound, value for single select, selectedOptions for multi-select
    
            // binding direction: Knockout <- Chosen
    
            $element.on('change', function(e, item) {
    
                var valueProp = allBindings.has('value') && allBindings.get('value');
    
                var selectedOptionsProp = allBindings.has('selectedOptions') && allBindings.get('selectedOptions');
    
                if (item) {
    
                    if (allBindings.has('options')) {
    
                        var allOptions = ko.unwrap(allBindings.get('options'));
    
                        if (valueProp) {
    
                            // single select
    
                            if (ko.isObservable(valueProp)) {
    
                                if (!item.isMultiple) {
    
                                    if (item.selecting) {
    
                                        valueProp(allOptions[item.index]);
    
                                    } else {
    
                                        valueProp(null);
    
                                    }
    
                                }
    
                            }
    
                        }
    
                        if (selectedOptionsProp) {
    
                            // multi select
    
                            if (ko.isObservable(selectedOptionsProp)) {
    
                                if (item.isMultiple) {
    
                                    // handle multi select
    
                                    if (item.selecting) {
    
                                        // select
    
                                        selectedOptionsProp.push(allOptions[item.index]);
    
                                    } else {
    
                                        // deselect
    
                                        selectedOptionsProp.remove(allOptions[item.index]);
    
                                    }
    
                                }
    
                            }
    
                        }
    
                    }
    
                } else {
    
                    // this is triggered w/o args when the control is reset. This happens when deselecting during single-select
    
                    if (valueProp) {
    
                        // single select
    
                        if (item === undefined && ko.isObservable(valueProp)) {
    
                            valueProp(null);
    
                        }
    
                    }
    
                }
    
            });
            // handle updating the chosen component's UI when the underlying
    
            // options, selectedOptions or value changes
    
            // binding direction: Knockout -> Chosen
    
            ['options', 'selectedOptions', 'value'].forEach(function(propName){
    
                if (allBindings.has(propName)){
    
                   var prop = allBindings.get(propName);
    
                    if (ko.isObservable(prop)){
    
                        //console.log('subscribing to:', propName, ' for:', $element);
    
                        prop.subscribe(function(value){
    
                            if (value != null) {
    
                                //console.log('calling chosen:updated');
    
                                var options = ko.unwrap(allBindings.get('options'));
    
                                // console.log('got options:', options);
    
                                if (options) {
    
                                    if (options.indexOf(value) > -1) {
    
                                        // item is in options
    
                                        //        console.log('value is in options:', value);
    
                                    } else {
    
                                        // item is not in options, try to match ID
    
                                        options.some(function (item) {
    
                                            if (item.ID == value) {
    
                                                // update the obs. to the entire item, not the ID
    
                                                prop(item);
    
                                            }
    
                                        });
    
                                    }
    
                                }
    
                            }
    
                            $element.trigger('chosen:updated');
    
                        });
    
                    }
    
                }
    
            });       
    
            // add chosen css class (not sure this is needed)
    
            $element.addClass('chosen-select');
    
    
    
            // create chosen element, passing in options if any were specified
    
           if (typeof options === 'object') {
    
                $element.chosen(options);
    
            } else {
    
                $element.chosen();
    
            }
    
            $element.trigger('chosen:updated');
    
        }
    
    };