Archive

Archive for the ‘Knockout.js’ Category

koGrid: Get reference to grid for direct manipulations

January 26th, 2017 No comments

    Having used the koGrid on the page, sometimes you may need to manipulate with the grid directly, for example, to programmatically select or deselect items and so on. For this you need to get a reference to the grid instance first. The koGrid supports plugins to enhance and extend grid’s capabilities. Each plugin should expose the onGridInit method being called from the koGrid-binding and fed with the grid instance, which can be saved and used later. Below is a fragment of the koGrid-binding where the plugins are being initialized:

// koGrid-2.1.1.debug.js
ko.bindingHandlers['koGrid'] = (function () {
  return {
    'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
       var options = valueAccessor();
       ...
       var grid = new window.kg.Grid(options);
       ...
       //initialize plugins
       $.each(grid.config.plugins, function (i, p) {
          if (typeof p.onGridInit === 'function') {
             p.onGridInit(grid);
          }
       });
       ...
       return { controlsDescendantBindings: true };
    }
  };
}());

Let’s create a plugin to capture the reference to the grid. The following simple typescript class keeps the reference and exposes it to outer code. Note the use of ko.utils.domNodeDisposal.addDisposeCallback to release the reference if the grid disappears from the DOM-model (for example, being wrapped in if– or with-bindings).

// typescript
export class KoGridRef {

  public grid = null;

  public onGridInit(grid) {
    // save the reference
    this.grid = grid;
    
    var self = this;
    // hook up the node removing to release the reference to the grid instance
    ko.utils.domNodeDisposal.addDisposeCallback(grid.$root[0], function() {
      self.grid = null;
    });
  }
}

Below is how to declare, attach and use the plugin

// typescript

// declare
private gridRef: KoGridRef = new KoGridRef();

// attach
public koGridOptions = ko.pureComputed(() => {        
  return {
    data: this.items, // observable array of data
    selectedItems: this.selectedItems, // observable array to store selected items		
    plugins: [gridRef], // array of plugins
    ...
  }
});

// use
public get grid() { // property to return the grid instance
  return this.gridRef.grid;
}	

public selectAll(grid: any, state: boolean) { // method to select or deselect all items
  grid.allSelected(state);
}	
...
this.selectAll(this.grid, Math.random() >= 0.5); // randomly select or deselect all items
...
Related posts:

koGrid: Bug – Checkboxes column duplication

January 24th, 2017 No 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:

Knockout.js: Subscription that fires only once

November 16th, 2016 No comments

    It might be useful sometimes to have an observable subscription, which fires only once. The straightforward implementation is to use kind of “already fired” internal flag that would prevent a handler from being called more than once. It’s not the best approach though, as the subscription itself remains alive and continues firing every time the value has mutated. Much more efficient solution is to “kill” subscription by disposing it at the first call:

ko.subscribable.fn.subscribeOnce = function (handler, target, event) {
    var subscription = this.subscribe(function (newValue) {
        subscription.dispose(); // remove subscription, so it won't fire further
        handler(newValue);
    }, target, event);

    return subscription; // mimic the normal "subscribe" by returning the subscription
};

In case of computed observables, it gives even more benefits since it also removes all underlying subscriptions to other observables the computed observable depends on.

In TypeScript this will be an extension to the KnockoutSubscribableFunctions<T> interface

// knockoutjs-extensions.d.ts
interface KnockoutSubscribableFunctions<T> {
    subscribeOnce(callback: (newValue: T) => void, target?: any, event?: string): KnockoutSubscription;
    subscribeOnce<TEvent>(callback: (newValue: TEvent) => void, target: any, event: string): KnockoutSubscription;
}

TypeScript has the “declaration merging” concept, so the interfaces are “open-ended”. That means two or more separate declarations with the same name are to be merged by compiler into a single one. Note, to be effective the declaration has to be placed in a TypeScript Declaration File (*.d.ts).

Below is a usage example

...
private someObservable: KnockoutObservable<SomeType> = ko.observable(null);
...
this.someObservable.subscribeOnce(newVal => {
	// do something here
});

Knockout.js Validation: Check if value is in range

June 20th, 2015 1 comment

    To validate a model and its properties (observable and ordinary ones) we use such knockout.js plugin as Knockout Validation. This validation library is simple and easy to extend. For example, a new validation rule to check if value is in range could be as simple as the following:

import ko = require("knockout");
import validation = require("knockout.validation");
...
export function enableCustomValidators() {

 validation.rules['inRange'] = {
  validator: function (val, params) {
   var minVal = ko.validation.utils.getValue(params.min);
   var maxVal = ko.validation.utils.getValue(params.max);

   var res = params.includeMin ? val >= minVal : val > minVal;

   if (res)
    res = params.includeMax ? val <= maxVal : val < maxVal;

   if (!res)
    this.message = ko.validation.formatMessage(params.messageTemplate || this.messageTemplate, 
       [minVal, maxVal, (params.includeMin ? '[' : '('), (params.includeMax ? ']' : ')') ]);

   return res;
   },
   messageTemplate: 'Value has to be in range {2}{0}, {1}{3}'
 };

 validation.registerExtenders(); // adds validation rules as extenders to ko.extenders
}
...
// the enableCustomValidators should be called when the custom 
// validation rules are supposed to be used

Note the code above and further is TypeScript. The TypeScript definition to the Knockout Validation is available as a NuGet package, namely knockout.validation.TypeScript.DefinitelyTyped.

The inRange rule accepts the start and end values and allows to indicate should or should not they be included in the range (by default boundaries are excluded). The validation error message is being generated based on the custom defined messageTemplate or default one. The default message template uses four placeholders where the {0} and {1} are start and end values correspondingly. The latter two represent brackets: square or round ones depending on the boundaries inclusion.

The example below defines inRange validation to keep the value between 0 (including it) and 10 (excluding it):

public someNumber = ko.observable<number>(null).extend(
{
	required: { params: true, message: "This field is required" },
	number:   { params: true, message: "Must be a number" },
	
	inRange:  {
		params: {
			min: 0,
			includeMin: true,
			max: 10,
			//includeMax: true,
		}
	}
});

In case of a wrong value the message will says “Value has to be in range [0, 10)“.

To redefine the message template the following might be used:

public somePositiveNumber = ko.observable<number>(null).extend(
{
 required: { params: true, message: "This field is required" },
 number:   { params: true, message: "Must be a number" },
	
 inRange:  {
  params: {
   min: 0,			
   max: 1,
   messageTemplate: "Must be a positive number less than {1}"// the range end value will be put
  }
 }
});

Note the ko.validation.formatMessage has been used to generate the final validation message. Till recently the ko.validation.formatMessage wasn’t able to process more than one placeholder in template. So, if you use one of such outdated versions of Knockout Validation library, you may be interested in the actual formatting method implementation shown below. It’s borrowed from the same Knockout Validation, but of up-to-date version.

function formatMessage(message, params, observable?) {
    if (ko.validation.utils.isObject(params) && params.typeAttr) {
        params = params.value;
    }
    if (typeof message === 'function') {
        return message(params, observable);
    }
    var replacements = ko.utils.unwrapObservable(params);
    if (replacements == null) {
        replacements = [];
    }
    if (!ko.validation.utils.isArray(replacements)) {
        replacements = [replacements];
    }
    return message.replace(/{(\d+)}/gi, function (match, index) {
        if (typeof replacements[index] !== 'undefined') {
            return replacements[index];
        }
        return match;
    });
}

Knockout.js: HtmlNoScript binding

February 14th, 2013 No comments

    The Knockout.js is one of the most popular and fast-developing libraries that bring the MVVM pattern into the world of JavaScript development. Knockout.js allows to declaratively bind UI (Document Object Model elements) to an underlying data model. One of the built-in bindings is the html-binding allowing to display an arbitrary Html provided by an associated property/field of the data model. The typical use of this binding may look like this:

<div data-bind="html: comment" />

where the comment property may be specified as shown in the following data model:

...
// include the knockout library
<script src="js/knockout/knockout-2.2.0.debug.js" type="text/javascript"></script>
...
$(function () {
	// define a data model
	var viewModel = function () {
        var self = this;		
		self.comment = ko.observable("some html comment <span>Hello<script type='text/javascript'>alert('Hello!');<\/script></span>");
		...
	}
	...
	// create instance of the data model
	var viewModelInstance = new viewModel();
	// bind the instance to UI (since that moment all of the declarative bindings have worked off and the data are displayed)
	ko.applyBindings(viewModelInstance);
});

Applying a value to a DOM element, the html-binding exploits the jQuery‘s html() method, of course, if jQuery is available on the page; otherwise knockout.js relies on its own logic that ends up with call of the DOM‘s appendChild function. In both cases if the html being applied contains inclusions of JavaScripts, the scripts execute once the html has been added to the DOM. Preventing all unwanted scripts from running is the best practice when displaying random htmls on the page. Using the approach described in my post How to prevent execution of JavaScript within a html being added to the DOM we are able to develop our own knockout binding disabling the scripts. It could look like the following:

 ko.bindingHandlers.htmlNoScripts = {
    init: function () {
        // Prevent binding on the dynamically-injected HTML
        return { 'controlsDescendantBindings': true };
    },
    update: function (element, valueAccessor, allBindingsAccessor) {
        // First get the latest data that we're bound to
        var value = valueAccessor();
        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.utils.unwrapObservable(value);
		// disable scripts
        var disarmedHtml = valueUnwrapped.replace(/<script(?=(\s|>))/i, '<script type="text/xml" ');
        ko.utils.setHtml(element, disarmedHtml);
    }
};
 

An alternative way to prevent scripts from running is to use the innerHTML property of a DOM element as follows:

 var html = "<script type='text/javascript'>alert('Hello!');<\/script>";
 var newSpan = document.createElement('span');
 newSpan.innerHTML = str;
 document.getElementById('content').appendChild(newSpan);
 

Some people, however, report that this doesn’t work in Internet Explorer 7 for unknown reasons. So, let’s combine these two methods in one to minimize the risk of unwanted scripts running. My tests show the combined approach successfully prevent JavaScript from being executed in all popular browsers. Even in the worst case, at least one of the methods works off. The resultant knockout binding may look like the following:

ko.bindingHandlers.htmlNoScripts = {
    init: function () {
        // Prevent binding on the dynamically-injected HTML
        return { 'controlsDescendantBindings': true };
    },
    update: function (element, valueAccessor, allBindingsAccessor) {
        // First get the latest data that we're bound to
        var value = valueAccessor();
        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.utils.unwrapObservable(value);
        // disable scripts
        var disarmedHtml = valueUnwrapped.replace(/<script(?=(\s|>))/i, '<script type="text/xml" ');        
        // create a wrapping element
        var newSpan = document.createElement('span');
        // safely set internal html of the wrapping element
        newSpan.innerHTML = disarmedHtml;
        // clear the associated node from the previous content
        ko.utils.emptyDomNode(element);
        // add the sanitized html to the DOM
        element.appendChild(newSpan);
    }
};

This htmlNoScripts binding can be used as follows:

<div data-bind="htmlNoScripts: comment" />

Related posts: