Make a Trello Clone in 15 Minutes

In the past weeks, I’ve written about how to make a drag & drop list using Angular CDK and how to enable dragging & dropping between multiple lists. Trello is the app that first comes to mind when I think of how this can be used to create a great experience.

At its core, a Trello board has just three pieces of functionality: create lists, create cards within the lists, and reorganize cards within and between the lists. The articles above give us all the tools we need to do this quickly ourselves, so let’s do it!

We’ll use the Angular CLI to do the following:

  1. Create a new app
  2. Add a list component
  3. Add a board component
  4. Make it pretty

Create New App

This first step’s pretty easy. We’re just going to use ng to create a new app and install the Angular CDK.

$ ng new lists-app --defaults
$ cd lists-app
$ npm install @angular/cdk

Do a quick check to make sure we’re starting in a good state:

$ ng serve --open

Add List Component

Now we’ll make our list component using ng generate component.

$ ng g c list

Hop into the code and make three changes. First, we need to import the DragDrop and Forms modules in src/app/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { FormsModule } from '@angular/forms';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ListComponent } from './list/list.component';

@NgModule({
  declarations: [
    AppComponent,
    ListComponent
  ],
  imports: [
    BrowserModule,
    DragDropModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Next, update src/app/list/list.component.ts to support drag/drop and dynamic creation of items. We’ll add the items array to store our list items, a drop function to handle our drop/drop event, and an onSubmit function to add new items to our list.

import { Component, OnInit } from '@angular/core';
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {

  items: string[] = [];

  constructor() { }

  ngOnInit(): void {
  }

  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex);
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex);
    }
  }

  onSubmit(newItemForm: NgForm) {
    this.items.push(newItemForm.value.newItem);
    newItemForm.reset();
  }
}

The third step is to change our markup in src/app/list/list.component.html. This is just two parts, displaying the drag-&-droppable list items and accepting input for new items.

<div cdkDropList [cdkDropListData]="items" (cdkDropListDropped)="drop($event)">
    <div *ngFor="let item of items" cdkDrag>{{item}}</div>
</div>
<form #newItemForm="ngForm" (ngSubmit)="onSubmit(newItemForm)">
    <input name="newItem" ngModel type="text" placeholder="Enter a new item"><button type="submit">Add Item</button>
</form>

That’s it for our list component, but let’s make one more change to app/app.component.html so we can test. Replace its entire contents with the following:

<app-list></app-list>

Now let’s do another check-in. We should be able to add items to our list and move them around via drag & drop.

$ ng serve --open

Add Board Component

Once again, we look to ng generate component to create our board component.

$ ng g c board

Just like the list component allows for dynamic creation of list items, we want our board component to allow dynamic creation of lists. So, let’s modify src/app/board/board.component.ts to support this:

import { Component, OnInit } from '@angular/core';
import { ListComponent } from '../list/list.component';

@Component({
  selector: 'app-board',
  templateUrl: './board.component.html',
  styleUrls: ['./board.component.css']
})
export class BoardComponent implements OnInit {

  lists: ListComponent[] = [];

  constructor() { }

  ngOnInit(): void {
  }

  addList() {
    var newList = new ListComponent();
    this.lists.push(newList);
  }
}

And make the markup changes in src/app/board/board.component.html. One thing to note is the use of cdkDropListGroup. This makes all the lists connected and allows dragging & dropping between them.

<button (click)="addList()">Add List</button>
<div class="list-container" cdkDropListGroup>
    <app-list *ngFor="let list of lists"></app-list>
</div>

We’ll also modify src/app/board/board.component.css so that lists will be added horizontally.

.list-container {
    display: flex;
    flex-direction: row;
}

Finally, we’ll update app.component.html to use our board component instead of a single list:

<app-board></app-board>

Our board component is complete, so let’s do another check-in.

$ ng serve --open

Make It Pretty

At this point, the hard part’s done. We have our core functionality implemented. We can add lists, add items to the lists, and move the items around. Now let’s make it look nice!

Begin by installing Angular Material:

$ ng add @angular/material

Then import the MatCard, MatButton, and MatInput modules in src/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ListComponent } from './list/list.component';
import { BoardComponent } from './board/board.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [
    AppComponent,
    ListComponent,
    BoardComponent
  ],
  imports: [
    BrowserModule,
    DragDropModule,
    FormsModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatCardModule,
    MatInputModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now we can use mat-card, mat-raised-button, and mat-form-field in src/app/list/list.component.html:

<div class="container">
    <div class="list" cdkDropList [cdkDropListData]="items" (cdkDropListDropped)="drop($event)">
        <mat-card class="list-item" *ngFor="let item of items" cdkDrag>
            <mat-card-content>
                <p>{{item}}</p>
            </mat-card-content>
        </mat-card>
    </div>
    <form #newItemForm="ngForm" (ngSubmit)="onSubmit(newItemForm)">
        <mat-form-field>
            <input matInput name="newItem" ngModel type="text" placeholder="Enter a new item">
        </mat-form-field>
        <button mat-raised-button type="submit" color="accent">Add Item</button>
    </form>
</div>

And we’ll add a little CSS to src/app/list/list.component.css:

.container {
    margin-right: 10px;
}

.container button {
    margin-left: 5px;
}

.list {
    padding: 10px;
    max-width: 100%;
    border: solid 1px #ccc;
    min-height: 60px;
    display: block;
    background: #fafafa;
    border-radius: 4px;
    overflow: hidden;
}

.cdk-drag-preview {
    box-sizing: border-box;
    border-radius: 4px;
    box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.cdk-drag-placeholder {
    opacity: 0;
}

.cdk-drag-animating {
    transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

.list-item {
    margin: 5px;
}

.list-item:last-child {
    border: none;
}

Then we’ll do some similar things to src/app/board/board.component.html and board.component.css:

<button (click)="addList()" mat-raised-button color="primary">Add List</button>
<div class="list-container" cdkDropListGroup>
    <app-list *ngFor="let list of lists"></app-list>
</div>
button {
    margin: 10px;
}

.list-container {
    display: flex;
    flex-direction: row;
    margin: 10px;
}

Now we’ve got fancy buttons, some input animations, and shadows while dragging, and it looks quite nice!

That’s where we’ll leave it today. The code I wrote while constructing this post can be found here.

Author: Adam Prescott

I'm enthusiastic and passionate about creating intuitive, great-looking software. I strive to find the simplest solutions to complex problems, and I embrace agile principles and test-driven development.

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s