2015年10月21日星期三

DNN7 WebApi CamelCase problem

I used to encounter an error with DotNetNuke version 7 several time which displays all modules with a name of "undefined" and a broken picture.



This happens when people try to configure their DNN Web Api library to JSON format. Because in C# this is usually done by PascalCase and in JavaScript this is but done using camelCase. So when people want to consume the service with some javascript library, AngularJS for example, they configure the data source to JSON format. In DNN, it's usually done like this.

public class RouteMapper : IServiceRouteMapper
{
  public void RegisterRoutes(IMapRoute mapRouteManager)
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    formatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

    mapRouteManager.MapHttpRoute("Invoice", "default", "{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }, namespaces: new[] { "DnnWebApi.Controllers" });
  }
}

This will however configure the data format to be camelcase globally. And DNN has its own Web Api implementations which use actually PascalCase. So camelCase will make its client scripts break.

for (var i = 0; i < moduleList.length; i++) {
  ul.append('<li><div class="ControlBar_ModuleDiv" data-module=' + moduleList[i].ModuleID + '><div class="ModuleLocator_Menu"></div><img src="' + moduleList[i].ModuleImage + '" alt="" /><span>' + moduleList[i].ModuleName + '</span></div></li>');
}

So my solution is pretty simple and dumb. Switch to camelcase when the controller instantiates, and switch it back when it disposes.

public class AppController: DnnApiController, IDisposable
{
  private IContractResolver _resolver;

  public AppController()
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;

    _resolver = formatter.SerializerSettings.ContractResolver;
    formatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

  }

  //your implementation...

  void IDisposable.Dispose()
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    formatter.SerializerSettings.ContractResolver = _resolver;            
  }
}

2015年10月17日星期六

Simple sidebar transition

Simple Sidebar Transition

Sidebars are responsive components that display various forms of information to the side of applications or sites. This approach is designed to fit in with the Bootstrap framework. Hence, Bootstrap components and JQuery are needed.

Default Sidebar

Default Sidebar works as same as .col-md-3 in Bootstrap grid system. But when the device width is less than its breakpoint size, it hides itself instead of Collapsing to start.

<div class="container">
  <div class="row">
    <div class="sidebar">
    </div>
    <div class="col-md-10">
    </div>
  </div>
</div>

Fixed to Top

The sidebar hides itself when the device width is less than its breakpoint size. And it can be toogled to show and hide the element via class changes.

Adjustment required

The fixed sidebar will overlay Bootstrap Navbar and create a gap when the screen scrolls down. So the following adjustments are needed.

  • Add .sidebar-fixed and include a .sidebar-container to contain content.
  • Add .navbar-fixed-top to navbar if available.
  • Add offsetting class .col-md-offset-2 to the right side.
  • Add padding to the top of the <body>. By default, the navbar is 50px high.
  • Set up toggle element via data-toogle-target attribute.

Example

<div class="container">
  <div class="row">
    <div class="sidebar sidebar-fixed">
      <button type="button" class="btn" data-toggle-target="#sidebar-fixed-1"><span class="glyphicon glyphicon-align-justify"></span></button>
      <div class="sidebar-container">
    </div>
    <div class="col-md-10 col-md-offset-2">
    </div>
  </div>
</div>

Javascript

Include the following code snippet to your page and replace the toggle element selector with your own. By default, it is .sidebar>.btn It requires JQuery.

+function($){
  function toggleSidebar(e) {
    $($(e.currentTarget).attr('data-toggle-target')).toggleClass('open');
  }

  $(document).on('click.sidebar', '.sidebar>.btn', toggleSidebar);
}(jQuery)

Inverted Fixed to Top

Modify to sidebar to have it collapse regardless deveice width by adding .sidebar-fixed-inverse.

<div class="container">
  <div class="row">
    <div class="sidebar sidebar-fixed-inverse">
      ...
    </div>
  </div>
</div>

Style sidebar

Include the following style to your page or stylesheet file to make it properly display.

.sidebar-fixed, .sidebar-fixed-inverse{
  background-color:#eee;
  position:fixed;
  top:51px;
  bottom:0;
  z-index:2;
  transition: left 0.3s;
  -moz-transition: left 0.3s; /* Firefox 4 */
  -webkit-transition: left 0.3s; /* Safari 和 Chrome */
  -o-transition: left 0.3s; /* Opera */
}
.sidebar>.btn{
  display:none;
  border-radius:0;
  background-color:#eee;
  border-color:#e0e0e0;
}
.sidebar-fixed>.btn, .sidebar-fixed-inverse>.btn{
  position:absolute;
  right:-40px;
  top:0;
 display:block;
}
.sidebar-fixed>.sidebar-container, .sidebar-fixed-inverse>.sidebar-container{
  overflow:auto;
  position:absolute;
  top:0;
  bottom:0;
  margin:0;
  width:100%;
}
.sidebar.open{
  left:0;
}
@media(min-width: 768px){
  .sidebar{
    display:none;
  }
  .sidebar-fixed, .sidebar-fixed-inverse{
    display:block;
    width:25%;
    left:-25%;
  }
}
@media(min-width: 992px){
  .sidebar{
    display:block;
    float:left;
    width:16.66666667%;
  }
  .sidebar-fixed, .sidebar-fixed-inverse{
    width:16.66666667%;
    left:-16.66666667%;
  }
  .sidebar-fixed{
    left:0;
  }
  .sidebar-fixed>.btn{
    display:none;
  }
}

2015年10月4日星期日

UNBLUR BACKGROUND (PART 3)

Fading effect

So the fading effect would be using a loop of timed-out call to simulate. It will be something like this...

var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], 
    pctx = painter.getContext('2d'), ctx = $('#canvas2')[0].getContext('2d'), img= $('img')[0];
for(var i = i; i <= 10; i++){
  (function(i){
    setTimeout(function(){
        pctx.strokeStyle = "rgba(0,0,0,"+(1 - i / 10)+")";
        pctx.beginPath();
        pctx.moveTo(760, 20);
        pctx.lineTo(760, 100);
        pctx.stroke();
        ctx.drawImage(img, 0, 0, img.width, img.height);
        ctx.globalCompositeOperation = "destination-in";
        ctx.drawImage(painter, 0, 0);
        ctx.globalCompositeOperation = "source-over";
 },i * 200);
  })(i);
}

But I cannot make it directly work with mousemove event, because it fires for every pixel mouse move which is very costly and inefficient. And also for every line it draws, it draws also one image and one canvas as well, which is also very expensive.

This is what I'm going to do. Firstly, the event, instead of directly drawing, it just records the current mouse coordinate and the time stamp and trigger one recursive function if it's not started. And then the recursive function, which will constantly run once in a short while till no more event occurs, it contains the logic of drawing, coordinate collection adding and removal and the indicator that decide whether it can be triggered. Finally, I need a collection that stores coordinates so that I can draw all of them in the painter and copy the painter to the main canvas.

Collect the coordinates

The first thing is, I'm going to need an array to store coordinate objects.

var points = [];

Handle the mousemove event

Firstly I instantiate several global variables so I can read in the recursive function. And in the event handler, I record the coordinate and time stamp and trigger the recursive function if it’s not started yet.

var x, y, eventTimestamp, isContinue;
$(document).on('mousemove', function(e){
  x = e.clientX, y = e.clientY, eventTimestamp = Date.now;
  if(!isContinue) draw();
});

The recursive function

This function does basically three things.

  • It adds and removes coordinates of the collection.
  • It checks and decides whether it continues and can be triggered by the event handler.
  • It draws all lines using adjacent coordinates and put it to the main canvas.

This function makes a delayed call to itself if mousemove event keeps firing. And when it executes it adds the new coordinate object to the collection. I can use this for the fading effect. So I put time stamps in coordinate objects and compare them with the newest one to decide the opacity value. By doing this, it draws lines with different fading effects and refreshes the canvas every time it executes. That's how the amination works.

function draw(){
  var currentTimestamp = Date.now, newPoint = {x: x, y: y, t: currentTimestamp};
  
  //check if to continue
  isContinue = currentTimestamp - eventTimestamp < 500;
  //add new coordinate
  if(isContinue){
 points.unshift(newPoint);
  }
  //remove faded out coordinates
  for(var i = 0; i < points.length;){
 currentTimestamp - points[i].t > 1000 ? points.splice(i, points.length) : i++;
  }
  //recursive call
  if(points.length){
 setTimeout(draw, 20);
  }
  
  var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], pctx = painter.getContext('2d');
  //clear painter, ready to draw
  pctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  //draw all lines in the collection
  for (var i = 1; i < points.length; i++) {
 pctx.strokeStyle = "rgba(0,0,0," + Math.max(1-(currentTimestamp-points[i].t)/1000, 0) + ")";
 //umcomment this line to get dynamic line width
 //pctx.lineWidth = 25+75*Math.max(1-Math.sqrt(Math.pow(points[i].x-points[i-1].x,2)+Math.pow(points[i].y-points[i-1].y,2))/50,0);
 pctx.lineWidth = 50;
 pctx.beginPath();
 pctx.moveTo(points[i - 1].x, points[i - 1].y);
 pctx.lineTo(points[i].x, points[i].y);
 pctx.stroke();
  }
  var ctx = $('canvas')[0].getContext('2d'), img= $('img')[0];
  //copy-paste 
  ctx.drawImage(img, 0, 0, img.width, img.height);
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(painter, 0, 0);
  ctx.globalCompositeOperation = "source-over";
}

Handle resize

The background will automatically re-adjust to adopt the new width or height. So the image and canvases are also need to be taken care of.

$(window).on('resize', function(){
var img = $('img')[0];
  var windowHeight = $(window).height(), windowWidth = $(window).width(), windowRate = windowWidth/windowHeight;
  var imgHeight = img.naturalHeight, imgWidth = img.naturalWidth, imgRate = imgWidth/imgHeight;

  if(windowRate>=imgRate){
    $(img).attr('height', (imgHeight/imgWidth*windowWidth)).attr('width', windowWidth);
  }else{
    $(img).attr('width', (imgWidth/imgHeight*windowHeight)).attr('height', windowHeight);
  }
  $('canvas').attr('width', windowWidth).attr('height', windowHeight);
});

So that's the all of it. I put the demo effect on the background. Feel free to try.



UNBLUR BACKGROUND (PART 1)

UNBLUR BACKGROUND (PART 2)

2015年9月30日星期三

UNBLUR BACKGROUND (PART 2)

Start to draw

The basic idea is to draw lines with blurring effect from the last coordinate to the current coordinate whenever mouse moved and make it fade over time to zero. Here I want to draw line with brash effect and blur effect. And then to make it fade, I simply manipulate the opacity a bit. So let's see what comes out.

var ctx = $('canvas')[0].getContext('2d');
ctx.lineCap = "round";
ctx.shadowColor = "#000";
ctx.lineWidth = 30;
ctx.shadowBlur = 30;
for(var i=0; i<=10;i++){
 ctx.strokeStyle = "rgba(0,0,0,"+(1-i/10)+")";
 ctx.beginPath();
 ctx.moveTo(i*70+50, 20);
 ctx.lineTo(i*70+50, 100);
 ctx.stroke();
}

Well, to draw lines with a single color is easy. What about copy some photo parts and fill them to those lines? There are two options, compositing and clipping. Initially I thought clipping was more promising because it was simpler and more straightforward. Soon after that I began to have problems with implementing blurring and fading effects. And most importantly the browser started to lag when those was added. So I decided to try the other approach.

Global Composite Operation

By definition, the canvas 2D rendering context globalCompositeOperation property of the Canvas 2D API sets the type of compositing operation to apply when drawing new shapes. And the type "destination-in" keeps the existing canvas content where both the new shape and existing canvas content overlap. Everything else is made transparent. So I can draw a line and paste the image to achieve the goal. See the effect.

But wait, it can only draw something in the overlap zone. to make trace of mouse move I'm definitely going to draw lots of lines not just in a narrowing area. Ideally, it would be great I can draw whole trace in somewhere else and paste it into canvas. It took me a while to figure out that it could actually copy the drawing from one canvas and paste it to another one. So the idea came out. Draw the whole trace in a temporary canvas and then paste the drawing to the main canvas. And I found out that this temp canvas doesn't have to be inside the body. It just needs to be instantiate properly with exactly same width and height.

var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], 
    pctx = painter.getContext('2d');

pctx.lineCap = "round";
pctx.shadowColor = "#000";
pctx.lineWidth = 30;
pctx.shadowBlur = 30;

for(var i = 0; i <= 10; i++){
 pctx.strokeStyle = "rgba(0,0,0," + (1 - i / 10) + ")";
 pctx.beginPath();
 pctx.moveTo(i * 70 + 50, 20);
 pctx.lineTo(i * 70 + 50, 100);
 pctx.stroke();
}

var ctx = $('canvas')[0].getContext('2d'), img= $('#imgtarget')[0];

ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(painter, 0, 0);
ctx.globalCompositeOperation = "source-over";

So I've got the method to draw. The next step would be to use it to handle the mousemove event. But handling the event was far more complicated than I thought. I will break it down next time.



UNBLUR BACKGROUND (PART 1)

UNBLUR BACKGROUND (PART 3)

2015年9月27日星期日

UNBLUR BACKGROUND (PART 1)

Recently I've found a very interesting animation effect from this website, which can unblur the blurred background and leave a disappearing track on the screen. I want to make one myself but I couldn't find any library can do something like that. So I tried to build one from scratch. So the basic idea is to use a blurred image as background image and use HTML5 <canvas> tag to draw a small dot with the original image on the location whenever mouse move.

Prepare background

Frist thing I will need two same images, one blur and one not. And then set up the blurred image as background and make it not repeat, cover the whole screen and not moving with scroll. Here background-attachment:fixed makes it fixed with regard to the viewport, and background-size:cover makes it always cover the whole screen and keep the image aspect ratio.

body{
  background: url('images/background-blur.jpg') no-repeat 0 0 fixed;
  background-size: cover;
}

Prepare image

And then set up the image to draw. This image will basically need to be kept invisible. But I cannot use display:none here because I'm going to need to read its width and height in the future. Most importantly, the image needs to be adjust its width and height to be identical with the background.

<div style="position:absolute;width:0;height:0;overflow:hidden;">
  <img src="background.jpg" alt="" / >
</div>
<script>
  var img = $('img')[0];
  var windowHeight = $(window).height(), windowWidth = $(window).width(), windowRate = windowWidth/windowHeight;
  var imgHeight = img.naturalHeight, imgWidth = img.naturalWidth, imgRate = imgWidth/imgHeight;

  if(windowRate>=imgRate){
    $(img).attr('height', (imgHeight/imgWidth*windowWidth)).attr('width', windowWidth);
  }else{
    $(img).attr('width', (imgWidth/imgHeight*windowHeight)).attr('height', windowHeight);
  }
</script>

Prepare canvas

I also need create a <canvas> element, make it cover the whole screen and set the position to fixed. Here I got a problem. It is a bit difficult to set height. And I have to use a piece of javascript to make it work properly.

<script>
  $('body').append(
    $('<canvas style="position:fixed;top:0;left:0;z-index:-1;"></canvas>')
   .attr('width', $(window).width()).attr('height', $(window).height())
  );
</script>

I'm going to have to split this article to several parts. Next time I'm going to explain how it works between image and canvas.



UNBLUR BACKGROUND (PART 2)

UNBLUR BACKGROUND (PART 3)

2015年9月23日星期三

CSS: hiding text when it's overflow

Use the following CSS to hide the text. This piece of CSS will hide the exceeded part and replace with some "..." instead.

.nowrap{
  white-space: nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

This will work with both fixed width and percentage width. See the sample below:

The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.

But sometime it doesn't work. I encountered several problems when using it. So I list the problems here just in case somebody got the problem.

  • "text-overflow" will only apply to its PLAIN text child elements.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
  • "text-overflow" will not apply to "table" element.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.

2015年9月21日星期一

Nested Table using AngularJS and Bootstrap

Last time I created a bootstrap like nested table. Now I'm going to show you that how it's going to work with AngularJS. Let's say I got some JSON data from a RESTful service. BTW, AngularJS doesn't really need JQuery. But I simply attached the JQuery and use it whenever I need it.

$scope.data =
[{
  name: 'test name',
  createDate: '2013-05-10T15:04:44.593',
  description: "Don't use data attributes from multiple plugins on the same element.",
  amount: '$20',
  quantity: '2',
  children: [{
    name: 'test name',
    createDate: '2013-05-10T15:04:44.593',
    description: "Don't use data attributes from multiple plugins on the same element.",
    amount: '$20',
    quantity: '2'
  },
  {
    name: 'test name',
    createDate: '2013-05-10T15:04:44.593',
    description: "Don't use data attributes from multiple plugins on the same element.",
    amount: '$20',
    quantity: '2'
  }]
},
{
  name: 'test name',
  createDate: '2013-05-10T15:04:44.593',
  description: "Don't use data attributes from multiple plugins on the same element.",
  amount: '$20',
  quantity: '2',
  children: []
}];

And the angular way of doing this would be basically like this.

<div class="table">
  <div class="header">
    <div class="line">
      <div ng-repeat="(key, value) in data[0]" >{{key}}</div>
    </div>
  </div>
  <div class="body">
    <div class="line" ng-repeat="row in data" >
      <div class="main">
     <div ng-show="row.children && row.children.length" ><button.../div>
        <div ng-repeat="(key, value) in row" >{{value}}</div>
      </div>
      <div class="sub">
        <div class="table">
          ...
        </div>
      </div>
    </div>
  </div>
</div>

There are two major problems about this solution. It needs two definitions for each data in the array and columns to show. For each data, there needs an object to store some status, functions and maybe the data also. For columns, there could also store some definitions such as width, formatter. Fortunately, Angular also developed a lot more advanced solution called Angular UI-Grid that provides some pretty good concepts I can use for my own. So I took a bit of its concepts and implemented them to my template.

The column concept

So in the controller, I created an option object in which I put column definitions. But unlike UI-Grid, I put the data in the scope and passed them separately to the template instead of just one option object. Because the nested table will need to bind with child objects which is different from its parent.

var app = angular.module('demoApp', ['ngRoute']);
app.controller('demoController', ['$scope', '$timeout', function ($scope, $timeout) {
  $scope.options = {
    colDefs: [
      { name: "name", displayName: "Name", width: '15%' },
      { name: "createDate", displayName: "Create Date", width: '15%', cellFilter: "date:'yyyy-MM-dd'" },
      { name: "description", displayName: "Description", width: '35%' },
      { name: "quantity", displayName: "Quantity", width: '10%' },
      { name: "amount", displayName: "Total", width: '10%' }
    ]
  };
  
  $timeout(function(){
    $scope.options.data = [...];
  });
}]);

The table template directives

Then I will need to create four directives for table, row, header and column. Here I'm going to explain a bit about the row directive first. ng-repeat creates a child scope and store the iteration in it. that gives the perfect place to store the row definitions. So in table directive I looped the data array and created a separate object for each element. And then here came a problem. Since I created an array of row objects, ng-repeat will not be able to notice whether there is something change in the data array. So I will need to watch the data manually, and see if there is any data insertion and removal. The header and column directives are relative simple. They just needs to read the column definitions from the option, display the value or name in a proper format.

app.directive('uiTable', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    scope: {
      options: "=uiTable",
      items: "=uiTableModel"
    },
    controller: ["$scope", "$element", "$attrs", function ($scope, $element, $attrs) {
      //template
      var template =  "<div class=\"table\">\n" +
              "  <div class=\"header\">\n" +
              "    <div class=\"line\">\n" +
              "      <div ng-repeat=\"colDef in options.colDefs\" grid-header-render ></div>\n" +
              "    </div>\n" +
              "  </div>\n" +
              "  <div class=\"body\" ng-show=\"rows.length\" >\n" +
              "    <div class=\"line\" ng-class=\"{ 'open': row.isOpened }\" ng-repeat=\"row in rows\" >\n" +
              "      <div class=\"main\" grid-row-render ></div>\n" +
              "      <div class=\"sub\" data-ui-table=\"options\" data-ui-table-model=\"row.entity[options.children]\" ></div>\n" +
              "    </div>\n" +
              "  </div>\n" +
              "   <div ng-hide=\"rows.length\" >No Data</div>\n" +
              "</div>";

      var row = function (entity) {
        this.entity = entity;
      };
      row.prototype.expand = function (e) {
        e.preventDefault();
        this.isOpened = !this.isOpened;
      };

      $scope.rows = [];
      $scope.options.children = $scope.options.children || 'children';
      //watch data
      var deregFunctions = [];
      var dataWatchFunction = function () {
        var newData = $scope.items, newRows = [];
        $.each(newData, function (i, entity) {
          var oldRows = $scope.rows.filter(function (o) { return o.entity == entity; });
          if (oldRows.length) {
            newRows.push(oldRows[0]);
          } else {
            newRows.push(new row(entity));
          }
        });
        $scope.rows = newRows;
      };
      if (!angular.isString($scope.options.data)) {
        deregFunctions.push($scope.$watch(function () { return $scope.items; }, dataWatchFunction));
        deregFunctions.push($scope.$watch(function () { return $scope.items.length; }, dataWatchFunction));
      }

      var $table = $compile(template)($scope);

      if ($scope.options.cssClass) {
        $table.addClass($scope.options.cssClass);
      }

      //Rendering template.
      $element.html('').append($table);
      //destory watch
      $scope.$on('$destroy', function () {
        deregFunctions.forEach(function (deregFn) { deregFn(); });
      });
    }]
  };
}])
.directive('gridRowRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.options.rowTemplate || "<div ng-repeat=\"colDef in options.colDefs\" grid-col-render></div>";
      var $row = $compile(template)($scope);

      if ($scope.options.rowCssClass) {
        $row.addClass($scope.options.rowCssClass);
      }

      $element.append($row);
    }
  };
}])
.directive('gridColRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.colDef.cellTemplate || "<div><span class=\"view\">{{row.entity[colDef.name] CELLFILTER }}</span></div>";
      template = template.replace("CELLFILTER", $scope.colDef.cellFilter ? "| " + $scope.colDef.cellFilter : "");
      var $cell = $compile(template)($scope);

      if ($scope.colDef.cellClass) {
        $cell.addClass($scope.colDef.cellClass);
      }

      if ($scope.colDef.width) {
        $cell.css('width', $scope.colDef.width);
      }

      $element.replaceWith($cell);
    }
  };
}])
.directive('gridHeaderRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.colDef.cellHeaderTemplate || $scope.options.headerTemplate || "<div>{{ colDef.displayName || colDef.name }}</div>";

      var $row = $compile(template)($scope);

      if ($scope.colDef.headerClass) {
        $row.addClass($scope.colDef.cellClass);
      }

      if ($scope.colDef.width) {
        $row.css('width', $scope.colDef.width);
      }
        
      $element.replaceWith($row);
    }
  };
}]);

Sample