Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Knockout 'with' binding and select2 in jQuery dialog

Problem:

The select2 jQuery plugin does not work when used on a jQuery dialog nested under an element that uses the knockout with databinding. Remove the with binding and the select2 works fine. If with is bound to a nested property then it stops working.

Background:

So I must have battled for the best part of 3 hours trying to get select2 to work on a jQuery dialog form....talk about pi$$ing up the proverbial wrong tree, I thought it was purely jQuery dialog and select2. It probably worked from the very start with the _allowInteraction fix. Until I broke the problem right down to simple steps and teh cause started to reveal itself. Problem is with the with binding.

Disclaimer

Apologies as I work for an asinine company that blocks jsFiddle. Also I have broken down my implementation for illustration purposes as the actual model is quite large.

// models

function Department() {
  this.name         = ko.observable('dept1');
  this.selectedTeam = ko.observable( new Team() );
}
    
function Team() {
  this.name = ko.observable('team1');
}
    
function MainModel() {
  this.department = new Department();
  this.showTeam   = function() { 
    $('#addTeamDialog').dialog('open');
  };
}

// setup

ko.applyBindings( new MainModel() );
    	
$('#addTeamDialog').dialog({
  // fix allow select2 to work on the jq dialog
  _allowInteraction: function (event) {
    return !!$(event.target).is(".select2-input") || this._super(event);
  }		
});
    	
$('#someList').select2({
  data: [
    { id: 0, text: 'enhancement' },
    { id: 1, text: 'bug' },
    { id: 2, text: 'duplicate' },
    { id: 3, text: 'invalid' },
    { id: 4, text: 'wontfix' }
  ]
});
<link href="http://code.jquery.com/ui/1.11.4/themes/ui-lightness/jquery-ui.css" rel="stylesheet"/>
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet" />

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.min.js"></script>

<button data-bind="click: showTeam">Add Team</button>

<div id="addTeamDialog">
  <fieldset data-bind="with: department">
    
    <div class="lite-dialog-field">
      <div class="label">
        <span data-bind="text: name"></span>
      </div>
      <div class="field">
        <input type="hidden" id="someList" />
      </div>    
    </div>
        
  </fieldset>
</div>

Removing the data-bind on the fieldset and select2 works fine.

When the data-bind on the fieldset is set to department the select2 works fine.

When the data-bind on the fieldset is set to department.selectedTeam select2 doesn't working.

like image 484
Alan Alcock Avatar asked Sep 28 '22 22:09

Alan Alcock


1 Answers

When you work with Knockout, it's highly recommended to wrap external libraries such as select2 in bindings. While you only initialize them once, bindings such as with, template or foreach can modify the DOM at any time after that.

You face the danger of either

  1. initializing select2 too early when Knockout hasn't rendered anything yet, or
  2. Knockout throwing away and re-rendering the markup at a later point, so that your select2 is suddenly not bound any longer

For example, this would happen when Department.selectedTeam is changed.

I've found a quick and dirty select2 binding from Knockouts' rniemeyer himself here. Other than that, I only changed the select2 markup into a standard <select> and made MainModel.department into a proper observable for the sake of consistency and safety.

ko.bindingHandlers.select2 = {
    init: function(element, valueAccessor) {
      var options = ko.toJS(valueAccessor()) || {};
      setTimeout(function() { 
          $(element).select2(options);
      }, 0);
    }
};

// models

function Department() {
  this.name         = ko.observable('dept1');
  this.selectedTeam = ko.observable( new Team() );
};
    
function Team() {
  this.name     = ko.observable('team1');
  this.values   = ["red", "grey", "blue"];
  this.selected = ko.observableArray(["blue"]);
};
    
function MainModel() {
  this.department = ko.observable( new Department() );
  this.showTeam   = function() { 
    $('#addTeamDialog').dialog('open');
  };
};

// setup

ko.applyBindings( new MainModel() );
    	
$('#addTeamDialog').dialog({
  // fix allow select2 to work on the jq dialog
  _allowInteraction: function (event) {
    return !!$(event.target).is(".select2-input") || this._super(event);
  }		
});
select {
  width: 200px;
}
<link href="http://code.jquery.com/ui/1.11.4/themes/ui-lightness/jquery-ui.css" rel="stylesheet"/>
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.min.js"></script>

<button data-bind="click: showTeam">Add Team</button>

<div id="addTeamDialog">
  <fieldset data-bind="with: department().selectedTeam">
    
    <select data-bind="options: values,
                       selectedOptions: selected, 
                       select2: { placeholder: 'pick some colors' }">
    </select>
        
  </fieldset>
</div>
like image 130
janfoeh Avatar answered Nov 15 '22 09:11

janfoeh