Categories
Vue

Build a Drag and Drop App with Vue.js

Spread the love

Drag and drop is a feature of many interactive web apps. It provides an intuitive way for users to manipulate their data. Adding drag and drop feature is easy to add to Vue.js apps.

The App We Are Building

We will create a todo app that has 2 columns — a To Do column and a Done column. You can drag and drop between the two to change the status from to do to done and vice versa. To build the app, we use Vue Material library with the Vue Draggable package to make the app look good and provide the drag and drop ability easily. It will also have a navigation menu and a top bar.

Getting Started

To begin building the app, we start by installing Vue CLI. We install it by running npm i -g @vue/cli. After that, we can create the project. To do this, we run vue create todo-app. Instead of choosing the default, we customize the scaffolding of the app by choosing the alternative option and pick Vue Router, Babel, and CSS preprocessor. The scaffolding should be finished after following the instructions and then we are ready to add some libraries.

We need to add Axios for making HTTP requests, and the Vue Material library with the Vue Draggable packages we mentioned before to make our app prettier and to provide the drag and drop capabilities we desire respectively. In addition, we need the Vee Validate package to let us do form validation in our app. To install these packages, we run npm i axios vuedraggable vue-material vee-validate@2.2.14 .

Building the App

Now we can write some code. To start, we add a mixin to let us make our requests. To do this, we create a mixins folder and add a file called todoMixin.js, and we then put the following into our file:

const axios = require('axios');
const apiUrl = 'http://localhost:3000';

export const todoMixin = {
 methods: {
 getTodos() {
 return axios.get(`${apiUrl}/todos`);
 },

 addTodo(data) {
 return axios.post(`${apiUrl}/todos`, data);
 },

 editTodo(data) {
 return axios.put(`${apiUrl}/todos/${data.id}`, data);
 },

 deleteTodo(id) {
 return axios.delete(`${apiUrl}/todos/${id}`);
 }
 }
}

These functions will be used in our home page to make HTTP requests to do CRUD on our todo list. We will create the home page now. In Home.vue, we replace what we have with the following:

<template>
  <div class="home">
    <div class="center">
      <h1>To Do List</h1>
      <md-button class="md-raised" @click="showDialog = true"
        >Add Todo</md-button
      >
    </div>

    <div class="content">
      <md-dialog :md-active.sync="showDialog">
        <md-dialog-title>Add Todo</md-dialog-title>
        <form @submit="addNewTodo" novalidate>
          <md-field :class="{ 'md-invalid': errors.has('description') }">
            <label for="description">Description</label>
            <md-input
              type="text"
              name="description"
              v-model="taskData.description"
              v-validate="'required'"
            ></md-input>
            <span class="md-error" v-if="errors.has('description')">{{
              errors.first("description")
            }}</span>
          </md-field>

          <md-dialog-actions>
            <md-button class="md-primary" @click="showDialog = false"
              >Close</md-button
            >
            <md-button class="md-primary" @click="showDialog = false"
              >Save</md-button
            >
          </md-dialog-actions>
        </form>
      </md-dialog>
      <div class="lists">
        <div class="left">
          <h2>To Do</h2>
          <draggable v-model="todo" group="tasks" @change="updateTodo">
            <div v-for="t in todo" :key="t.id" class="item">
              {{ t.description }}
              <a @click="deleteTask(t.id)">
                <md-icon>close</md-icon>
              </a>
            </div>
          </draggable>
        </div>
        <div class="right">
          <h2>Done</h2>
          <draggable v-model="done" group="tasks" @change="updateTodo">
            <div v-for="d in done" :key="d.id" class="item">
              {{ d.description }}
              <a @click="deleteTask(d.id)">
                <md-icon>close</md-icon>
              </a>
            </div>
          </draggable>
        </div>
      </div>
    </div>
  </div>
</template>


<script>
// @ is an alias to /src
import draggable from "vuedraggable";
import { todoMixin } from "@/mixins/todoMixin";

export default {
  name: "home",
  components: {
    draggable,
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some((key) => this.fields[key].dirty);
    },
  },
  mixins: [todoMixin],
  data() {
    return {
      todo: [],
      done: [],
      showDialog: false,
      taskData: {},
    };
  },
  beforeMount() {
    this.getNewTodos();
  },
  methods: {
    async addNewTodo(evt) {
      evt.preventDefault();
      if (!this.isFormDirty || this.errors.items.length > 0) {
        return;
      }
      await this.addTodo(this.taskData);
      this.showDialog = false;
      this.getNewTodos();
    },

    async getNewTodos() {
      const response = await this.getTodos();
      this.todo = response.data.filter((t) => !t.done);
      this.done = response.data.filter((t) => t.done);
    },

    async updateTodo(evt) {
      let todo = evt.removed && evt.removed.element;
      if (todo) {
        todo.done = !todo.done;
        await this.editTodo(todo);
      }
    },

    async deleteTask(id) {
      const todo = await this.deleteTodo(id);
      this.getNewTodos();
    },
  },
};
</script>

<style lang="scss" scoped>
.center {
  text-align: center;
}

.md-dialog {
  width: 70vw;
}

form {
  width: 92%;
}

.md-dialog-title.md-title {
  color: black !important;
}

.lists {
  padding-left: 5vw;
  display: flex;
  align-items: flex-start;
  .left,
  .right {
    width: 45vw;
    padding: 20px;
    min-height: 200px;
    .item {
      padding: 10px;
      border: 1px solid black;
      background-color: white;
      display: flex;
      justify-content: space-between;
      a {
        cursor: pointer;
      }
    }
  }
}
</style>

We added a dialog box with a form to enter the description for our task. The field is required but the user can enter anything. The addTodo function takes the data entered and submit it if it’s valid. The this.fields function is provided by the Vee Validate package and has all the fields in the object so we can check if the fields have been changed or not. Anything in the computed property is computer whenever anything returned by the function changes.

Next, we added 2 lists that we drag between to let us change the status of the tasks to done or not done. The lists are the draggable components on the template. We defined the models in the draggable components in the object returned by the data function. It is important that we have the same group prop so that we can drag between them the 2 draggable components. Whenever drag and drop happens, the change event is raised and the updateTodo function is called. The function will toggle the done flag of the task and make a request to save the task.

Each task also has a button to delete it. When the close button is clicked, the deleteTodo function is called. The id of the task is passed into the function, so we can make the request to delete the task.

Next in App.vue, we add the menu and a left-side navigation bar with the following code:

<template>
  <div id="app">
    <md-toolbar>
      <md-button class="md-icon-button" @click="showNavigation = true">
        <md-icon>menu</md-icon>
      </md-button>
      <h3 class="md-title">Todo App</h3>
    </md-toolbar>
    <md-drawer :md-active.sync="showNavigation" md-swipeable>
      <md-toolbar class="md-transparent" md-elevation="0">
        <span class="md-title">Todo App</span>
      </md-toolbar>

      <md-list>
        <md-list-item>
          <router-link to="/">
            <span class="md-list-item-text">Home</span>
          </router-link>
        </md-list-item>
      </md-list>
    </md-drawer>

    <router-view />
  </div>
</template>

<script>
export default {
  name: "app",
  data: () => {
    return {
      showNavigation: false,
    };
  },
};
</script>

<style>
.center {
  text-align: center;
}

form {
  width: 95vw;
  margin: 0 auto;
}

.md-toolbar.md-theme-default {
  background: #009688 !important;
  height: 60px;
}

.md-title,
.md-toolbar.md-theme-default .md-icon {
  color: #fff !important;
}
</style>

The <router-view /> displays the routes that we will define in router.js, which only consists of the home page.

In main.js,we put:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import VueMaterial from 'vue-material';
import VeeValidate from 'vee-validate';
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'

Vue.config.productionTip = false;

Vue.use(VueMaterial);
Vue.use(VeeValidate);

new Vue({
 router,
 store,
 render: h => h(App)
}).$mount('#app')

to include the Vue.js add-on libraries that we use in this app.

And in router.js, we add:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

This adds the home page to our list of routes so that it will be displayed to the user when typing in the URL or clicking a link to the page.

Our JSON API will be added without writing any code by using the JSON Server Node.js package, located at https://github.com/typicode/json-server. Data will be saved to a JSON file, so we don’t have to make our own back end add to save some simple data. We install the server by running npm i -g json-server. Then once that is done, go into our project directory then run json-server --watch db.json. In db.json, we put:

{
 "todos": []
}

so that we can use those endpoints for saving data to db.json, which have the same URLs as in todoMixin.

After all the work is done we have the following:

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *