Building a Trello Board clone with Angular 10

Building a Trello Board clone with Angular 10


In this article, We will build a Trello Board clone with Angular 10. Trello is a web-based Kanban-style list-making application 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. 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.

Here’s the demo of our app:

But first, let’s get our system set up for Angular development.

Create a new Angular project using the Angular CLI

Now we need to move into the client directory that we created earlier and create a new Angular project.Incase you don’t have the angular CLI globally installed in your development machine, open up your terminal and type:

npm install -g @angular/cli

But if you already have the Angular CLI installed you can skip that step and just move into the project directory to set up a new angular project. Open your terminal on your desktop and type this commands:

cd desktop
ng new Trelloclone

The ng new Trelloclone command will create a new angular project and prompts you for information about features to include in the initial app.

Now to run the application run this on the terminal:

cd Trelloclone
ng serve --open

The ng serve command launches the server, watches your files, and rebuilds the app as you make changes to those files and the --open flag will open your browser to http://localhost:4200/ where your application is being hosted.

Taking a look at the Kanban board image below we can see two visual components namely lists and cards, a third component not visible is the board component.

Create the required components using the following command:

ng generate component Board
ng generate component List
ng generate component Card

Next, we edit the files 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: #0079bf;
    display: flex;
    padding: 0 5px;
    height: 100vh;
    overflow-x: scroll;
  }

We will now create Three files required for our app:

src/app/cardschema.ts

It is the class from which every card instance will be created.

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

src/app/cardstore.ts

we use CardStore to maintain a collection of cards and it will be used as datastore of sort.

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[];
}

ListSchema is used to create instances of a list of cards.

Now, Let’s modify the card and list components we created:

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);
  }
} 

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/card/card.component.html

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

Here, 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.

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

src/app/card/card.component.css

p {
	background: darkgoldenrod;
	margin: 0 0 6px 0;
	padding: 6px 6px 2px 8px;
	border: 1px solid green;
font-style: italic;
color: #fff;
}

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);
  }
}

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

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.css

.list {
	background:lightyellow;
	width: 350px;
	padding: 6px;
	margin: 5px;
	display: inline-block;
	
}
.list__title {
	margin: 0;
	padding: 16px 0;
}
.list a {
	width: 100%;
	display: block;
	text-decoration: none;
	margin-top: 10px;
}

input{
  width: 300px;
  padding: 10px;
  border: 1px solid violet;
  outline: 0;
  background: #fff;
  box-shadow:none;
}

Conclusion

I hope you learned a few things about Angular. Every article can be made better, so please leave your suggestions and contributions in the comments below. If you have questions about any of the steps, please do ask also in the comments section below.

You can check out the Trelloclone repo.


Share on social media

//