achecopar
achecopar

Reputation: 460

Knockout: autocomplete on combobox binding not clearing the observable

I have an autocomplete combobox using both knockout and jquery ui libraries.

When you type something in that is not in the autocomplete list, the combobox gets blank and that is exactly what I want it to do, but it does not update my selectedOption value too (I would like to set it to null when the combobox is left blank)

I have tried adding a subscribe event to the selectedOption but knockout does not fire it in this case. I also tried with the valueAllowUnset property but it did not work either.

Thank you very much for your help!

Here is my snippet (it looks a bit ugly because I did not add CSS but it shows the problem):

function viewModel() {
  var self = this;

  self.myOptions = ko.observableArray([{
      Name: "First option",
      Id: 1
    },
    {
      Name: "Second option",
      Id: 2
    },
    {
      Name: "Third option",
      Id: 3
    }
  ]);

  self.selectedOption = ko.observable(3);
}

var myViewModel = new viewModel();
ko.applyBindings(myViewModel);
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script>

<html>

<body>
  <p> Combobox: </p>
  <select id="myCombo" data-bind="options: myOptions, optionsText: 'Name', optionsValue: 'Id', value: selectedOption, valueAllowUnset: true, combo: selectedOption"></select>

  <p> Option selected Id: </p>
  <texarea data-bind="text: selectedOption()"> </texarea>
</body>

</html>

<script>
  // ko-combo.js
  (function() {

    //combobox 
    ko.bindingHandlers.combo = {
      init: function(element, valueAccessor, allBindingsAccessor) {
        //initialize combobox with some optional options
        var options = {};
        $(element).combobox({
          select: function() {
            var observable = valueAccessor();
            observable($(element).combobox('value'));
          }
        });

        //handle the field changing
        ko.utils.registerEventHandler(element, "change", function() {
          var observable = valueAccessor();
          observable($(element).val());
        });

        //handle disposal (if KO removes by the template binding)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
          $(element).combobox("destroy");
        });

      },
      //update the control when the view model changes
      update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        $(element).combobox('value', value);
      }
    };

  })();
</script>

<script>
// jquery.ui.combobox.js
/*!
 * Copyright Ben Olson (https://github.com/bseth99/jquery-ui-extensions)
 * jQuery UI ComboBox @VERSION
 *
 *  Adapted from Jörn Zaefferer original implementation at
 *  http://www.learningjquery.com/2010/06/a-jquery-ui-combobox-under-the-hood
 *
 *  And the demo at
 *  http://jqueryui.com/autocomplete/#combobox
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 */

(function( $, undefined ) {

   $.widget( "ui.combobox", {

       options: {
           editable: false
       },
      version: "@VERSION",
      widgetEventPrefix: "combobox",
      uiCombo: null,
      uiInput: null,
      _wasOpen: false,

      _create: function() {

         var self = this,
             select = this.element.hide(),
             input, wrapper;

         input = this.uiInput =
                  $( "<input />" )
                      .insertAfter(select)
                      .addClass("ui-widget ui-widget-content ui-corner-left ui-combobox-input")
                      .val( select.children(':selected').text() );

         wrapper = this.uiCombo =
            input.wrap( '<span>' )
               .parent()
               .addClass( 'ui-combobox' )
               .insertAfter( select );

         input
          .autocomplete({

             delay: 0,
             minLength: 0,

             appendTo: wrapper,
             source: $.proxy( this, "_linkSelectList" )

          });

         $( "<button>" )
            .attr( "tabIndex", -1 )
            .attr( "type", "button" )
            .insertAfter( input )
            .button({
               icons: {
                  primary: "ui-icon-triangle-1-s"
               },
               text: false
            })
            .removeClass( "ui-corner-all" )
            .addClass( "ui-corner-right ui-button-icon ui-combobox-button" );


         // Our items have HTML tags.  The default rendering uses text()
         // to set the content of the <a> tag.  We need html().
         input.data( "ui-autocomplete" )._renderItem = function( ul, item ) {

               return $( "<li>" )
                           .append( $( "<a>" ).html( item.label ) )
                           .appendTo( ul );

            };

         this._on( this._events );

      },


      _linkSelectList: function( request, response ) {

         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), 'i' );
         response( this.element.children('option').map(function() {

                  var text = $( this ).text();
                  
                  if ( this.value && ( !request.term || matcher.test(text) ) ) {
                      
                     return {
                           label: text.replace(
                              new RegExp(
                                  "(?![^&;]+;)(?!<[^<>]*)(" +
                                  $.ui.autocomplete.escapeRegex(request.term) +
                                  ")(?![^<>]*>)(?![^&;]+;)", "gi"),
                                  "<strong>$1</strong>"),
                           value: text,
                           option: this
                        };
                  }
              })
           );
      },
     
      _events: {

         "autocompletechange input" : function(event, ui) {
           
            var $el = $(event.currentTarget);

            if ( !ui.item ) {

               var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $el.val() ) + "$", "i" ),
               valid = false;

               this.element.children( "option" ).each(function() {
                     if ( $( this ).text().match( matcher ) ) {
                        this.selected = valid = true;
                        return false;
                     }
                  });

               if (!this.options.editable) {
                   if (!valid) {

                       // remove invalid value, as it didn't match anything
                       $el.val("");
                       this.element.prop('selectedIndex', -1);
                       //return false;

                   }
               }
            }

            this._trigger( "change", event, {
                  item: ui.item ? ui.item.option : null
                });

         },

         "autocompleteselect input": function( event, ui ) {
          
            ui.item.option.selected = true;
            this._trigger( "select", event, {
                  item: ui.item.option
            });


         },

         "autocompleteopen input": function ( event, ui ) {

            this.uiCombo.children('.ui-autocomplete')
               .outerWidth(this.uiCombo.outerWidth(true));
         },

         "mousedown .ui-combobox-button" : function ( event ) {
            this._wasOpen = this.uiInput.autocomplete("widget").is(":visible");
         },

         "click .ui-combobox-button" : function( event ) {

            this.uiInput.focus();

            // close if already visible
            if (this._wasOpen)
               return;

            // pass empty string as value to search for, displaying all results
            this.uiInput.autocomplete("search", "");

         }

      },

      value: function ( newVal ) {
         var select = this.element,
             valid = false,
             selected;

         if (!arguments.length) {
             selected = select.children(":selected");
             return selected.length > 0 ? selected.val() : null;
         } 

         select.prop('selectedIndex', -1);
         select.children('option').each(function() {
               if ( this.value == newVal ) {
                  this.selected = valid = true;
                  return false;
               }
            });

         if ( valid ) {
            this.uiInput.val(select.children(':selected').text());
         } else {
            this.uiInput.val( "" );
            this.element.prop('selectedIndex', -1);
         }

      },

      _destroy: function () {
         this.element.show();
         this.uiCombo.replaceWith( this.element );
      },

      widget: function () {
         return this.uiCombo;
      },

      _getCreateEventData: function() {

         return {
            select: this.element,
            combo: this.uiCombo,
            input: this.uiInput
         };
      }

    });


}(jQuery));

</script>

Upvotes: 0

Views: 741

Answers (1)

Serge K.
Serge K.

Reputation: 5323

Change the select event by change event in your binding handler :

function viewModel() {
  var self = this;

  self.myOptions = ko.observableArray([{
      Name: "First option",
      Id: 1
    },
    {
      Name: "Second option",
      Id: 2
    },
    {
      Name: "Third option",
      Id: 3
    }
  ]);

  self.selectedOption = ko.observable(3);
}

var myViewModel = new viewModel();
ko.applyBindings(myViewModel);
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script>

<html>

<body>
  <p> Combobox: </p>
  <select id="myCombo" data-bind="options: myOptions, optionsText: 'Name', optionsValue: 'Id', value: selectedOption, valueAllowUnset: true, combo: selectedOption"></select>

  <p> Option selected Id: </p>
  <texarea data-bind="text: selectedOption()"> </texarea>
</body>

</html>

<script>
  // ko-combo.js
  (function() {

    //combobox 
    ko.bindingHandlers.combo = {
      init: function(element, valueAccessor, allBindingsAccessor) {
        //initialize combobox with some optional options
        var options = {};
        $(element).combobox({
          change: function() {
            var observable = valueAccessor();
            observable($(element).combobox('value'));
          }
        });

        //handle the field changing
        ko.utils.registerEventHandler(element, "change", function() {
          var observable = valueAccessor();
          observable($(element).val());
        });

        //handle disposal (if KO removes by the template binding)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
          $(element).combobox("destroy");
        });

      },
      //update the control when the view model changes
      update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        $(element).combobox('value', value);
      }
    };

  })();
</script>

<script>
// jquery.ui.combobox.js
/*!
 * Copyright Ben Olson (https://github.com/bseth99/jquery-ui-extensions)
 * jQuery UI ComboBox @VERSION
 *
 *  Adapted from Jörn Zaefferer original implementation at
 *  http://www.learningjquery.com/2010/06/a-jquery-ui-combobox-under-the-hood
 *
 *  And the demo at
 *  http://jqueryui.com/autocomplete/#combobox
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 */

(function( $, undefined ) {

   $.widget( "ui.combobox", {

       options: {
           editable: false
       },
      version: "@VERSION",
      widgetEventPrefix: "combobox",
      uiCombo: null,
      uiInput: null,
      _wasOpen: false,

      _create: function() {

         var self = this,
             select = this.element.hide(),
             input, wrapper;

         input = this.uiInput =
                  $( "<input />" )
                      .insertAfter(select)
                      .addClass("ui-widget ui-widget-content ui-corner-left ui-combobox-input")
                      .val( select.children(':selected').text() );

         wrapper = this.uiCombo =
            input.wrap( '<span>' )
               .parent()
               .addClass( 'ui-combobox' )
               .insertAfter( select );

         input
          .autocomplete({

             delay: 0,
             minLength: 0,

             appendTo: wrapper,
             source: $.proxy( this, "_linkSelectList" )

          });

         $( "<button>" )
            .attr( "tabIndex", -1 )
            .attr( "type", "button" )
            .insertAfter( input )
            .button({
               icons: {
                  primary: "ui-icon-triangle-1-s"
               },
               text: false
            })
            .removeClass( "ui-corner-all" )
            .addClass( "ui-corner-right ui-button-icon ui-combobox-button" );


         // Our items have HTML tags.  The default rendering uses text()
         // to set the content of the <a> tag.  We need html().
         input.data( "ui-autocomplete" )._renderItem = function( ul, item ) {

               return $( "<li>" )
                           .append( $( "<a>" ).html( item.label ) )
                           .appendTo( ul );

            };

         this._on( this._events );

      },


      _linkSelectList: function( request, response ) {

         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), 'i' );
         response( this.element.children('option').map(function() {

                  var text = $( this ).text();
                  
                  if ( this.value && ( !request.term || matcher.test(text) ) ) {
                      
                     return {
                           label: text.replace(
                              new RegExp(
                                  "(?![^&;]+;)(?!<[^<>]*)(" +
                                  $.ui.autocomplete.escapeRegex(request.term) +
                                  ")(?![^<>]*>)(?![^&;]+;)", "gi"),
                                  "<strong>$1</strong>"),
                           value: text,
                           option: this
                        };
                  }
              })
           );
      },
     
      _events: {

         "autocompletechange input" : function(event, ui) {
           
            var $el = $(event.currentTarget);

            if ( !ui.item ) {

               var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $el.val() ) + "$", "i" ),
               valid = false;

               this.element.children( "option" ).each(function() {
                     if ( $( this ).text().match( matcher ) ) {
                        this.selected = valid = true;
                        return false;
                     }
                  });

               if (!this.options.editable) {
                   if (!valid) {

                       // remove invalid value, as it didn't match anything
                       $el.val("");
                       this.element.prop('selectedIndex', -1);
                       //return false;

                   }
               }
            }

            this._trigger( "change", event, {
                  item: ui.item ? ui.item.option : null
                });

         },

         "autocompleteselect input": function( event, ui ) {
          
            ui.item.option.selected = true;
            this._trigger( "select", event, {
                  item: ui.item.option
            });


         },

         "autocompleteopen input": function ( event, ui ) {

            this.uiCombo.children('.ui-autocomplete')
               .outerWidth(this.uiCombo.outerWidth(true));
         },

         "mousedown .ui-combobox-button" : function ( event ) {
            this._wasOpen = this.uiInput.autocomplete("widget").is(":visible");
         },

         "click .ui-combobox-button" : function( event ) {

            this.uiInput.focus();

            // close if already visible
            if (this._wasOpen)
               return;

            // pass empty string as value to search for, displaying all results
            this.uiInput.autocomplete("search", "");

         }

      },

      value: function ( newVal ) {
         var select = this.element,
             valid = false,
             selected;

         if (!arguments.length) {
             selected = select.children(":selected");
             return selected.length > 0 ? selected.val() : null;
         } 

         select.prop('selectedIndex', -1);
         select.children('option').each(function() {
               if ( this.value == newVal ) {
                  this.selected = valid = true;
                  return false;
               }
            });

         if ( valid ) {
            this.uiInput.val(select.children(':selected').text());
         } else {
            this.uiInput.val( "" );
            this.element.prop('selectedIndex', -1);
         }

      },

      _destroy: function () {
         this.element.show();
         this.uiCombo.replaceWith( this.element );
      },

      widget: function () {
         return this.uiCombo;
      },

      _getCreateEventData: function() {

         return {
            select: this.element,
            combo: this.uiCombo,
            input: this.uiInput
         };
      }

    });


}(jQuery));

</script>

Upvotes: 1

Related Questions