April 30, 2019

Build a Task Management App Using Vue.js and a Node.js Backend

by Lamin Sanneh

Build a Task Management App Using Vue.js and a Node.js Backend

Node.js is a Javascript backend framework and has been around for many years. In comparison to more established frameworks for Java, PHP, and Ruby, it is still young. Even with this, there is still some positive news. There are many JavaScript backend frameworks in the top starred projects on GitHub.

Javascript's flexibility allows it to work on both the frontend and backend. There are several components and patterns which seem to be common among many applications: drag-and-drop widgets, rearranging items to change their order, seeding data in the backend for development purposes, editable-inline-fields, a central event system for sending messages across different parts of an application, among others. There are several frontend libraries to help with the above. We will not be using any of them. We will build the above-mentioned features from scratch. This is so that we are aware of how the underlying mechanics of the base web technologies work.

In this article, we will build a simple Task Manager application. We will build it using Node.js in the backend and Vue.js in the frontend. We will have a list of boards, each containing several lists. Each list will contain several cards which will represent the tasks.

Being acquainted with the basic structure of the application, let's start building it. We will need Node.js installed to follow along.

The finished code for this tutorial is accessible at these GitHub repositories:

Initialise the Backend and Frontend Application Folders

In an empty folder, initialize the frontend using

vue init webpack client  

Accept with yes for all the prompts, except for “tests”. Enter the required information where necessary. Also, accept to install the dependencies using npm.

Navigate to the frontend folder using cd client. Install the required packages using;

npm install --save axios  

This is for making HTTP calls to the server.

Install the following packages which we will use for compiling Sass:

npm install sass-loader node-sass --save-dev  

Navigate to the base folder using cd ../ and create the server folder called server. Navigate to the folder using cd server and create a file named index.js. This will be the entry point for the server. Initialize a new package.json file using npm init. Answer yes to all the prompts or fill in the details if you deem it necessary.

Install the following server packages;

npm install --save body-parser express faker mongoose  

The packages we installed are responsible for:

  • body-parser: this helps us to parse and use frontend parameters in the backend.
  • express: A backend framework for Node.js.
  • faker: A library to help create dummy data for seeding database during development.
  • Mongoose: A database ORM library to do operations with the MongoDB database.

In the file index.js, paste in the following:

const mongoose = require('mongoose')  
const express = require('express')  
const app = express()  
const port = 3000  
const bodyParser = require('body-parser')  
const config = require('./config/index')  
const seederService = require('./services/seeder.service');

mongoose.connect(config.dbConnection, { useNewUrlParser: true})

app.use(bodyParser.json())

const corsConfig = function(req, res, next) {  
    res.header('Access-Control-Allow-Origin', 'http://localhost:8080')
    res.header('Access-Control-Allow-Credentials', true)
    res.header('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT')
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
    next()
}

app.use(corsConfig);

const apiRoutes = require('./routes/api');  
app.use('/api', apiRoutes);

if (config.seedData) {  
    seederService.seedData()
}

app.listen(port, () => console.log(`Example app listening on port ${port}!`))  

In the above code, we are initializing the Express.js server. We instruct the server to allow frontend connections from certain URLs — in this case: http://localhost:8080. Next, we import the seeder and config file ./services/seeder.service and ./config/index. These do not exist yet but we will get to that soon. We will only initialize the seeding mechanism if the config option seedData is set to true. This should only for use during development but that choice is up to the developer.

Create the config file in config/index.js and paste in the following:

module.exports = {  
    seedData: true,
    dbConnection: "mongodb://127.0.0.1:27017/task-management-system",
    numberOfBoards: 15,
    numberOfListsPerBoard: 8,
    numberOfCardsPerList: 4
}

We will need MongoDB running to carry on with this tutorial. For more information on that, please follow the instructions in the article How to Create a Public File Sharing Service with Vue.js and Node.js. This was published recently.

Create Backend Data Structure For Cards, Lists and Boards

Cards Data Structure

Now, let us create the models for the entities of our application. Still in the server folder; create the board model in the file models/board.model.js and paste in the following:

const mongoose = require("mongoose");  
const Schema = mongoose.Schema;  
const Types = Schema.Types;  
const List = require("./list.model")

const BoardSchema = new Schema({  
    title: Types.String,
    lists: [
        {type: Types.ObjectId, ref: "List", default: []}
    ]
})

module.exports = mongoose.model("Board", BoardSchema, "boards")  

Create the list model in the file models/list.model.js and paste in the following:

const mongoose = require("mongoose");  
const Schema = mongoose.Schema;  
const Types = Schema.Types;  
const Card = require("./card.model")

const ListSchema = new Schema({  
    title: Types.String,
    cards: [
        {type: Types.ObjectId, ref: "Card", default: []}
    ]
})

module.exports = mongoose.model("List", ListSchema, "lists")  

Create the card model in the file models/card.model.js and paste in the following:

const mongoose = require("mongoose");  
const Schema = mongoose.Schema;  
const Types = Schema.Types;

const CardSchema = new Schema({  
    title: Types.String,
    body: Types.String,
})

module.exports = mongoose.model("Card", CardSchema, "cards")  

Seed Some Data into the Boards, Cards and Lists Database (MongoDB)

Let us now create the seeder service. This will create some initial data in the database for us to use during development. Create a file in services/seeder.service.js and paste in the following:

const faker = require('faker')  
const Board = require('../models/board.model')  
const List = require('../models/list.model')  
const Card = require('../models/card.model')  
const config = require('../config/index')

module.exports = {  
    seedData () {
        Board.countDocuments((err, count) => {
            if (count > 0) {
                return;
            }

            this.createBoards()
        })
    },
    createBoards () {
        let boards = [];

        Array.from(Array(config.numberOfBoards)).forEach(() => {
            boards.push({
                title: faker.lorem.sentence(7)
            })
        })

        Board.insertMany(boards, (err, savedBoards) => {
            this.createListsForBoards(savedBoards)
        })
    },
    createListsForBoards (boards) {
        boards.forEach((board) => {
            this.createLists(board)
        })
    },
    createLists (board) {
        let lists = [];
        Array.from(Array(config.numberOfListsPerBoard)).forEach((val, index) => {
            lists.push({
                title: index + faker.lorem.sentence(3),
            })
        })

        List.insertMany(lists, (err, savedLists) => {
            savedLists.forEach((savedList) => {    
                board.lists.push(savedList.id)
            })
            board.save(() => {
                this.createCardsForLists(savedLists)
            })
        })
    },
    createCardsForLists (lists) {
        lists.forEach((list) => {
            this.createCards(list)
        })
    },
    createCards (list) {
        let cards = [];

        Array.from(Array(config.numberOfCardsPerList)).forEach(() => {
            cards.push({
                title: faker.lorem.sentence(5),
                body: faker.lorem.paragraph(1),
            })
        })

        Card.insertMany(cards, (err, savedCards) => {
            savedCards.forEach((savedCard) => {
                list.cards.push(savedCard.id)
            })
            list.save()
        })
    }
}

The method seedData initializes the seeder; we are creating several boards using createBoards. We will create several lists for each of these boards using createListsForBoards. Then, we are creating several cards for each of the lists using createCardsForLists.

Display the Boards, Cards and Lists

Backend

Now that we have the basic infrastructure in place, let's build the routes. These will be responsible for communicating with the frontend.

Create a route file in routes/api.js. Paste in the following:

const express = require("express")  
const router = express.Router()  
const boardService = require("../services/board.service")

router.get("/boards", boardService.getAll.bind(boardService))  
router.get("/boards/:boardId", boardService.getById.bind(boardService))

module.exports = router  

This declares the routes to list all the boards. We also have a route to get a single board along with all the nested lists and cards.

Next, create the board-service in services/board.service.js and paste in the following:

const Board = require('../models/board.model')

module.exports = {  
    getAll (req, res) {
        Board.find({}, 'title', (err, boards) => {
            this._handleResponse(err, boards, res)
        })
    },
    getById (req, res) {
        Board.findOne({_id: req.params.boardId})
            .populate({
                path: "lists",
                select: ["title"],
                model: "List",
                populate: {
                    path: "cards",
                    select: ["title", "body"],
                    model: "Card"
                }
            })
            .exec((err, board) => {
                this._handleResponse(err, board, res)
            })
    },
    _handleResponse (err, data, res) {
        if (err) {
            res.status(400).end()
        } else {
            res.send(data)
        }
    }
}

This service will handle the requests for the routes above.

Frontend

Now, let's create the frontend files for listing the boards, lists and cards. Navigate to the frontend folder client. In the file src/App.vue, remove the piece of code-line:

<img src="./assets/logo.png">  

In the style section, replace it with the following:

<style>  
html {  
  box-sizing: border-box;
  font-size: 62.5%;
}
body {  
  margin: 0;
}
html, body, #app {  
  height: 100%;
}
body {  
  background-color: #4fc08d;
  font-size: 1.6rem;
  font-family: Helvetica Neue,Arial,Helvetica,sans-serif;
  line-height: 20px;
}
*, *:before, *:after {
  box-sizing: inherit;
}
</style>  

This is doing some minimal resets and base styles.

In the file src/main.js, after the line Vue.config.productionTip = false, add the following:

axios.defaults.baseURL = 'http://localhost:3000'  

Import the axios library using:

import axios from 'axios'  

This configures the axios library to direct all HTTP calls to the URL http://localhost:3000.

To list the boards, open the route file src/router/index.js. Change the existing routes to the following:

import Vue from "vue"  
import Router from "vue-router"  
import Boards from "@/components/Boards"  
import BoardPage from "@/components/BoardPage"

Vue.use(Router)

export default new Router({  
  routes: [
    {
      path: "/",
      name: "Boards",
      component: Boards
    },
    {
      path: "/boards/:boardId",
      name: "BoardPage",
      component: BoardPage
    }
  ]
})

We have some pending components to create. Let’s do that now:

First, let’s create the boards component in src/components/Boards.vue. Paste in the following:

<template>  
  <div class="boards">
    <router-link v-for="board in boards" class="board" :key="board._id" :to="{ name: 'BoardPage', params: {boardId: board._id }}">
      {{ board.title }}
    </router-link>
  </div>
</template>

<script>  
import boardService from "../services/board.service"  
export default {  
  name: "Boards",
  data () {
    return {
      boards: []
    }
  },
  mounted () {
    boardService.getAll()
      .then(((boards) => {
        this.$set(this, "boards", boards)
      }).bind(this))
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->  
<style scoped>  
.boards {
  width: 80%;
  margin: 0 auto;
  padding-top: 100px;
  display: flex;
  flex-wrap: wrap;
}
.board {
  border-radius: 3px;
  color: #FFFFFF;
  display: block;
  text-decoration: none;
  width: 15%;
  min-width: 150px;
  min-height: 80px;
  padding: 10px;
  background-color: rgb(0, 121, 191);
  margin: 0 15px 15px 0;
}
</style>  

Protect your Vue App with Jscrambler

The code above uses the board service to fetch the boards from the backend and loops over them to display. We have some CSS styles as well.

Next, create the frontend board service in src/services/board.service.js. Paste in the following:

import axios from "axios"

export default {  
  getAll() {
    return axios.get("/api/boards").then(res => res.data)
  },
  findById(boardId) {
    return axios.get("/api/boards/" + boardId).then(res => res.data)
  }
}

Now, create the single-board component in src/components/BoardPage.vue. Put in the following:

<template>  
  <div class="board-page-main">
    <template v-if="board">
      <div class="board-title">
        <h2>{{ board.title }}</h2>
      </div>
      <div class="board-lists">
        <div class="board-lists-inner">
          <list 
            v-for="(list, i) in lists" 
            :key="list._id" 
            :index="i" 
            :list-prop="list"/>
        </div>
      </div>
    </template>
  </div>
</template>

<script>  
import boardService from "../services/board.service";  
import List from "./List";  
export default {  
  components: {

  },
  data() {
    return {
      board: null,
      lists: []
    };
  },
  created() {

  },
  mounted() {
    boardService.findById(this.$route.params.boardId).then(
      (board => {
        this.$set(this, "board", board);
        this.$set(this, "lists", board.lists);
      }).bind(this)
    );
  },
  methods: {

  },
};
</script>

<style>  
.board-title .is-editing {
  background-color: #ffffff;
  color: #000000;
  padding: 8px;
  display: inline-block;
  min-width: 600px;
}
.add-new-list .is-editing {
  background-color: #ffffff;
  color: #000000;
  padding: 8px;
  margin: 0;
}
</style>

<style scoped lang="scss">  
.add-new-list {
  display: inline-block;
  width: 270px;
}
.board-title {
  color: #ffffff;
  padding: 10px;
  height: 90px;
}
.board-page-main {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.board-lists {
  flex-grow: 1;
  margin-bottom: 20px;
  position: relative;
}
.board-lists-inner {
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow-x: scroll;
  position: absolute;
  white-space: nowrap;
}
</style>  

Then, create the single-list component in src/components/List.vue . Add in the following:

<template>  
  <div
    class="board-list"
  >
    <div class="list-inner">
      <div 
        v-if="list" 
        class="list-title">
        <h3>
          {{ list.title }}
        </h3>
      </div>
      <div class="list-cards">
        <card
          v-for="(card, i) in cards"
          :card-prop="card"
          :list-prop="list"
          :key="card._id"
          :index="i"
        />
      </div>
    </div>
  </div>
</template>

<script>  
import Card from "./Card"  
export default {  
  components: {
    Card
  },
  props: [
    "listProp",
    "index"
  ],
  data () {
    return {
      list: null,
      cards: []
    }
  },
  mounted () {
    this.$set(this, "list", this.listProp)
    this.$set(this, "cards", this.listProp.cards)
  },
  methods: {

  },
}
</script>

<style>  
.list-title .is-editing {
  background-color: #ffffff;
  color: #000000;
}
</style>

<style scoped lang="scss">  
.list-inner {
  background-color: #dfe3e6;
  padding: 10px;
  white-space: normal;
  border-radius: 3px;
}
.board-list {
  display: inline-block;
  margin-bottom: 10px;
  margin-right: 10px;
  vertical-align: top;
  width: 270px;
  max-height: 100%;
  overflow-y: scroll;
  &.is-dragging-list {
    transform: rotate(1deg)
  }
  &.drag-entered {
    border: 3px solid #237bda;
  }
}
</style>  

For the final component, create the card-component in src/components/Card.vue. Add in the following:

<template>  
  <div
    v-if="card"
    class="list-card"
  >
    <div class="card-title">{{ card.title }}</div>
    <div class="card-body">{{ card.body }}</div>
  </div>
</template>

<script>  
export default {  
  props: ["cardProp", "listProp", "index"],
  data() {
    return {
      card: null,
      list: null
    };
  },
  mounted() {
    this.$set(this, "card", this.cardProp);
    this.$set(this, "list", this.listProp);
  },
  methods: {

  },
};
</script>

<style scoped lang="scss">  
.card-title {
  text-decoration: underline;
}
.list-card {
  background-color: #ffffff;
  border-radius: 3px;
  padding: 10px;
  margin-bottom: 10px;

  &.is-dragging-card {
    transform: rotate(1deg);
  }
  &.drag-entered {
    border: 3px solid #237bda;
  }
}
</style>  

Edit Board Title

Backend

Let's now set up the backend for editing a board title. Navigate to the backend folder. In the file services/board.service.js, add the following method:

update (req, res) {  
    Board.findByIdAndUpdate(req.params.boardId, {title: req.body.title}, (err, board) => {
        this._handleResponse(err, board, res)
    })
}

Inside the router routes/api.js, add the following:

router.put("/boards/:boardId", boardService.update.bind(boardService))  

Frontend

Now, navigate to the frontend folder. In the component file src/components/BoardPage.vue, locate the code snippet below:

<h2>{{ board.title }}</h2>  

And replace it with this one:

<editable  
  v-slot:default="slotProps"
  :field-value="board.title"
  @editable-submit="editableSubmitted"
  >
  <h2>{{ slotProps.inputText }}</h2>
</editable>  

In the above code, we are invoking a component named Editable, but we are yet to create that. Before we do that, let's explain how it will behave. The component will be a general component responsible for editing text. We expect it to emit an event named editable-submit anytime the user modifies the value. We also pass in a property to act as the initial value to display in the component using :field-value="board.title". Since the component will have a slot section, we passed in the HTML to display in there. Then, we will get access to the component's data scope using v-slot:default="slotProps".

Add this method to src/components/BoardPage.vue to handle the emitted event above:

editableSubmitted(inputText) {  
  if (inputText === this.board.title) {
    return;
  }
  boardService.update(this.board._id, inputText).then(() => {
    this.board.title = inputText;
  })
},

Import the Editable component using the line:

import Editable from "./Editable";  

And add it to the components list as shown below:

components: {  
  List,
  Editable
},

Now, let's create the Editable component. Create a file in src/components/Editable.vue. Paste in the following:

<template>  
  <div>
    <h2
      v-show="isEditing"
      ref="editableField"
      :class="{'is-editing': isEditing}"
      contenteditable="true"
      @keydown.enter="submit"
      @blur="onBlur"
      @keydown.esc="escape"
    >{{ inputText }}</h2>
    <template v-if="isEditing === false">
      <div @click="onBoardTitleClick()">
        <slot 
          :isEditing="isEditing" 
          :inputText="inputText"/>
      </div>
    </template>
  </div>
</template>

<script>  
export default {  
  props: ["fieldValue"],
  data() {
    return {
      inputText: "",
      isEditing: false
    };
  },
  mounted() {
    this.$set(this, "inputText", this.fieldValue);
  },
  methods: {
    onBoardTitleClick() {
      this.$set(this, "isEditing", true);
      setTimeout((() => {
        this.$refs.editableField.focus()
      }).bind(this), 200)
    },
    submit(event) {
      this.$set(this, "inputText", event.currentTarget.innerText)
      this.$emit("editable-submit", event.currentTarget.innerText)
      this.$set(this, "isEditing", false);
    },
    escape(event) {
      this.$set(this, "inputText", event.currentTarget.innerText)
      this.$emit("editable-submit", event.currentTarget.innerText);
      this.$set(this, "isEditing", false);
    },
    onBlur (event) {
      this.$set(this, "inputText", event.currentTarget.innerText)
      this.$emit("editable-submit", event.currentTarget.innerText);
      this.$set(this, "isEditing", false);
    }
  }
};
</script>  

In this component, we have a slot section which shows when we are not in edit mode. When in edit mode, the content-editable div is displayed. We are listening to several events. When the title gets clicked, we set the component in edit mode. This will display the content editable div. When in this mode, we can change the title. When we press the escape key, an event called editable-submit is emitted with the updated value to the parent. Likewise, we do the same for when the user presses the enter key. When blurred, we disable edit-mode but keep the changed value if any.

The parent component src/components/BoardPage.vue listens for the editable-submit event. We have not defined the handler yet. Let's do that now. Inside the parent, add the method below:

editableSubmitted(inputText) {  
  if (inputText === this.board.title) {
    return;
  }
  boardService.update(this.board._id, inputText).then(() => {
    this.board.title = inputText;
  })
},

This calls the board-service to update the board on the backend. If successful, we set the board title with the value emitted from the Editable component.

In the board-service src/services/board.service.js, add the update method below:

update(boardId, title) {  
  return axios.put(
      "/api/boards/" + boardId,
      {
        title: title
      }
    ).then(res => res.data)
}

Protect your Vue App with Jscrambler

Add a New List

Backend

Now, let's set up the backend infrastructure to be able to add a new list. Navigate to the backend folder. Create a file in services/list.service.js and add the following:

const List = require('../models/list.model')  
const Board = require('../models/board.model')

module.exports = {  
    create (req, res) {
        Board.findById(req.body.boardId, (err, board) => {
            if (err) {
                return this._handleResponse(err, null, res)
            }

            if (!board) {
                return this._handleResponse("Error", null, res)
            }

            List.create({title: req.body.title}, (err, card) => {
                board.lists.push(card._id)
                board.save(() => {
                    this._handleResponse(err, card, res)
                })
            })
        })
    },
    _handleResponse (err, data, res) {
        if (err) {
            res.status(400).end()
        } else {
            res.send(data)
        }
    }
}

Next, in the route file routes/api.js, add the following:

router.post("/lists", listService.create.bind(listService))  

And also import the list-service like below:

const listService = require("../services/list.service")  

Frontend

Navigate to the frontend folder. In the file src/components/BoardPage.vue, at the end of the tag:

<div class="board-lists-inner">  

Add in the following:

<addable  
  class="add-new-list"
  @addable-submit="addableSubmit">
  <div>Add list</div>
</addable>  

We introduced another general component called Addable. This is will be responsible for adding new items. It will have a slot inside it as well. We expect it to emit an event called addable-submit.

Let's add a method to handle this event. Inside the file src/components/BoardPage.vue, add the method below:

addableSubmit(listTitle) {  
  if (!listTitle || listTitle.length === 0) {
    return;
  }
  listService.create(this.board._id, listTitle).then((newList) => {
    this.board.lists.push(newList)
  })
},

Import the Addable component and the list-service with the following:

import listService from "../services/list.service";  
import Addable from "./Addable";  

Next, add the Addable component to the list of imported components like below:

components: {  
  List,
  Editable,
  Addable,
}

Create a list-service inside src/services/list.service.js. Put in the following:

import axios from "axios"

export default {  
  create (boardId, listTitle) {
    return axios.post("/api/lists", {
      boardId: boardId,
      title: listTitle
    }).then(res => res.data)
  }
}

Finally, let's create the component itself. Create a file in src/components/Addable.vue. Paste the following in:

<template>  
  <div>
    <h2
      v-show="isAdding"
      ref="addableField"
      :class="{'is-editing': isAdding}"
      contenteditable="true"
      @keydown.enter="submit"
      @blur="onBlur"
      @keydown.esc="escape"
    >{{ inputText }}</h2>
    <h2>{{ inputText }}</h2>
    <template v-if="isAdding === false">
      <div @click="onTitleClick()">
        <slot/>
      </div>
    </template>
  </div>
</template>

<script>  
export default {  
  data () {
    return {
      isAdding: false,
      inputText: ""
    }
  },
  methods: {
    onTitleClick () {
      this.$set(this, "isAdding", true)
      setTimeout((() => {
        this.$refs.addableField.focus()
      }).bind(this), 200)
    },
    onBlur () {
      this.$set(this, "isAdding", false)
    },
    escape () {
      this.emptyInput()
    },
    submit (event) {
      this.$emit("addable-submit", event.currentTarget.innerText)
      this.emptyInput()
    },
    emptyInput () {
      this.$set(this, "inputText", "")
      this.$refs.addableField.innerText = ""
      this.$set(this, "isAdding", false);
    }
  }
}
</script>  

In here, when the text value is clicked, we set the component to add mode. We are showing the slot template when not in add mode. When in add mode, we show the editable div. When the user presses enter, we empty the input and emit an event named addable-submit. When escape is pressed, we empty the input and disable add mode but no event is emitted. When the editable div is blurred, we disable add mode.

Rearrange List

Backend

For our last feature, let's set up the backend feature to rearrange lists. Navigate to the backend folder. Inside routes/api.js, add the following route:

router.put(  
  "/boards/updateListsOrder",
  boardService.updateListsOrder.bind(boardService)
)

Rearrange the routes so that the following two are in this order. If not you might have conflicting issues:

router.put(  
    "/boards/updateListsOrder",
    boardService.updateListsOrder.bind(boardService)
  )
router.put("/boards/:boardId", boardService.update.bind(boardService))  

Inside services/board.service.js, add the method below:

updateListsOrder (req, res) {  
    Board.findById(req.body.boardId, (err, board) => {
        if (err) {
            res.status(400).end()
            return
        }

        board.lists = req.body.listIds
        board.save((err, savedBoard) => {
            this._handleResponse(err, savedBoard, res)
        })
    })
},

Frontend

Back to the frontend, inside src/components/BoardPage.vue, add the following data property:

fromListIndex: null,  

And the following methods:

onListDragStarted(fromListIndex) {  
  this.$set(this, "fromListIndex", fromListIndex)
},
onListDragEnd(event) {  
  this.$set(this, "fromListIndex", null);
},
onListDropped(toListIndex) {  
  if (this.fromListIndex === toListIndex) {
    return;
  }
  this.switchListPositions(this.fromListIndex, toListIndex);
  this.updateListsOrder();
},
updateListsOrder() {  
  let listIds = this.lists.map(list => list._id);
  boardService.updateListsOrder(this.board._id, listIds);
},
switchListPositions(fromListIndex, toListIndex) {  
  if (this.fromListIndex === null) {
    return;
  }

  this.lists.splice(toListIndex, 0, this.lists.splice(fromListIndex, 1)[0]);
},

The methods starting with the on keyword will respond to events which happen when we drag a list item. To wire those methods, add the following lines to the created function:

this.$eventBus.$on("list-drag-started", this.onListDragStarted);  
this.$eventBus.$on("list-dragend", this.onListDragEnd);  
this.$eventBus.$on("list-dropped", this.onListDropped);  

Update the board service src/services/board.service.js by adding in this method:

updateListsOrder(boardId, listIds) {  
  return axios
    .put("/api/boards/updateListsOrder", {
      boardId: boardId,
      listIds: listIds
    })
    .then(res => res.data)
}

You may notice that we are using an instance property named $eventBus. It is responsible for sending messages across our application. It allows for any component at any level to subscribe to any broadcasted messages. Currently, it does not exist, let's wire it up.

Create a file in src/event-bus/index.js. Paste in the following:

import Vue from "vue"

export default new Vue()  

Let's make it available to all Vue instances. Inside src/main.js, import it using:

import eventBus from './event-bus/index'  

Then wire it up using the statement:

Vue.prototype.$eventBus = eventBus  

It should now be available in every component.

Let's now modify the component src/components/List.vue. In there, in the top level template div node, after:

class="board-list"  

Add in the following:

:class="{'is-dragging-list': isDraggingList, 'drag-entered': dragEntered}"
draggable="true"  
@dragstart="onListDragStart(index, $event)"
@dragend="onListDragEnd"
@drop="onListDrop(index)"
@dragover.prevent
@dragover="onListDragOver"
@dragleave="onListDragLeave"

Then add the following handlers for the events above:

onListDragStart (fromIndex, event) {  
  if (!fromIndex) {
    fromIndex = 0
  }

  this.$set(this, "isDraggingList", true)
  this.$eventBus.$emit("list-drag-started", fromIndex)
},
onListDragEnd () {  
  this.$set(this, "isDraggingList", false)
  this.$eventBus.$emit("list-dragend")
},
onListDragOver (event) {  
  this.$set(this, "dragEntered", true)
},
onListDragLeave (index, list) {  
  this.$set(this, "dragEntered", false)
},
onListDrop (toIndex) {  
  this.$set(this, "dragEntered", false)
  this.$eventBus.$emit("list-dropped", toIndex)
},

Add in the following data properties:

isDraggingList: false,  
dragEntered: false  

And, inside src/components/Board.vue, add the following data property:

fromListIndex: null,  

Let's figure out what we are doing in the methods above. When we drag a list, it gets a class of is-dragging-list. This tilts the original list some degrees to show that it is the source. When a list is being dragged over, it gets a class of drag-entered. This adds a blue border around the list to show that it is currently being dragged over. When we release a list over another one, the original list takes the position of the one being dragged over. The target list gets shifted to the left.

Conclusion

That brings us to the end of this Task Manager Application. This is only a basic skeleton application. It has many of the basic elements for one to expand. We may have not implemented all the features. There is enough foundation in the code to create a more robust version of the application. Some features you could try to add are: delete feature for cards, lists and boards; draggable feature for cards within the same list or from one list to another; and finally to be able to edit the title for lists and cards as well as the card body.

We hope this inspires you to push the boundaries a little bit further and, as usual, if you have any questions, feel free to tweet the author directly @LaminEvra.

If you're building Vue applications with sensitive logic, be sure to protect them against code theft and reverse-engineering by following our guide.