Archive

Archive for the ‘TypeScript’ 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
});

jQuery File Upload: IE9 and ASP.Net Web API File Uploading

June 2nd, 2016 No comments

    The blueimp jQuery File Upload plugin uses the XMLHttpRequest to pass the file data to a server (only IE10+). If browser doesn’t support Ajax file uploading, the plugin makes a workaround by dynamically creating IFrame and sending the data on behalf of it through the traditional form POST. The JavaScript responsible for the workaround resides in jquery.iframe-transport.js, which accompanies the basic jquery.fileupload.js. The following TypeScript code could be used to initialize the plugin and submit file data (the code is intentionally kept as simple as possible – no progress bars, validations and so on):

//...
private fileData: any = {};
//...
$(".filePicker").fileupload({
	autoUpload: false, // will be submitted once button is clicked
	method: "PUT",
	dataType: "json", 
	url: "api/someController/someMethod", // Web Api method to receive and process the file
	formData: () => { // additional parameters accompanying the file data
		return [{
			name: "bookName",
			value: $("#bookName").val()
		},
		{
			name: "bookGenre",
			value: $("#bookGenre").val()
		},
		{
			name: "bookAuthor",
			value: $("#bookAuthor").val()
		}];
	},
	add: (e: JQueryEventObject, data: any) => { // event handlers
		this.fileData = data;
		//...
	},
	done: (e: JQueryEventObject, data: any) => {
		//...
	},
	fail: (e: JQueryEventObject, data: any) => {		
		//...
	},
	always: (e: JQueryEventObject, data: any) => {
		//...
	}
});
//...
$("#loadBook").on('click', function () { // the button to initiate the file sending
	if (this.fileData && this.fileData.hasOwnProperty("process")) {

		// file data validation: size, extension, whatever else...
		
		this.fileData.process().done(() => { // file sending
			this.fileData.submit();
		});
	}
});

On the server side the following code receives and processes the file data:

using System;
using System.Text;
using System·Web;
using System·Web.Http;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Linq;
...
namespace DotNetFollower.Web.Controllers.Api
{
    [RoutePrefix("api/someController")]
	public class someController : ApiController
	{        
        public someController()
		{
            //...
		}
		
        [Route("someMethod")]
        [HttpPut]
        [HttpPost] // this attribute allows processing traditional form POST
        public ServiceResult<BookOutput> someMethod()
        {
            try
            { 
			    //...
			    var request = HttpContext.Current.Request;
			    var files   = request.Files;

			    if (files.Count == 0) 
				    throw new Exception("Couldn't find a book to load!");

			    // read accompanying parameters
			    var bookName   = request.Form.Get("bookName");
			    var bookGenre  = request.Form.Get("bookGenre");
			    var bookAuthor = request.Form.Get("bookAuthor");
			
			    var file = new HttpPostedFileWrapper(files[0]);
			
			    // parsing file.InputStream ...
			
			    // processing the parsed file data ...
			
			    file.InputStream.Close();
			    //...
			
			    return new ServiceResult<BookOutput>() 
				    { 
					    Data = new BookOutput() 
						    { 
							    Name   = bookName, 
							    Genre  = bookGenre, 
							    Author = bookAuthor 
						    } 
				    };
            }
            catch(Exception ex)
            {
                return new ServiceResult<BookOutput>(ex);
            }
        }
	}
}

// Where ServiceResult and BookOutput are defined as follows

public class ServiceResult<T>
{
	public bool   Success      { get; set; }
	public string ErrorMessage { get; set; }	
	public T      Data         { get; set; }

	public ServiceResult()
	{
		Success = true;
	}

	public ServiceResult(string errorMessage)
	{   
		ErrorMessage = errorMessage;
	}

	public ServiceResult(Exception exception)
	{
		ErrorMessage = exception.Message;	
	}
}

public class BookOutput
{
	public string Name   { get; set; }
	public string Genre  { get; set; }
	public string Author { get; set; }
}

Unfortunately, the code doesn’t works as expected in Internet Explorer 9 (thankfully, the lower versions are not supposed to be supported by the project, so I don’t care about them). If IE9 sends an Ajax request to the Web Api method, it interprets the JSON response correctly. However, when uploading a file, the jQuery File Upload plugin sends traditional non-Ajax form POST. So, having received the JSON result, IE9 prompts for a JSON file download.

Download Json File Prompt

To bypass such IE9 behaviour the server response should contain the content-type header “text/html” rather than the “application/json” returned by the Web Api method by default. To avoid writing some IE9 specific logic on both server and client sides, I’ve introduced the following Web Api method-adapter:

[Route("someMethodAdapted")]
[HttpPut]
[HttpPost] // this attribute allows processing traditional form POST
public HttpResponseMessage someMethodAdapted()
{
	var res = someMethod(); // call the original method

	const string JsonContentType = "application/json";
	const string HtmlContentType = "text/html";

	var response = Request.CreateResponse(HttpStatusCode.OK); // return 200 OK
	var context  = HttpContext.Current;
	response.Content = new StringContent(ToJson(res), // serialize result object into JSON
	  Encoding.UTF8, 
	  // check if the JSON content-type is accepted 
	  // (it's not accepted in case of form POST coming from IE9)
	  context.Request.AcceptTypes.Contains(JsonContentType, StringComparer.OrdinalIgnoreCase) ?
	     JsonContentType : HtmlContentType); // return suitable content-type

	return response;
}

// the ToJson method is defined as follows

private static string ToJson<T>(T obj) where T : class
{
	if (obj == null)
		return string.Empty;

	DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
	using (MemoryStream stream = new MemoryStream())
	{
		serializer.WriteObject(stream, obj);
		return Encoding.Default.GetString(stream.ToArray());
	}
}

The someMethodAdapted is supposed to be used instead of someMethod everywhere on the client side. So, repoint the url to the method-adapter

...
$(".filePicker").fileupload({
	...
	// Web Api method-adapter to receive and process the file
	url: "api/someController/someMethodAdapted",
	...
});
...

The someMethodAdapted makes a content-type trick and perfectly serves IE9 and higher. The use of HttpResponseMessage gives a control over the response headers. The HttpContext.Current.Request.AcceptTypes is a list of client-supported content types (aka MIME types). If the “application/json” is not in the list, the “text/html” is the right choice. Below are the AcceptTypes of Ajax and non-Ajax requests made by IE9:

// IE9 Ajax request to a Web Api method (true for higher browser versions too)
HttpContext.Current.Request.AcceptTypes	{string[3]}	string[]
[0]	"application/json"	string
[1]	"text/javascript"	string
[2]	"*/*; q=0.01"	string

// IE9 non-Ajax form POST
HttpContext.Current.Request.AcceptTypes	{string[3]}	string[]
[0]	"text/html"	string
[1]	"application/xhtml+xml"	string
[2]	"*/*"	string