Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building a Kanban board application using AngularJS, WebApi, SignalR and HTML5

0.00/5 (No votes)
23 Aug 2014 1  
Building a Kanban board web application using AngularJS, WebApi, SignalR and HTML5

Introduction

In this article I will go through the steps of creating a Kanban board web application using AngularJS, WebApi, SignalR and HTML5. The idea behind this article is to show you how all these technologies can work together in harmony to produce an application that is easy to extend and manage. 

A View From the Top

Figure 1 shows how the various technologies stack up with each other to produce the whole solution

Figure 1: Overall technologies used in the solution

Figure 2 shows in more details how these components interact with each other. I will explain about every component in details later in the article. You can view the full size image from here

Figure 2: Overall architecture for the solution. View full size image here

The Server

The server components of the solution are ASP.NET MVC, WebApi and SignalR server. The MVC application acts as a container for the application so there is no great deal of implementation in it apart from one controller which serves the index page. The idea of having the MVC application as a container is to benefit from the bundling feature for the scripts among other benefits like layout page. As such the explanation for server components will focus on the WebApi and the SignalR server.

WebAPi

The BoardWebApiController holds the http service part of the solution. BoardWebApi http service contains the following services: Get, CanMove and MoveTask. Here is an explanation on every service.

The default Get method will return the columns in the Kanabn board along with their tasks. Listing 1 shows the implementation for this method. The method uses the BoardRepository to retrieve all the columns then serializes the data into JSON format then return it to the client in a form of HttpResponseMessage. I will explain about BoardRepository later in this article. 

[HttpGet]
public HttpResponseMessage Get()
{
    var repo = new BoardRepository();
    var response = Request.CreateResponse();

    response.Content = new StringContent(JsonConvert.SerializeObject(repo.GetColumns()));
    response.StatusCode = HttpStatusCode.OK;

    return response;
}

Listing 1: Get http service

The second service is CanMove. This service is called before a task can be moved from one column to another. In Kanban a task can be moved from left to right (Pull) only. This service takes the source and target column ids then returns a boolean to indicate whether a task can be moved between these two columns or not. Listing 2 shows the implementation for CanMove service

[HttpGet]
public HttpResponseMessage CanMove(int sourceColId, int targetColId)
{            
    var response = Request.CreateResponse();
    response.StatusCode = HttpStatusCode.OK;
    response.Content = new StringContent(JsonConvert.SerializeObject(new { canMove = false }));

    if (sourceColId == (targetColId - 1))
    {
        response.Content = new StringContent(JsonConvert.SerializeObject(new { canMove = true }));
    }

    return response;
}

Listing 2: CanMove http service

The last service is MoveTask. As the name indicates, this service will move one task from its current column to the target column. Listing 3 shows the implementation for MoveTask. Note how I am using the JObject to wrap the 2 parameters needed for this service. 

[HttpPost]        
public HttpResponseMessage MoveTask(JObject moveTaskParams)
{
    dynamic json = moveTaskParams;
    var repo = new BoardRepository();
    repo.MoveTask((int)json.taskId, (int)json.targetColId);
               
    var response = Request.CreateResponse();
    response.StatusCode = HttpStatusCode.OK;

    return response;
}

Listing 3: MoveTask http service

The MoveTask method on the repository will do the heavy lifting as we will see next.

The BoardRepository

The BoardRepository works on a collection of columns in memory. A column contains a list of tasks. A task holds a reference to its current column. Listing 4 shows the definition for Column and Task

public partial class Column
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }        
    public virtual List<Task> Tasks { get; set; }
}

public class Task
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public int ColumnId { get; set; }
}

Listing 4: Column and Task classes

The BoardRepository contains the following methods: GetColumn, GetTask, GetColumns, MoveTask and UpdateColumns.

GetColumns will return the collection of columns from cache if it is available otherwise it will create a new one. GetColumn returns a column for a passed column id and GetTask returns a task for a passed task id. Listing 5 shows the implementation for  GetColumns, GetColumn and GetTask

public List<Column> GetColumns()
{
    if (HttpContext.Current.Cache["columns"] == null)
    {
        var columns = new List<Column>();
        var tasks = new List<Task>();
        for (int i = 1; i < 6; i++)
        {            
           // Generating tasks is removed from this code snippet for brevity  
           HttpContext.Current.Cache["columns"] = columns;
        }
    }
    return (List<Column>)HttpContext.Current.Cache["columns"];
}

public Column GetColumn(int colId)
{
    return (from c in this.GetColumns()
            where c.Id == colId
            select c).FirstOrDefault();
}

public Task GetTask(int taskId)
{
    var columns = this.GetColumns();            
    foreach (var c in columns)
    {                
        foreach (var task in c.Tasks)
        {
            if (task.Id == taskId)
                return task;
        }
    }

    return null;
}

Listing 5: BoardRepository - 1 

The remaining two methods in the BoardRepository are MoveTask and UpdateColumns. MoveTask will take a task id and the target column id to which this task will be moving to. It then moves the task and calls UpdateColumns to update the columns collection in cache.

UpdateColumns will receive a collection of columns and updates the cache accordingly. Listing 6 shows the implementation of MoveTask and UpdateColumns.

public void MoveTask(int taskId, int targetColId)
{
    var columns = this.GetColumns();
    var targetColumn = this.GetColumn(targetColId);
    
    // Add task to the target column
    var task = this.GetTask(taskId);
    var sourceColId = task.ColumnId;
    task.ColumnId = targetColId;
    targetColumn.Tasks.Add(task);
    
    // Remove task from source column
    var sourceCol = this.GetColumn(sourceColId);
    sourceCol.Tasks.RemoveAll(t => t.Id == taskId);

    // Update column collection
    columns.RemoveAll(c => c.Id == sourceColId || c.Id == targetColId);
    columns.Add(targetColumn);
    columns.Add(sourceCol);

    this.UpdateColumns(columns.OrderBy(c => c.Id).ToList());
}

private void UpdateColumns(List<Column> columns)
{
    HttpContext.Current.Cache["columns"] = columns;
}

Listing 6: BoardRepository - 2

The Client

The client consists of 2 parts, namely: UI and AngularJS. boardService contains the SignalR proxy.

The KanbanBoard UI

The Kanban board in this application consists of 4 columns and they are: to do, in progress , test and done. Kanban boards can vary heavily depending on the team but most of the time you will find these columns in one way or another in any software development kanban board. Figure 3 shows the UI for our KanbanBoard application.

Figure 3: Kanban board UI

Kanban board markup

The markup behind the board contained in index.cshtml. The markup is controlled by an AngularJS controller (boardCtrl) which I will talk about in details later. Listing 7 shows the markup behind the board.

<div style="width:80%; margin: 0 auto 0 auto;">

    <div class="row" ng-controller="boardCtrl">

        <!-- Loading indicator: Listing 8 -->
        
        <!-- Board Columns: Listing 9 -->
        
    </div>

</div>

Listing 7: Kanaban board UI markup

The boardCtrl div consists of two div elements. The first one is the loading indicator and the second one is the columns collection div container. Listing 8 shows the definition of loading div.

<div ng-include="'/AppScript/busyModal.html'" ng-show="isLoading"></div>

Listing 8: Loading indicator

This div is using 2 AngularJS directives namely ng-include and ng-show. ng-include points to the html template which will be displayed for the loading indicator. Note how I am passing the name of the template in literal string. This tells AngularJS to treat this as a string and not as an expression. This is useful because in some scenarios you may want to include different templates depending on a given context so instead of a literal you can pass a scope variable and that variable may have different values.

ng-show on the other hand will hide or show the loading indicator based on the model variable isLoading which is set inside the boardCtrl.

The second part is displaying the columns collection. Listing 9 shows the markup behind displaying the board's columns.

<div class="col-lg-3 panel panel-primary colStyle" id="{{col.Id}}" 
     kanban-board-drop="over" ng-repeat="col in columns">

    <div class="panel-heading" style="margin-bottom: 10px;">
        <h3 class="panel-title">{{col.Name}}</h3>
    </div>

    <div class="thumbnail" draggable="true" kanban-board-dragg="task"
         ng-repeat="task in col.Tasks" style="margin-bottom: 10px;">

        <div class="caption">
            <h5><strong>{{task.Name}}</strong></h5>
            <p>{{task.Description}}</p>
            <p><a href="#" class="btn btn-primary btn-sm" role="button">Edit</a></p>
        </div>

    </div>
</div>

Listing 9: Displaying columns markup

This markup loops through a collection of columns using the ng-repeat directive. The inner ng-repeat loops through tasks in each column to display the tasks. The double curly brackets {{}} are AngularJS binding expression. I am using them to display the various information for columns and tasks. Note how I am assigning the id of the column's div container to the column id so I have a reference to it when we implement drag and drop later in this article.

The kanban-board-drop and kanban-board-dragg are both custom directives to handle drag and drop functionality between columns. I will explain about these directives in detail later but for now the kanban-board-drop property takes the name of the CSS class to be used with the hover effect on a the target column. The kanban-board-dragg directive takes the data item which is being dragged i.e. the task.

AngularJS

AngularJS development in this application can be divided into the following components: module, controller, service and custom directives. If you are familiar with AngularJS you will know that actually these are the main components in any AngularJS application. 

The AngularJS module

The AngularJS module acts as a container for all the components that can be shipped with it. Think of it like a Dynamic Link Library (DLL) in the .NET world. Listing 10 shows the implementation for our kanbanBoardApp module

// application global namespace
var sulhome = sulhome || {};
sulhome.kanbanBoardApp = angular.module('kanbanBoardApp',[]);

Listing 10: kanbanBoardApp definition

I am starting by defining a namespace called sulhome. This is a good practice so your JavaScript code will not mix with other global variables which can be defined somewhere else on the page (from other libraries). The module kanbanBoardApp is created and assigned to a variable on the namespace.

The board controller (boardCtrl)

In AngularJS, the role of the controller is to manage the model i.e. the data that will be bound back and forth from the view. boardCtrl is doing exactly this by maintaining the board model which is in this case the columns collection and the isLoading variable which controls the loading indicator. Listing 11 shows the implementation of boardCtrl.

sulhome.kanbanBoardApp.controller('boardCtrl', function ($scope, boardService) {
    // Model
    $scope.columns = [];
    $scope.isLoading = false;

    function init() {
        $scope.isLoading = true;
        boardService.initialize().then(function (data) {
            $scope.isLoading = false;
            $scope.refreshBoard();
        }, onError);
    };

    $scope.refreshBoard = function refreshBoard() {        
        $scope.isLoading = true;
        boardService.getColumns()
           .then(function (data) {               
               $scope.isLoading = false;
               $scope.columns = data;
           }, onError);
    };

    $scope.onDrop = // Listing 16 
    
    // Listen to the 'refreshBoard' event and refresh the board as a result
    $scope.$parent.$on("refreshBoard", function (e) {
        $scope.refreshBoard();
        toastr.success("Board updated successfully", "Success");
    });

    var onError = function (errorMessage) {
        $scope.isLoading = false;
        toastr.error(errorMessage, "Error");
    };

    init();
});

Listing 11: kanbanBoardApp definition

The boardService gets injected into the controller. As we will see later, this service is responsible for communicating with the http service. The SignalR proxy also resides in this service (boardService).

The controller starts by defining the model variables namely columns and isLoading. The init function is called automatically once the controller is loaded. This function calls the service to initialise the SignalR proxy then calls refreshBoard which in turn calls the service to retrieve the columns. The onError function is used to log an error which may occur from communicating with the http service to the screen. I am using John Papa’s toastr to show the error.

The controller also listens to an event named refreshBoard. This event will be raised from SignalR proxy and it will allow clients to refresh their board to get the latest columns. This event is raised once a task is moved from one column to another so it makes sure that all clients are in synchronisation with each other i.e. all clients have the latest board.

One method is missing from this controller which is the onDrop method. I will talk about this method when I discuss drag and drop later in this article.

The board service (boardService)

The board service is responsible for communicating with http services and SignalR server. The first 3 methods getColumns, canMoveTask and moveTask calls the corresponding http services explained earlier and return the result back to the caller i.e. the controller (boardCtrl). Listing 12 shows the implementation for these methods.

sulhome.kanbanBoardApp.service('boardService', function ($http, $q, $rootScope) {
    
    var getColumns = function () {
        return $http.get("/api/BoardWebApi").then(function (response) {
            return response.data;
        }, function (error) {
            return $q.reject(error.data.Message);
        });
    };

    var canMoveTask = function (sourceColIdVal, targetColIdVal) {
        return $http.get("/api/BoardWebApi/CanMove", 
                        { params: { sourceColId: sourceColIdVal, targetColId: targetColIdVal } })
            .then(function (response) {
                return response.data.canMove;
            }, function (error) {
                return $q.reject(error.data.Message);
            });
    };

    var moveTask = function (taskIdVal, targetColIdVal) {
        return $http.post("/api/BoardWebApi/MoveTask", 
                         { taskId: taskIdVal, targetColId: targetColIdVal })
            .then(function (response) {
                return response.status == 200;
            }, function (error) {
                return $q.reject(error.data.Message);
            });
    };
    
    var initialize = // Listing 12: boardService -2    
    var sendRequest = // Listing 12: boardService -2

    return {
        initialize: initialize,
        sendRequest: sendRequest,
        getColumns: getColumns,
        canMoveTask: canMoveTask,
        moveTask: moveTask
    };
});

Listing 12: boardService -1 

The methods getColumns, canMoveTask and moveTask uses AngularJS $http service to communicate with kanban board http service. These functions process the response to return the data instead of sending the raw response. If an error occurs, the request will be rejected using the promis service ($q) and the onError method will be called on the controller (boardCtrl). These functions also uses the short version of $http service which is $http.get and $http.post. As you can notice these methods matches the http verbs for get and post. Using this short version will save you from specifying the verb i.e. the verb will be GET when using $http.get and POST when using $http.post.

Listing 13 shows the remaining implementation for boardService. The remaining implementation deals with SignalR proxy and evetns.

sulhome.kanbanBoardApp.service('boardService', function ($http, $q, $rootScope) {
    var proxy = null;

    var initialize = function () {

        connection = jQuery.hubConnection();
        this.proxy = connection.createHubProxy('KanbanBoard');

        // Listen to the 'BoardUpdated' event that will be pushed from SignalR server
        this.proxy.on('BoardUpdated', function () {
            $rootScope.$emit("refreshBoard");
        });

        // Connecting to SignalR server        
        return connection.start()
        .then(function (connectionObj) {
            return connectionObj;
        }, function (error) {
            return error.message;
        });
    };

    // Call 'NotifyBoardUpdated' on SignalR server
    var sendRequest = function () {        
        this.proxy.invoke('NotifyBoardUpdated');
    };    
});

Listing 13: boardService -2

The private variable proxy holds a reference to the SignalR proxy which is created in the initialize function. The initialize function connects to SignalR server and creates an event listener which listens to events being pushed from the SignalR server.

When a user moves a task to another column the SignalR server will push the BoardUpdated event to all clients. This event listener then make use of AngularJS $emit to publish the event across the AngularJS application. If you recall that the boardCtrl contains an event listener which listens to the refreshBoard event and calls refreshBoard method accordingly. This is how the Kanban board stays up to date across all clients.

The sendRequest method calls the SignalR server method to indicate that a task has been moved so the SIgnalR server can send a push event to all clients so they can update their board. As you will see later that this method (sendRequest) will be called from the onDrop method which is called once a task card is successfully dropped onto another column.

Drag and Drop directive

Drag and Drop functionality are programmed against HTML5 drag and drop API. So it will work as long as the browser supports HTML5 drag and drop API.

The manipulation of DOM in AngularJS should happen in a directive. For example hiding an element like a div based on a condition is accomplished by using ng-show which is an attribute directive i.e. it is used as an attribute on a DOM element. Drag and Drop functionality is no different from hiding an element, from a DOM manipulation perspective. As such it is been handled in its own directive.

Listing 14 shows the implementation for kanbanBoardDragg directive. This directive will be applied on the item that you drag so in our application that will be the task card.

sulhome.kanbanBoardApp.directive('kanbanBoardDragg', function () {
    return {
        link: function ($scope, element, attrs) {

            var dragData = "";
            $scope.$watch(attrs.kanbanBoardDragg, function (newValue) {
                dragData = newValue;
            });

            element.bind('dragstart', function (event) {
                event.originalEvent.dataTransfer.setData("Text", JSON.stringify(dragData));
            });
        }
    };
});

Listing 14: kanbanBoardDragg directive

The default type of directive is attribute, as such this is an attribute directive. The link property points to a function which will be called when the directive is loaded. This function receives 3 parameters:
$scope: This is the scope of the controller in our example
element: is the element on which this directive is applied, in our case this is the task card div
attrs: is an array of all the attributes on the element.

Note the usage of the $watch function on the $scope. AngularJS will treat the value of the kanbanBoardDragg attribute as an expression and it will reevaluate it every time it is changed. In our case this value is the task object. The dragstart event handler will store the dragData in the dataTransfer attribute of the event. This will allow us to retreive this data when the item is dropped i.e. ondrop event as you will see in the kanbanBoardDrop directive.

Listing 15 shows the implementation for kanbanBoardDrop directive. The dragOverClass holds the name of the css class that will be applied on the target column to which the task will be dropped (moved). The class is set in the kanbanBoardDrop attribute.

sulhome.kanbanBoardApp.directive('kanbanBoardDrop', function () {
    return {
        link: function ($scope, element, attrs) {

            var dragOverClass = attrs.kanbanBoardDrop;

            //  Prevent the default behavior. This has to be called in order for drob to work
            cancel = function (event) {
                if (event.preventDefault) {
                    event.preventDefault();
                }

                if (event.stopPropigation) {
                    event.stopPropigation();
                }
                return false;
            };
            
            element.bind('dragover', function (event) {
                cancel(event);
                event.originalEvent.dataTransfer.dropEffect = 'move';
                element.addClass(dragOverClass);                
            });

            element.bind('drop', function (event) {
                cancel(event);
                element.removeClass(dragOverClass);                
                var droppedData = JSON.parse(event.originalEvent.dataTransfer.getData('Text'));
                $scope.onDrop(droppedData, element.attr('id'));

            });

            element.bind('dragleave', function (event) {
                element.removeClass(dragOverClass);
            });
        }
    };
});

Listing 15: kanbanBoardDrop directive

Dragover and dragleave event handlers control the CSS class by adding or removing it from the column respectively. I am using originalEvent instead of event because the latter is a jQuery wrapper around the originalEvent and hence it doesn't contain the methods which I need to use with drag and drop events.

dropEffect = 'move' is a visual effect that will appear on the target to which the task will be dropped. Other values are none, link and copy. Setting this value doesn't have any effect on the functionality but it is a UI indicator to the user to show what will be the effect of dropping a dragged item on the target. For example if you are copying items from one list to another using drag and drop then the dropEffect will be copy.

The drop event handler reads the data which was stored in the drag event then calls the onDrop method which resides in the controller (boardCtrl). The onDrop method accepts the task object and the target column id. The source column id can be read from the task object. Listing 16 shows the implementation for the onDrop method which resides in the boardCtrl

$scope.onDrop = function (data, targetColId) {        
    boardService.canMoveTask(data.ColumnId, targetColId)
        .then(function (canMove) {                
            if (canMove) {                 
                boardService.moveTask(data.Id, targetColId).then(function (taskMoved) {
                    $scope.isLoading = false;                        
                    boardService.sendRequest();
                }, onError);
                $scope.isLoading = true;
            }

        }, onError);
};

Listing 16: OnDrop method

The onDrop method checks that the task can be moved and if yes it calls the moveTask service method to move the task. Once the task is moved then the isLoading flag is set to false and sendRequest method is called. As mentioned earlier, sendRequest will call the SignalR server to send a push event for all the clients so they can update their board.

Conclusion

In this article I have explained how to build a Kanban board web application using AngularJS, SignalR, WebApi and HTML5. I hope that by the end of this article you have a good understanding on how these technologies can work together to achieve the end result. 

History

V 1.0 - 24.08.2014: Created

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here