facebook
Bob at Genuitec
Virtual evangelist at large. The face of Genuitec often appearing in graphics. Pen-name for our more shy writers of content.
Posted on Jul 13th 2017

In this tutorial, we will be creating a simple Kanban board with Angular 4. A Kanban board is a work and workflow visualization tool that enables you to optimize the flow of your work, a basic Kanban board has a three-step workflow: To Do, In Progress, and Done.

Live Demo

kanban-board-blank

We represent every work item as a separate card on the board to allow us to track the progress of work through the workflow in a highly visual manner. For this tutorial, I am using Angular IDE, though you can use whatever editor you prefer.
Let’s get started by opening up Angular IDE, and creating a new project `SimpleBoard`.
kanban-new-project

kanban-board-sample-task-list

Taking a look at the Kanban board image above, we can identify two visual components, namely Lists and Cards, but a third component is not visible. To create a component, we right click on ‘app’ directory in ‘src’, select New > Component. Let us create `BoardComponent`.
kanban-board-component
Next, we edit the files created by Angular IDE as follows:

src/app/board/board.component.ts

import { Component, OnInit } from '@angular/core';
import { CardStore } from '../CardStore';
import { ListSchema } from '../ListSchema';

@Component({
  selector: 'app-board',
  templateUrl: './board.component.html',
  styleUrls: ['./board.component.css']
})
export class BoardComponent implements OnInit {
  cardStore: CardStore;
  lists: ListSchema[];
  constructor() { }
  setMockData(): void {
    this.cardStore = new CardStore();
    const lists: ListSchema[] = [
      {
        name: 'To Do',
        cards: []
      },
      {
        name: 'Doing',
        cards: []
      },
      {
        name: 'Done',
        cards: []
      }
    ]
    this.lists = lists;
  }

  ngOnInit() {
    this.setMockData();
  }

}

/src/app/board/board.component.html

< div>
  <app-list *ngFor="let list of lists" [list]="list" [cardStore]="cardStore"></app-list>
< /div>

src/app/board/board.component.css

div {
    background: #ffffff;
    display: flex;
    padding: 0 5px;
    height: 100vh;
    overflow-x: scroll;
}

We need to add three more files:
src/app/cardschema.ts

export class CardSchema {
  id: string;
  description: string;
}

src/app/cardstore.ts

import {CardSchema} from './cardschema';

export class CardStore {
  cards: Object = {};
  lastid = -1;
  _addCard(card: CardSchema) {
    card.id = String(++this.lastid);
    this.cards[card.id] = card;
    return (card.id);
  }

  getCard(cardId: string) {
    return this.cards[cardId];
  }

  newCard(description: string): string {
   const card = new CardSchema();
   card.description = description;
   return (this._addCard(card));
  }
}

src/app/listschema.ts

export class ListSchema {
  name: string;
  cards: string[];
}

`CardSchema` is the class from which every card instance will be created, we use `CardStore` to maintain a collection of cards and it will be used as datastore of sort (in subsequent articles we will connect the kanban board to a backend), finally `ListSchema` is used to create instances of lists of cards. `ListSchema` has only two attributes, the name of the list which is a string and the cards in the list which is an array (it could be better represented by a linked list, but an array will do for now).

Next, we create `ListComponent` and `CardComponent` with the following content:

src/app/card/card.component.css

p {
    background: white;
    margin: 0 0 6px 0;
    padding: 6px 6px 2px 8px;
}

src/app/card/card.component.html

< p class="card" draggable="true" (dragstart)="dragStart($event)" id="{{card.id}}">
 {{card.description}}
< /p>

src/app/card/card.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { CardSchema } from '../cardschema';

@Component({
  selector: 'app-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit {
  @Input() card: CardSchema;
  constructor() { }

  ngOnInit() {
  }

  dragStart(ev) {
    ev.dataTransfer.setData('text', ev.target.id);
  }

}

src/app/list/list.component.css

.list {
    background: #e2e4e6;
    width: 258px;
    padding: 6px;
    margin: 5px;
    display: inline-block;

}
.list__title {
    margin: 0;
    padding: 16px 0;
}
.list a {
    width: 100%;
    display: block;
    text-decoration: none;
}

input{
  width: 248px;
  padding: 5px;
  border: 2px solid orange;
  outline: 0;
  background: #fff;
  box-shadow:none;
}

src/app/list/list.component.html

< div class="list" (dragover)="allowDrop($event)" (drop)="drop($event)">
    < p class="list__title"><strong>{{list.name}}</strong></p>
    < div class="cards">
        <app-card *ngFor="let cardId of list.cards" [card]="cardStore.getCard(cardId)"></app-card>
    < /div>

    < input #addCardInput type="text" (keyup.enter)="onEnter(addCardInput.value); addCardInput.value=''; displayAddCard=false;" *ngIf="displayAddCard" autofocus>
    <a href="#" class="list__newcard" (click)="toggleDisplayAddCard();">Add a card...</a>
</ div>

src/app/list/list.component.ts

import { Component, HostListener, Input, OnInit } from '@angular/core';
import { CardSchema } from '../CardSchema';
import { ListSchema } from '../ListSchema';
import { CardStore } from '../CardStore';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {
  @Input() list: ListSchema;
  @Input() cardStore: CardStore;
  displayAddCard = false;

  constructor() { }
  toggleDisplayAddCard() {
    this.displayAddCard = ! this.displayAddCard;
  }
  ngOnInit(): void {
  }

  allowDrop($event) {
    $event.preventDefault();
  }

  drop($event) {
    $event.preventDefault();
    const data = $event.dataTransfer.getData('text');

    let target = $event.target;
    const targetClassName = target.className;

    while( target.className !== 'list') {
      target = target.parentNode;
    }
    target = target.querySelector('.cards');

    if(targetClassName === 'card') {
      $event.target.parentNode.insertBefore(document.getElementById(data), $event.target);
    } else if(targetClassName === 'list__title') {
      if (target.children.length) {
        target.insertBefore(document.getElementById(data), target.children[0]);
      }else {
        target.appendChild(document.getElementById(data));
      }
    } else {
      target.appendChild(document.getElementById(data));
    }

  }

  onEnter(value: string) {
    const cardId =  this.cardStore.newCard(value);
    this.list.cards.push(cardId);
  }
}

Code review

Now we have a working Kanban board.

kanban-board-cropped
So I’ll discuss some parts.

src/app/card/card.component.html

In `src/app/card/card.component.html` we set the `draggable` HTML attribute of the `p` element to` true` which makes it possible for us to drag and drop an HTML element. In HTML5, drag, and drop is part of the standard: Any element can be draggable.

Also, we call a method `dragStart` when the `dragstart` event is triggered on the `p` element.

src/app/card/card.component.ts

The DataTransfer object is used to hold the data that is being dragged during a drag and drop operation. It may hold one or more data items, each of one or more data types. This object is available from the dataTransfer property of all drag events. It cannot be created separately (i.e. there is no constructor for this object).

src/app/list/list.component.ts

On drop, we add a new node to the DOM.

Conclusion

In a follow-up article, we will connect our Kanban board to a Django backend for persistence.
Also, Read create a dashboard for an Ecommerce store with Angular 4.
Ready to use Angular IDE?