gzost gzost - 3 months ago 44
Javascript Question

Re-init Materialize.css select box after removal from Knockout.js options array

I have a select box where the options and selection are handled via Knockout.js. I want to style this using Materialize CSS.

This works OK for the initial display of the select box, and when options are added to the Knockout.js 'options' observableArray by using the 'optionsAfterRender' binding to (re)initialize after each option has been added (wasteful, but works).

When removing an option, Knockout.js does not provide anything similar to 'optionsAfterRender', so there is no obvious way to trigger a re-initialization of the Materialize CSS magic.

Question: Are there any non-insane options which you can see?

Code:

<select data-bind="

options: options,
optionsText: function(item) { return optionsText[item] },
value: displayedValue,

optionsAfterRender: function (option, item) {
setTimeout(function() {
$(option.parentElement).material_select();
}, 0);
}
">
</select>


(The 'setTimeout' is necessary since otherwise the selected option is not picked.)

Answer

A custom binding handler is more appropriate for integrating a custom UI component like material_select with KnockoutJS. Here's one way to build such a handler:

ko.bindingHandlers["materializeSelect"] = {
  after: ['options'],
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Initial initialization:
    $(element).material_select();
    
    // Find the "options" sub-binding:
    var boundValue = valueAccessor();
    
    // Register a callback for when "options" changes:
    boundValue.options.subscribe(function() {
      $(element).material_select();
    });
  },
  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    
  }
};

function RootViewModel() {
  var self = this, i = 2;
  self.options = ko.observableArray([{id: 1, txt: "Option A"}, {id: 2, txt: "Option two"}]);
  self.selectedOption = ko.observable(null);
  
  // For testing purposes:
  self.addOption = function() { self.options.push({id: ++i, txt: "Dynamic option " + i}); };
}

ko.applyBindings(new RootViewModel());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.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/materialize/0.97.4/js/materialize.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.4/css/materialize.min.css" rel="stylesheet"/>

<select data-bind="materializeSelect: { options: options },
                   options: options,
                   optionsText: 'txt',
                   value: selectedOption">
</select>

<button data-bind="click: addOption">Add option dynamically</button>

To be honest, I feel it's an issue/omission or perhaps even a bug in MaterializeCSS that it apparently doesn't notice select options changing. IIRC libraries like Select2 and Chosen do have this feature.

In any case, if MaterializeCSS would notice dynamically added options correctly, I'd still stuggest using a custom binding handler, only a much simpler one:

ko.bindingHandlers["materializeSelect"] = {
  after: ['options'],
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    $(element).material_select();
  },
  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Handle ViewModel changes of which MaterializeCSS needs custom
    // notification here.
  }
};
Comments