Home > HTML, JavaScript, Knockout.js, koGrid, TypeScript > koGrid: Bug – Checkboxes column duplication

koGrid: Bug – Checkboxes column duplication

January 24th, 2017 Admin Leave a comment Go to comments

    Using the koGrid to bring an edit-in-place grid to a web app, I’ve run into a bug when the predefined first column of checkboxes are being unexpectedly replicated, i.e. being wrapped in the if-binding, the grid has as many checkboxes columns as many times its presence has been changed.

koGrid: Checkbox Column Duplication Bug

I use the following to define the grid

<!-- ko if: shouldGridBeVisible -->
   <div data-bind="koGrid: koGridOptions()"></div>
<!-- /ko -->
// typescript
private customKoGridColumnDefs = [
   { field: 'Name', displayName: 'Name' },
   ...
];

private koGridColumnDefs = ko.pureComputed(() => {
   return this.customKoGridColumnDefs;
});

public koGridOptions = ko.pureComputed(() => {
   return {
      data: this.items, // observable array of data
      selectWithCheckboxOnly: true, // only want to be able to select with checkboxes
      selectedItems: this.selectedItems, // observable array to store selected items
      columnDefs: this.koGridColumnDefs, // observable columns definitions
      displaySelectionCheckbox : true // show column of checkboxes
      ...
   }
});

The “buggy” place is within the buildColumns function defined in the window.kg.Grid. Among other things the function adds a column of checkboxes to the existing columns definitions, not checking if such column is already there. Below are shortened listings of the buildColumns and related functions, ending with koGrid-binding

// koGrid-2.1.1.debug.js
window.kg.Grid = function (options) {
   ...
   self.config.columnDefs = ko.utils.unwrapObservable(options.columnDefs);
   ...
   self.buildColumns = function () {
      var columnDefs = self.config.columnDefs,
         cols = [];

      if (!columnDefs) {
         self.buildColumnDefsFromData();
         columnDefs = self.config.columnDefs;
      }
      if (self.config.displaySelectionCheckbox && self.config.canSelectRows) {
         // the columns definitions array passed in the options is about to be modified
         columnDefs.splice(0, 0, {
            field: '\u2714',
            width: self.elementDims.rowSelectedCellW,
            sortable: false,
            resizable: false,
            headerCellTemplate: '<input class="kgSelectionHeader" type="checkbox" data-bind="visible: $grid.multiSelect, checked: $grid.allSelected" />',
            cellTemplate: '<div class="kgSelectionCell"><input class="kgSelectionCheckbox" type="checkbox" data-bind="checked: $parent.selected" /></div>'
         });
      }
   ...
   };
   ...
   self.init = function () {
      ...
      self.buildColumns(); // build columns
      ...
   };
   ...
   self.init(); // initialize grid
};
...
ko.bindingHandlers['koGrid'] = (function () {
   return {
      'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
         var options = valueAccessor();
         ...
         var grid = new window.kg.Grid(options); // create and initialize grid
         ...
         // if columndefs are observable watch for changes and rebuild columns.
         if (ko.isObservable(options.columnDefs)) {
            options.columnDefs.subscribe(function (newDefs) {
               grid.columns([]);
               grid.config.columnDefs = newDefs;
               grid.buildColumns(); // rebuild the columns
               grid.configureColumnWidths();
            });
         }
         ...
         return { controlsDescendantBindings: true };
      }
   };
}());

So, whenever if-binding’s conditions are met, the grid becomes visible, and we have the following call sequence:

ko.bindingHandlers['koGrid'].init -> window.kg.Grid.init -> window.kg.Grid.buildColumns

which ends up with adding the next-in-turn checkboxes column to the columns definitions and, therefore, to the grid. The same, by the way, may happen also if the columns definitions (options.columnDefs) are observable and being mutated (line 49 in the listing above).

It’s a well-known issue (I was able to find at least 2 describing this bug: #292 and #209) with a simple fix already residing among the pull requests (#213), but not applied yet (and may never be applied as the project have not been updated since 2014 and looks abandoned). The fix is to the window.kg.Grid.buildColumns and shown below:

// koGrid-2.1.1.debug.js
self.buildColumns = function () {
   var columnDefs = self.config.columnDefs,
      cols = [];

   if (!columnDefs) {
      self.buildColumnDefsFromData();
      columnDefs = self.config.columnDefs;
   }
   if (self.config.displaySelectionCheckbox && self.config.canSelectRows) {
      if (columnDefs.length > 0 && columnDefs[0].field != '\u2714') {
         columnDefs.splice(0, 0, {
            field: '\u2714',
            width: self.elementDims.rowSelectedCellW,
            sortable: false,
            resizable: false,
            headerCellTemplate: '<input class="kgSelectionHeader" type="checkbox" data-bind="visible: $grid.multiSelect, checked: $grid.allSelected"/>',
            cellTemplate: '<div class="kgSelectionCell"><input class="kgSelectionCheckbox" type="checkbox" data-bind="checked: $parent.selected" /></div>'
         });
      }
   }
   ...
};

I really don’t like changing the 3rd party libraries’ source code in my projects and always try to find an alternative way. This issue can be resolved with the fix utilizing the knockout.js‘s power and placed outside of the koGrid-2.1.1.debug.js and koGrid-2.1.1.js. So, below is the binding-wrapper to prevent duplication right before the original koGrid-binding gets control:

// javascript
ko.bindingHandlers["koGridFixed"] = {
   init: function (element, valueAccessor, allBindingsAccessor, data, context) {
      var gridOptions = ko.utils.unwrapObservable(valueAccessor());
      if (gridOptions && gridOptions.columnDefs) {
         var columnDefsArr = ko.utils.unwrapObservable(gridOptions.columnDefs);
         if (columnDefsArr && columnDefsArr.length > 0 && columnDefsArr[0].field === '\u2714')
            columnDefsArr.splice(0, 1);
      }

      return ko.bindingHandlers["koGrid"].init(element, valueAccessor, allBindingsAccessor, data, context);
   }
};

So, just replace the koGrid-binding everywhere in views with the koGridFixed and the problem is solved with no affecting the grid’s source code

<!-- ko if: shouldGridBeVisible -->
   <div data-bind="koGridFixed: koGridOptions()"></div>
<!-- /ko -->
Related posts:
 
  1. No comments yet.
  1. No trackbacks yet.