Customise data table

In this post I will show how to build the data table in to your app and some basic customization of the data table.

We had a lot of requirements what we expected from our data table implementation.

  • Custom data source – at a time we have only the data of one page
  • Hiding columns
  • Ordering by header

These are really core functionalities but according to our special data source all of this functionalities needed a little trick to work fine.
After reading this post you will be able to form the original data table according to your needs like we could do it as well.

Let’s start from the beginning with adding the rows and the header titles to the table.

The original define method

It is very easy to get done with dataTables. You can do it at creation time with setting the property ‘aoColumns’ for header titles and the property ‘aaData’ for all the rows of the table.

$("#my-data-table").dataTable({
                        "aoColumns": headers,
                        "aaData": rows });

Both properties wait an array of primitive types. Therefore, if you work with JSON objects then you need to convert the objects and you will lost your origin objects in the table. If you want to handle later one row from the table first you have to convert it back to an object. But we will check how to handle this.

For converting the objects of the data source to the correct form for the data table we have two methods a ‘prepareHeader’ and a ‘prepareRows’.

To illustrate the conversion we will use in this post the data from the sample app that you can get from github.
In this sample app it is used a fake data source and the data is in the following format:

 	    { id : 3, engine: "Trident", browser: "Internet Explorer 6.0", platform: "Win 98+", version: 6, grade: "A" },
            { id : 4, engine: "Trident", browser: "Internet Explorer 7.0", platform: "Win XP SP2+", version: 7, grade: "A" },
            { id : 5, engine: "Gecko", browser: "Firefox 1.5", platform: "Win 98+ / OSX.2+", version: 1.8, grade: "A" },
            { id : 8, engine: "Webkit", browser: "Safari 1.2", platform: "OSX.3", version: 125.5, grade: "A" }

Header

First I create the collection of the header title.

                var prepareHeader = function (header) {

                    headers = [];

                    var sortables = scope[attrs.sortingProperties];
                    var toHide = scope[attrs.hidingProperties];

                    //get the property names for header titles
                    for (var item in header) {

                        if (!toHide || toHide.indexOf(item) == -1) {

                            var sortable = attrs.allSortable == "" ? true : false;

                            if (sortables && sortables.indexOf(item) != -1) {
                                sortable = true;
                            }

                            headers.push({ "sTitle": item, "bSortable": sortable });
                        }
                    }
                }

The parameter of this function is the first row of the data source collection. To set the title of the headers I get the property names from the first object of the data source.

It has only one restriction that the first object has to contain all of the properties we want to show in the table. If the object of the first row contains only 3 properties and all of the other objects have 5, the table will show only 3.

Sorting and hiding

As you could see in the method it was some extra. The option to hide properties from the data table and the option to sort by a property.

Through the API of the directive the user can define ‘sortingProperties’ and ‘hidingProperties’ as HTML attributes. Each property requires an array of strings.
(From the last commit I support case-insensitivity by giving the name of the property inside the array.)

In the directive I get the two arrays from the scope. (The scope in my directive is the “main” scope coming from the controller.) I can get the arrays through indexing the scope with the name of the array coming from the ‘attrs’ property of the directive.

I walk through all of the header elements and if the certain property name is not part of the property names in the ‘to hide’-collection than I add it to the headers.

Before adding the property name actually to the headers I check if the user wants to have the functionality to order by this property or not. According to the chose? I set the ‘bSortable’ property of the header to true or to false.
If the ‘sortingProperties’ collection is empty than I set false to ‘bSortable’ for all of the header elements.

The last step in the defining the header titles process is to add the result of this function to the data table with setting the ‘aoColumns’ property value to this array.

Rows

From all of the other objects in the source collection we can create the rows.

var prepareRows = function (source) {

                    var rows = [];
                    var toHide = scope[attrs.hidingProperties];

                    for (var i = 0; i < source.length; i++) {

                        var row = [];

                        for (var item in source[i]) {

                            if (!toHide || toHide.indexOf(item.toLowerCase()) == -1) {
                                row.push(source[i][item]);
                            }
                        }

                        rows.push(row);
                    }

                    return rows;
                }

In this method we use only the values of the properties. One object will be represented with an array of property values.

‘aaData’ waits a two dimensional array. The array represent the rows of the data table and the arrays in the array are the individual rows containing the property values. In default case.

Handling the data change

We want to set the header rows only once but the rows inside the table are changing according to the new settings. Pagesize, new page, sorting and so on.

At the certain time we have only rows of one page according to all of the settings (page size, page index, order, sort). But only this amount of the data comes from the server.

At the time of the data table creation we have only the data of the first page. The first page has the default settings. (Page size 10, page index 0, no order, no filter.)
And every time the user set something we send a request to the server and get the data of the new page with the new settings.

Because of this we have two functions. One to create the first page and a refresh function to clear the actual page and set the new rows.

The first function gets the source of one page and gets the values of the properties from the objects and if the user doesn’t want to hide the column it will add it to the table.

		var prepareRows = function (source) {

                    var rows = [];
                    var toHide = scope[attrs.hidingProperties];

                    for (var i = 0; i < source.length; i++) {

                        var row = [];

                        for (var item in source[i]) {

                            if (!toHide || toHide.indexOf(item) == -1) {
                                row.push(source[i][item]);
                            }
                        }

                        rows.push(row);
                    }

                    return rows;
                }

Refreshing the data clears the actual page with the built-in function ‘fnClearTable’. After we have an empty table it will prepare the rows with the function used for the first page to paste them into the table.
Last step to add the rows to the table with the built-in ‘fnAddData’ function and decorate the rows if needed. Decoration means inline buttons and setting the row selection mechanism.

var refreshData = function (source) {

                    originalSource = source;

                    //check if it is the end of the elements or probably it is more
                    elementsMaxReached = source.length < pageSize;

                    table.fnClearTable();

                    var rows = prepareRows(source);

                    table.fnAddData(rows);

                    decorateDataTableOnChange();
                };

The word “probably” in the comment is because our server side mechanism. We send the request but we can’t decide forward that it will give us more data back or not.
If the length of the data from the server is less than the actual page size it is obvious that it has no more data but it has the chance that we get data with the length that is equal with the page size but there is no more data on server.
But at the time we have no info about it. We will send a request to get the next page and we get nothing back. We have to deal with this situation.

Fortunately, if you give the dataTables an empty collection of rows with the ‘fnAddData’ than it will handle it.
It will write out “No data available in table” and now we know that there is no more page and will show only the previous navigation button.
Problem solved.

And we have the data table filled with the data of one page and the mechanism to change that on request.
The main request is the paging itself. The next post will be about it.

One last thing to this post. I mentioned it earlier that you loose the original object in the data table. Of course it is not acceptable. We need to get back our object for edit, for delete, for select and so on.

Two way connection between the original source and the data table

But how can we get back our original objects? – you would probably ask.

It could be easy. We need to store the original data inside the directive and we can later get the objects from it identified with the _id property of the row.

But in our case we could not choose this option because we didn’t want to show the _id in the table so it is a hidden property and not contained by the row at all.

But dataTables has the functionality to store the original order. So you can index the original source with the indexes of the rows in the table.

var originalIndex =$("#my-data-table tbody tr").context._DT_RowIndex;

var actualIndex = $("#my-data-table tbody tr").context.rowIndex;

So cool! : )

Spoiler

The next post will be about more customising and we will check on big change detailed. This change will be the rewriting of the DataTables pagination that is labeled by Allen with “Pagination plug-ins are (generally!) the most complicated of the plug-ins that DataTables allows.” We will go through it together and hopefully you will find it after reading the post not so difficult as it first looked like.

Advertisements

Continuous integration on server side – Part 2

Create own grunt task

In the previous post I mentioned an extra task, cleaning folders and removing the folders as well.

We found a tool to clean a folder called grunt-contrib-clean.
But we had two problems with that tool.

  • It doesn’t support removing the folder itself. It only clears the content of the specified folder.
  • You can use relative path from a specified base directory but you can’t use back navigation in the path.

At this point I need to admit that I didn’t do a brighter resource after checking the grunt-contrib-clean tool because I wanted to write an own grunt task and it was good opportunity to start writing one.

At first I started with checking the documentation on the webpage of the grunt.
I thought that it will be a good start but after reading the “Create own task” section I didn’t have the clue how to really start the coding.

Rather as a second option I checked the implementation of the uglify and the yuidoc tasks.
As I mentioned it in the previous post we use that two tools in our application and we found grunt task to execute them as well.

And that’s the part of the javascript/opensource world I love the most. You can read and learn from other’s implementation.
There are really good examples outside, like the two grunt tasks I chose.

After checking this two implementations I had the clue how to start.

My first implementation was this:

        
        module.exports = function (grunt) {

    	grunt.registerTask('myclean', "Remove folder and it's content recursive", function () {
    		grunt.log.ok();
    	}

and it worked! : )

I just loaded it into my Gruntfile.js:

 
  	grunt.loadTasks('core/tasks'); is the way to load it into the the actual Gruntfile.js

and after running it in my terminal the result was:

terminal_ok

At his point I had the skeleton and I could start to fill it with content.

As I mentioned it earlier I wanted to use relative path in the settings of the task.
I wanted to use ‘..’ in the path to step back one folder.

And I wanted to remove not just the full content of the specified folder but the folder itself as well.

Let’s start coding our own task.

As the result we wanted to see the following customisable task in the Gruntfile.js:

You can get the user setting of your task in the implementation of the task with the options command

     	
    	        var options = this.options({
        });

It loads you the options from the gruntfile.
Of course you need to check the existence of the expected option

                    
        if (!options.cleanPath) {
            grunt.log.writeln('cleanPath is empty or not defined');
            return false;
        }

After that you are safe here and you can use the ‘cleanPath’ filled with the value that the user specified

I show my implementation about creating the right path to clean:

        
        var baseDir = path.normalize(__dirname + '/../..'); //Server folder
                var parts = value.split('/');
                var endPath = '';

                for (var i = 0; i < parts.length; i++) {

                    if (parts[i].indexOf('..') != -1) {
                        baseDir += '/../';
                    } else {
                        endPath = path.join(endPath, parts[i]);
                    }
                }

                var actualDir = path.normalize(baseDir);

                var preparedPath = path.join(actualDir, endPath);

                deleteFolderRecursive(preparedPath);

With this implementation I can set the clearPath in my Gruntfile with the following value: “../../Deployment/Server”
and it will navigate from the current folder (Source/Server) back to the main project folder and then into the other sibling folder I gave to it.

And the delete part:

         
        var deleteFolderRecursive = function (cleanPath) {

            if (fs.existsSync(cleanPath)) {
                fs.readdirSync(cleanPath).forEach(function (file) {

                    var curPath = cleanPath + "/" + file;
                    if (fs.statSync(curPath).isDirectory()) {
                        deleteFolderRecursive(curPath);
                    } else { // delete file
                        fs.unlinkSync(curPath);
                    }
                });
                fs.rmdirSync(cleanPath);
            }
        };      

…witch deletes the specified folder and its content as well.

I don’t want to explain my code because it not so complicated I just want to highlight some grunt and node feature that I used and that are good to know for you as well

  • __dirname – gets the actual directory path, where the file is located.
  • path.normalize – it will parse the ‘..’ back navigation if it is at the end of the path
    • folder1/folder1.1/.. –> folder1
    • folder1/folder1.1/../folder1.2 –> will not navigate to folder1/folder1.2
  • path.join – after splitting the file path by ‘/’ it will create a correct path from the parts.
  • grunt.util has many other useful functions
    I used only _.each() to iterate through the input array but there are methods to check the types of the input parameters and a lot of other good stuff.

Task vs. Multitask

If you create task with the “registerTask” method call you can define the task later only with one settings.

  
        myclean: {
                options: {
                    cleanPath: ['./docs', '../../Deployment/Server']
                }
            }

But what if you don’t want to always clear all the paths you defined at first.
What if in certain situations you only want to remove he ‘docs’ folder.

With ‘registerTask’ you can’t do that, BUT with ‘registerMultiTask’ it is possible.
You only need to rewrite the method name. The content of the task can remain the same and grunt will do for you everything else.

From that time you can set more execution options and name them with an arbitrary name.

  
        myclean: {
            default: {
                options: {
                    cleanPath: ['./docs', '../../Deployment/Server']
                }
            },
            onlyDocs: {
                options: {
                    cleanPath: ['./docs']
                }
            }
        }

and later that you can use them like:

          
	grunt.registerTask('default', ['myclean:default', 'yuidoc', 'uglify']);

	grunt.registerTask('onlyDocs', ['myclean:onlyDocs','yuidoc']);

After that you can run the grunt with different switches:

Write ‘grunt’ into the terminal and it will run the ‘grunt default’. First it cleans every folder, then creates the documentation and then uglifies and copies the files into the Development folder.
And running ‘grunt onlyDocs’ executes the second set of tasks. It clears only the ‘docs’ folder and regenerate the documentation.

That’s it.

Spoiler

This was our start with grunt on server side. We created the base tasks and a self-made task as well. But the journey doesn’t end here. Int he following post of this series we will check how to configure grunt to run tests and run our express server.

Continuous integration on server side – Part 1

I will start this post from a bit far… Far, because I will start from our AngularJs client. : )

I start with Yeoman.
Yeoman, if you work with AngularJs you have probably heard about yeoman.

If you don’t you really need to check it on http://yeoman.io/

It makes your life much easier!
“Yeoman is more than just a tool. It’s a workflow.” “Modern workflows for modern webapps”

It is a collection of tools:

  • Yo – It generates you a good skeleton to work on an AngularJs project.
  • Grunt – It helps you to host the app with only one command – grunt server (and a lot of other things it can do for you but this is the easiest one and the first one you will meet.)
  • Bower – It helps you to find and install easily components to AngularJs. (Like the node Package Manager in the Node.js world)
  • To have the whole stack of tools I would suggest one more tool, a Bower module: Generator-Angular

Generator-Angular can generate for you all the necessary components of an AngularJs app. Like services, controllers, directives and so on with a simple command:

yo angular:directive myDirective 

and it will generate you everything to have a directive as part of your app.

But we are working on a server side now. Losing all the good staffs only because you work on server side would make you feel bad. As it would make us sad too. BUT Grunt helps you on server side as well!

What is this Grunt exactly? – you could ask.

Grunt is a task runner tool to automatize the execution of your repetitive tasks.

If you are a developer Grunt makes your life easier with automatising your not ‘coding’-tasks and
if you are a project manager it gives you well sounding-words to improve your project’s marketing.

Automation and continuous integration. They are really nice words for public but they are really important things in development as well.

There are tasks that you need to do repetitive and doing them always manual would be a lot of work.

But you can automatize your tasks!

If you think of automatizing something in software development, the most common thought is testing. It is very important of course and automatizing the test runs is a really good idea (we do that as well)
but there are other smaller tasks as well that you can automatize. Like generating documentation, minifying, uglifying the source code and so on.

You need to execute some tasks very often. Maybe some tasks more than once a day. With that we arrived to the concept of Continuous integration*.
*Continuous integration (CI) is the practice, in software engineering, of merging all developer working copies with a shared mainline several times a day.

And here comes the grunt for. It will support you to do all of this.

Now we know everything about the ‘Why?’ but let’s see the ‘How?’ part of all of this.

Let’s see how to use grunt on server side!

(I will write about CI with grunt in three posts. This is the first one with a longer intro and with integration of small third-party tasks, in the second we will create an own task and check how to use it and the last one will be about testing and automatizing the test runs with grunt.)

Our requirements were:

  • We wanted to generate documentation to our project for the customer.
  • We wanted to prepare a deliverable package with the minified and uglified source.
  • Extra was the request to clean the folders before regenerating the components.

For almost everything exists a grunt component. If you use a third party library to generate doc or to test you will find the grunt task version of it.

– To minify and uglify the source code we found the grunt-contrib-uglify.
– We use YuiDoc to create our documentation and of course there is a task to use yuidoc with grunt – grunt-contrib-yuidoc.

This modules are available on github, they have simple documentations and it is easy to use them.

Integrate Grunt into your project

To integrate Grunt you need to add only one file to your project called Gruntfile.js.

The content of the file needs to be inside a module.exports block

module.exports = function (grunt) {
    ...
};

…and the file have to contain 3 section:

1, To configure the modules you use:

    grunt.initConfig({
    	...
    	});

2, To load the tasks (the sources)

    grunt.loadNpmTasks('module_name');

    grunt.loadTasks('your_module');

3, To specify the grunt task collections you want to run later

    grunt.registerTask('default', ['myclean:default', 'yuidoc', 'uglify']);

    grunt.registerTask('onlyDocs', ['myclean:onlyDocs','yuidoc']);

Our configuration for the two modules that I mentioned before are really simple:

uglify: {
            options: {
                banner: '/*! innio <%= grunt.template.today("yyyy-mm-dd") %> */\n'
            },
            my_target: {
                files: [
                    {
                        expand: true,
                        src: ['./core/*.js', './routes/*.js'],
                        dest: '../../Deployment/Server',
                        ext: '.min.js'
                    }
                ]
            }
        }
yuidoc: {
            compile: {
                name: '<%= pkg.name %>',
                description: '<%= pkg.description %>',
                version: '<%= pkg.version %>',
                url: '<%= pkg.homepage %>',
                options: {
                    paths: './',
                    outdir: './docs/'
                }
            }
        }

They are only from the default examples on the module’s github pages. We only parametrized them other for our purpose.

Tip 1. Uglify creates by default one file for everything inside the source folder but with the following config it generates separated folders and files with the minified versions of it and structures them as the original structure looked like.

And that’s it.

module.exports = function (grunt) {

    // Project configuration.
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        uglify: {
            options: {
                banner: '/*! innio <%= grunt.template.today("yyyy-mm-dd") %> */\n'
            },
            my_target: {
                files: [
                    {
                        expand: true,
                        src: ['./core/*.js', './routes/*.js'],
                        dest: '../../Deployment/Server',
                        ext: '.min.js'
                    }
                ]
            }
        },

        yuidoc: {
            compile: {
                name: '<%= pkg.name %>',
                description: '<%= pkg.description %>',
                version: '<%= pkg.version %>',
                url: '<%= pkg.homepage %>',
                options: {
                    paths: './',
                    outdir: './docs/'
                }
            }
        }
    });

    /*Load tasks*/
    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.loadNpmTasks('grunt-contrib-yuidoc');


    /*Register tasks*/
    grunt.registerTask('default', ['yuidoc', 'uglify']);
};

To execute this you only need to write the command “grunt” into the terminal and hit Enter.

Spoiler

The next post will be about creating your own task. It is a more adventures challenge but we will go through it step by step.

Make data table reusable

In the previous post we checked the project itself behind this blog post series and the library that we chose as a base of our implementation.

Our next task with the base library in our hand was to place the dataTables inside our Angular.js app. This post will be about the steps to achive this.

The best way to create a reusable component in angular is to create a directive. So I wrapped the dataTables implementation inside a directive.

Let’s see how!

The directive

angular.module('angularTableApp')
    .directive('myDataTable', function ($parse) {
        'use strict';

        return {
            template: '<table class="display table table-striped table-hover" id="my-data-table"></table>',

            link: function postLink(scope, element, attrs) {

			…

			$("#my-data-table").dataTable(...);
			…	

		}
	};
);

I will not explain all the settings of an angular directive in this post because I plan to write posts about angular.js and about directives as well.
( But if you have never heard about directives you can check the official page. )

There are parts that we need to know at this point:

  • We have an angular module, the main module with the name ‘angularTableApp’ and a directive is a reusable UI elment we can define
  • ‘myDataTable’ is the name of the directive → my-data-table will be the name of the directive in HTML
  • This directive will be available as an attribute of a HTML element.
  • If a div has our attribute, angular will build the content of the template inside the div.
  • In the ‘template’ I defined a HTML table with the Twitter Bootstrap classes and with the id to identify it. (Instead of id you can use class to have more table on one page.)
  • The div with the id ‘customHeader’ is a placeholder for the custom header controls.
  • In the ‘link’ function we are after the compile phase. We have all the DOM elements in place – the content of the template is inside the div – and we can manipulate them.

This ‘link’ function has three parameters

  • ‘scope’ – The scope from the controller
  • ‘element’ – The element on which the my-data-table attribute is
  • ‘attrs’ – The attrs gives us all the other attributes on the container div

The HTML part

We need to mark the HTML element with the attribute of the directive – ‘my-data-table’ – and we can add other attributes which we want to use later inside the directive:

<div my-data-table 
		my-source="dataSource" 
		refresh="refresh(data)" 
		header="headerControls" 				//optional
		page-sizes="pageSizes" 					//optional
        sorting-properties="sortingProperties"  //optional
        hiding-properties="hidingProperties" 	//optional
        delete-row="deleteRow(item)" 			//optional
        edit-row="editRow(item)" 				//optional
        select-row="select(item)" 				//optional
        >
</div>

‘my-source’ and ‘refresh’ are required parts of the data table. The others are only optional tags.

  • ‘my-source’ is the data source of the actual page of the table.
  • ‘refresh’ is the callback to react on the change of the table queries.

Inside the directive

On the script side, inside the directive’s link function we will use now only two from the paramaters.

We need attrs to to reach all the optional parameters of the container element and the scope to get the bound values this attributes from the actual scope of the page.

We can bind functions and properties of the actual scope as attributes of the HTML element.
On the directive’s side there are two different method to get the bound items correctly.
One to get the functions and one to get the properties.

How to handle bound functions inside the directive

We can get the name of the function from the attributes and after that we need to call the $parse function of angular with the name of the function.

var refreshHandler = $parse(attrs.refresh);

‘$parse’ can be requested from angular and get through dependency injection

angular.module('angularTableApp').directive('myDataTable', function ($parse) { … }

After parse you get the function itself and you can call it in an angular specified way.

scope.$apply(function () {

                        refreshHandler({data: conditions}, scope, { $event: event });
                    }); 

Tip 1 – Check noop
If you try to parse function by the attribute name but the user has not defined the attribute – in this case ‘refresh’ – it will not work.
It will give you an empty function with the name ‘noop’. (Angular built-in function.)
If it’s a ‘noop’ function and you call it like it would be your function, nothing will happen. It will not throw an exception but will not do what you request from it.

To avoid this malfunctioning you can check the result of the parse:

if (refreshHandler != angular.noop) {/*activate the option to call this and/or do the call*/}

Tip 2 – How to use parameters
If you want to add parameters to the function, they have to be defined in parse time.
Which means that in HTML you need to write refresh=”refresh(data)”.

In this case you can call the function later with the ‘data’ parameter.
If you write in html only refresh=”refresh()” it will not handle the parameter. It will not throw an exception. It will only ignore the parameter.

If you registered the parameter you can pass it to the function and use it in the controller.

Tip 3 – The way of function call
The from attribute parsed functions are outside the angular environment / angular loop so we can’t just call them as an original function of the angular scope.
We need to call them inside the apply method of the angular’s scope.

scope.$apply(function () {

                        refreshHandler({data: conditions}, scope, { $event: event });
                    }); 

Calling the function requires to pass the scope and to set the $event to it. (The way of Angular.js)

How to handle bound properties inside the directive

It is much easier because we have the scope of the controller. (Remember the scope is one of the parameters of our ‘link’ function.)

We only need to index the scope with the property name.

	scope[attrs.header]

The property name what we want to reach comes from the the other parameter of the function, from ‘attrs’.
That’s it!

Summary

These were the steps of the integration. Now we have the data table as a reusable component to use it inside an Angular.js app. We created the channel to communicate between our directive and the page’s scope.
We can hide the implementation of all the features that we want to add to our table inside this directive and we can publish to the user of our table a nice API.

In the next post we will check how to fill the table with features and I will show the implementation of the features from the sample app.

Data table in angular

A whole new extension from me! Download it, use it, rate it! Any feedback is welcomed!

banner

Have you ever found yourself in need of a fully customisable data table in an angular app?

No? – Than this is not the post you are looking for.

We needed one anyway.

So, first we started to look for a good data table implementation.
After some researching we found a pretty one, which is called DataTables.
It has many cool functions and it is well documented as well. But its best feature is that it is fully customisable. Exactly what we wanted!

How easy is the use the dataTable?
1. Create a <table> HTML tag with an id.
2. Write the following call inside your script.

$(document).ready(function() {
$('#theId').dataTable();
} );

3. Ready.

It looks cool and has a lot of built-in functionalities.

dataTables-original-table

So we had the base implementation but we had a lot of custom requirements for it:

  • We needed it as an angular.js directive.
  • We use Twitter Bootstrap to design our UI and to style the data table as well.
  • Custom header controls for many other filtering options.
  • Custom paging. We had the dodgiest requirement here.
    (To explain this it will be devoted a whole post.)
  • Custom row selection. Row selection and navigation with the arrow keys.
  • Custom controls for rows. Inline delete, edit options to rows.
  • Custom column hiding and sorting.

And we wanted all of this through a friendly API. How we succeeded?

Check the result!

The usage of my implementation in HTML:

&lt;div my-data-table
   my-source=&quot;dataSource&quot;
   refresh=&quot;refresh(data)&quot;
   header=&quot;headerControls&quot; //optional
   page-sizes=&quot;pageSizes&quot; //optional
   sorting-properties=&quot;sortingProperties&quot; //optional
   hiding-properties=&quot;hidingProperties&quot; //optional
   delete-row=&quot;deleteRow(item)&quot; //optional
   edit-row=&quot;editRow(item)&quot; //optional
   select-row=&quot;select(item)&quot; //optional
&gt;
&lt;/div&gt;

And the result looks like this:

dataTables-my-table

This sort blog post series will be about showing the main steps of the implementation.

My implementation with a small sample app is available on github. So you can check it out, download it and try it for yourself!

By reading the upcoming blog post you can get the big picture about the frameworks of the development. You can get the answers of the why?-s and how?-s.

Check the next post in this series!