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?
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');
}
};