Constructing a light-saber - battling a bootstrap multi-select component

Struggling with a slow bootstrap-multiselect component? Learn why building a custom multi-select was a better solution for us, using just 80 lines of JavaScript, and how it improved performance and reduced dependencies. Dive into the details and elevate your coding game!

Constructing a light-saber - battling a bootstrap multi-select component

Lightsaber construction

In order to fully understand her weapon, a Jedi must build her own light-saber.

Analogous to that.

Before you introduce an external component into your codebase, spend a few hours trying to build the needed component yourself. That way you learn where the difficulties lie, what to look for in a good solution, and you see if the external dependency is worth it.

I was having trouble with a component at work.
We need a multi-select control in our user interface. We found one in the bootstrap-multiselect project.

It has all the features we wanted for the interface, save one. For historical reasons we want the list of options to be reordered based on the selection. In essence, we want all the selected items at the top.

We had introduced the bootstrap-multiselect component for a part of our user interface, when we hit a snag.

With our use-case the multi-select was very slow. Especially while loading.

Dan North introduced to me the concept of the Light-saber pattern in 2013. I had forgotten all about it until my girlfriend Ulrika Malmgren reminded me of it over dinner.

Armed with that reminder, I took a look at the multi-select box and simply asked. Is it really warranted to use 1400 lines of JavaScript to solve this problem?

I took a look at what we have available at work:

I wanted to use knockout.js's components to create a custom element for my multi-select needs. The wireup for this was already setup using Sammy.js.

Bootstrap has a drop-down component. What if I could fill the drop-down with a list of options, each holding it's selected state in an observable.

After about an hour I ended up with this:

module.exports = function () {
    var vm = {
        selection_size: ko.observable(0)
    };

    function selection_changed(item, jqueryEvent) {
        item.selected(!item.selected());
        jqueryEvent.stopPropagation();
        vm.selected(vm.values().filter(function (item) {
          return item.selected();
        }).map(function (item) {
          return item.Href;
        }));
        vm.selection_size(vm.selected().length);
        return true;
      }

      function mark_selection(selected_options) {
        var i=0;
        vm.values().map(function (item) {
            if(selected_options.indexOf(item.Href || item) > -1){
                item.selected(true);
                i++;
            } else {
                item.selected(false);
            }
        });
        vm.selection_size(i);
      }
    
      function sort(a,b) {
        if(a.selected() != b.selected()){
            if(a.selected()){
                return -1;
            }
            if(b.selected()){
                return 1;
            }
        }
        if (a.Title > b.Title) {
          return 1;
        }
        if (a.Title < b.Title) {
          return -1;
        }
        return 0;
      }
    
      vm.load = function(params){
        vm.values = ko.pureComputed(function () {
          var p = params.values;
          if(ko.isObservable(p)){
            p = p();
          }
          return p.map(function (item) {
            return {
              Title: item.Title || item,
              Href : item.Href || item,
              selected: ko.observable(false),
              selection_changed: selection_changed
            }
           });
        },vm);
       
        vm.sorted_values = ko.pureComputed(function () {
          var sorted = vm.values();
          sorted.sort(sort);
          return sorted;
        })
        
        if(params.selected) {
          vm.selected = params.selected;
          vm.selected.subscribe(mark_selection);
          mark_selection(vm.selected());
        }
      };
    
      return vm;
}

The HTML for the custom component:

<div class="dropdown">
  <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-expanded="true">
    <span data-bind="text: selection_size"/> <span data-bind="localize:'Client.Management.NumberOfTagsSelected'"/>
    <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1" data-bind="foreach: sorted_values">
    <li role="presentation"><a data-bind="click: selection_changed"><input type=checkbox data-bind="checked: selected"/> <span data-bind="text: Title"></a></li>
  </ul>
</div>

It turned out that for my needs I needed 80 lines of JavaScript. While this code is more specific to our needs it performs better and allows us one less dependency. This code as since been cleaned up a bit, but the essence and amount of lines remains the same.

The code has support for some of our "special needs", for example: reordering based on selection first. Dropping support for these features would reduce the code even further.

If you are looking for development work in the Stockholm area, I have two developer positions available at RemoteX:

  1. Front-end developer
  2. Developer