lesson1
- Starting Code
- Damian’s Twitter
-
Vue Newsletter
lesson2 - Starting Code
-
Finished Code
lesson3 - Starting Code
-
Finished Code
lesson4 - Starting Code
-
Finished Code
lesson5 - Starting Code
-
Finished Code
lesson6 - Starting Code
-
Starting Code
lesson7 -
Starting Code
lesson8 - Starting Code
-
Finished Code
lesson9
Finished Code
lesson10
Finished Code
Tour of the App
In this first Watch Us Build lesson Gregg Pollack works alongside Damian Dulisz, a Vue Core team member, to build a Trello like clone. Damian is also known for his work on the Vue-multiselect library and Vuelidate libraries.
In this course, we will be building an application together step by step including:
- Setting up Vuex to read & write.
- Saving state to local storage
- Using the browser drag and drop API
- Refactor big components into smaller ones
- Creating reusable components
- Additional refactoring
We welcome you to code along with us, which you can do by cloning this repo and checking out the ‘application-start’ tag with git checkout application-start
, or just downloading this startup code. We will provide you with all the code we are writing below each video, and if you get stuck the finishing code for each level is tagged appropriately.
I think you’ll find Damian’s approach to refactoring components extremely useful and implementing the browser’s drag and drop API is fun to watch. In the next lesson we will get familiar with the code base, and start hooking up our data out of Vuex.
Building our Board
In this lesson we start by taking a tour of our starting code. Please feel free to download and follow along if you like. We’ll be building out our initial Trello Board columns and tasks by pulling it out of Vuex. We’ll also learn how to save and load our Board’s state from our browser’s localstorage.
Feel free to copy paste the code below if you get stuck, look at the differences in GitHub, or simply clone the repo and check out the lesson-2-complete tag.
/src/views/Board.vue
<template>
<div class="board">
<div class="flex flex-row items-start">
<div
class="column"
v-for="(column, $columnIndex) of board.columns"
:key="$columnIndex"
>
<div class="flex items-center mb-2 font-bold">
{{ column.name }}
</div>
<div class="list-reset">
<div
class="task"
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
>
<span class="w-full flex-no-shrink font-bold">
{{ task.name }}
</span>
<p
v-if="task.description"
class="w-full flex-no-shrink mt-1 text-sm"
>
{{ task.description }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: mapState(['board'])
}
</script>
We’ll also create a small saveStatePlugin
so we can listen for mutations being made to our Vuex store, and store them in the local browser storage. This way if we refresh our browser, our board won’t get reset.
/src/utils.js
...
export function saveStatePlugin (store) {
store.subscribe(
(mutation, state) => {
localStorage.setItem(
'board',
JSON.stringify(state.board)
)
}
)
}
We’ll then need to use this plugin and restore the board from local storage if it already exists.
/src/store.js
import Vue from 'vue'
import Vuex from 'vuex'
import defaultBoard from './default-board'
import { saveStatePlugin } from './utils' // <-- Import saveStatePlugin
Vue.use(Vuex)
const board = JSON.parse(localStorage.getItem('board')) || defaultBoard
export default new Vuex.Store({
plugins: [saveStatePlugin], // <-- Use
state: {
board
},
In the next lesson we’ll open up our tasks in a modal. See you then!
Opening Tasks in a Modal
In this lesson we implement a modal showing task information using a child route. This opens when we click on a task in our board and closes when we click on the background. I hadn’t learned how to create a modal which is also a child route, and this is a great example.
We start by adding the child route.
/src/router.js
...
import Task from './views/Task.vue' // <-- Add Task
Vue.use(Router)
export default new Router({
...
routes: [
{
path: '/',
name: 'board',
component: Board,
children: [ // <-- Add child component
{
path: 'task/:id',
name: 'task',
component: Task
}
]
}
]
})
We also add a getter to our Vuex store so we can search for the task based on the ID from the url.
/src/store.js
...
state: {
board
},
getters: { // <-- Add a getter
getTask (state) {
return (id) => {
for (const column of state.board.columns) {
for (const task of column.tasks) {
if (task.id === id) {
return task
}
}
}
}
}
},
mutations: {}
})
Our board contains the code to open and close the task modal.
/src/views/Board.vue
...
<div
class="task"
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
@click="goToTask(task)"
>
...
<div
class="task-bg"
v-if="isTaskOpen"
@click.self="close"
>
<router-view/>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['board']),
isTaskOpen () {
return this.$route.name === 'task'
}
},
methods: {
goToTask (task) {
this.$router.push({ name: 'task', params: { id: task.id } })
},
close () {
this.$router.push({ name: 'board' })
}
}
}
</script>
...
And finally our Task.vue
component fetches the appropriate task and displays it’s name in a modal.
**/src/views/Task.vue**
<template>
<div class="task-view">
<div class="flex flex-col flex-grow items-start justify-between px-4">
{{ task.name }}
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['getTask']),
task () {
return this.getTask(this.$route.params.id)
}
}
}
</script>
...
In the next lesson we’ll add the ability to add and edit tasks using forms, stay tuned!
Adding and Editing Tasks
Now that we can display tasks in a modal, let’s start working on actually being able to change the underlaying task data. We’ll start by adding a textarea to actually print out the task description:
/src/views/Task.vue
<template>
<div class="task-view">
...
<textarea
placeholder="Enter task description"
class="relative bg-transparent px-2 border mt-2 h-64 border-none leading-normal"
:value="task.description"
/>
</div>
</div>
</template>
This will show our description on the modal we created for tasks from the last lesson.
To get started with the ability to add tasks we’ll create a Vuex mutation that can do just that. It will receive the lists of tasks within a column, the task name/title and simply push a new task into the list of tasks. We will also use uuid
that we import from our utils, to create unique IDs.
/src/store.js
import { uuid, saveStatePlugin } from './utils' // <--- uuid is being imported
...
mutations: {
CREATE_TASK (state, { tasks, name }) {
tasks.push({
name,
id: uuid(),
description: ''
})
}
}
...
Now lets add the mentioned input near the end of the column code, just after the v-for
with the tasks list, but within the v-for
that iterates over the columns.
/src/views/Board.vue
<template>
...
<div class="list-reset">
<div
class="task"
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
@click="goToTask(task)"
>
...
</div>
<input
type="text"
class="block p-2 w-full bg-transparent"
placeholder="+ Enter new task"
@keyup.enter="createTask($event, column.tasks)"
/>
</div>
...
Let’s make use of that mutation inside Board.vue
and add a createTask
method that we will later use on an <input>
element.
/src/views/Board.vue
methods: {
createTask (e, tasks) {
this.$store.commit('CREATE_TASK', { tasks, name: e.target.value })
// clear the input
e.target.value = ''
}
}
Done! We can now start typing new tasks in the input inside each column and after pressing the enter key, it will be added at the end of the column.
While we’re at managing the tasks, let’s also add a way to edit them. For that we need one simple mutation in our Vuex store. I will name it UPDATE_TASK
. The mutation is pretty straightforward. It accepts the task
we want to modify, the property that we want to change on the task and the new value of that property.
/src/store.js
CREATE_TASK (state, { tasks, name }) {
tasks.push({
name,
id: uuid(),
description: ''
})
},
UPDATE_TASK (state, { task, key, value }) {
Vue.set(task, key, value)
}
The editing itself will, however, take place inside the detailed view, that is, inside the / src/views/Task.vue
component. We will add two inputs there instead of just displaying the {{ task.name }}
. This is how the component’s template should look after the changes.
/src/views/Task.vue
<template>
<div class="task-view">
<div class="flex flex-col flex-grow items-start justify-between px-4">
<input
class="p-2 w-full mr-2 block text-xl font-bold"
:value="task.name"
@keyup.enter="updateTaskProperty($event, 'name')"
@change="updateTaskProperty($event, 'name')"
/>
<textarea
placeholder="Enter task description"
class="relative w-full bg-transparent px-2 border mt-2 h-64 border-none leading-normal"
:value="task.description"
@change="updateTaskProperty($event, 'description')"
/>
</div>
</div>
</template>
As you probably noticed, we used a method called updateTaskProperty
that is not yet present in our code, so let’s add it now to our Task.vue
component. It will be the bridge between the interface and the Vuex mutation.
/src/views/Task.vue
methods: {
updateTaskProperty (e, key) {
this.$store.commit('UPDATE_TASK', {
task: this.task,
key,
value: e.target.value
})
}
}
The method accepts the event and the property name that is being modified. It then forwards that information to the UPDATE_TASK
mutation attaching the task in question as the 3rd param.
Now we can both add and update the tasks. There is just one more thing that we need to make the board fully functional as a task manager that will make it possible to track our progress on the tasks. And to accomplish this we need to be able to move the tasks between columns.
Drag & Dropping Tasks
In this lesson we will be using the browser’s drag and drop API to implement the ability to move around tasks in our Trello app. This isn’t the easiest to implement, so this example of how to do it with Vue should be very useful.
We’ll start with our Vuex store. To move the tasks around we will create a new mutation called MOVE_TASK
. It’s purpose is to remove the task from the column it is located and move it to a new column. To make it happen, we need 3 arguments: the task list where the task is currently, the task list we want to move it to, and the index of where the task is located in the first column.
/src/store.js
...
mutations: {
...
MOVE_TASK (state, { fromTasks, toTasks, taskIndex }) {
const taskToMove = fromTasks.splice(taskIndex, 1)[0]
toTasks.push(taskToMove)
}
}
If the code above looks different than in the video, it’ll be changed to what I have listed above, as we thought of better parameter names. Now the hard part! We will need to dive a bit into the Drag’n’Drop browser API. It’s not the easiest browser API out there, so if you haven’t had the chance to use it yet, feel free to pause the video and take a look at the MDN docs. Although we will start with the most naive approach – by the end of this course, we will create a pretty nice abstraction on top of the API.
Let’s open the src/views/Board.vue
component and start building. Because we want to move our tasks, we need to add the draggable
attribute to our <div>
that wraps our task. This will make it possible to /drag/ the div around along with its content. And only that for now at least.
Next step is that we need to react to the task being dragged. We can listen to @dragstart
on the DOM element. The event listener will then be called with the default dragstart
, injected via $event
, the index of our task and the index of our column, $taskIndex
and $columnIndex
respectively. We will create the pickupTask
method and use it here.
/src/views/Board.vue
<div
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
class="task"
draggable
@dragstart="pickupTask($event, $taskIndex, $columnIndex)"
@click="goToTask(task)"
>
Let’s define this pickupTask
method, which is a method in this component:
pickupTask (e, taskIndex, fromColumnIndex) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.setData('task-index', taskIndex)
e.dataTransfer.setData('from-column-index', fromColumnIndex)
},
Inside pickupTask
we’re calling the setData
method on the dataTransfer
interface and saving the taskIndex
and then the same for the sourceListIndex
which stands for the index of the column where the task is located. Take note that the dataTransfer
interface works pretty similar to localStorage
in that it can only store properties that can be /stringified/. This means transferring recursive data structures or functions won’t work.
Now we need to make our column can accept an element being dropped. To make it happen we need another set of listeners, most importantly the @drop
event.
<div class="column"
v-for="(column, $columnIndex) of board.columns"
:key="$columnIndex"
@drop="moveTask($event, column.tasks)"
@dragover.prevent
@dragenter.prevent
>
As you can see, we also need to add two additional listeners, for the dragover
and dragenter
events and we need to prevent the default behavior. Pretty great that we’re using Vue, because we can simply add @dragover.prevent
. Let’s focus on the @drop
event. As the name suggests, it is being triggered when an element being dragged is released on top of our element. The listener will be called with the default $event
and in our case also with the list of tasks in the column.
moveTask (e, toTasks) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
const fromTasks = this.board.columns[fromColumnIndex].tasks
const taskIndex = e.dataTransfer.getData('task-index')
this.$store.commit('MOVE_TASK', {
fromTasks,
toTasks,
taskIndex
})
}
Now the moveTask
method. Once it is called, we can take the information transferred through the dataTransfer
interface and locate our tasks list. Now we have all the data we need to be able to call the MOVE_TASK
mutation. And that’s it, moving tasks should be working now.
In the next lesson we’ll expand this functionality to allow for moving columns around, as well as the ability to move tasks within columns. Stay tuned for that.
Dragging Columns
In this lesson we’ll start with the Vuex mutation for moving columns.
/src/store.js
MOVE_COLUMN (state, { fromColumnIndex, toColumnIndex }) {
const columnList = state.board.columns
const columnToMove = columnList.splice(fromColumnIndex, 1)[0]
columnList.splice(toColumnIndex, 0, columnToMove)
}
Similar to MOVE_TASK
we accept the fro``mColumnIndex
which is the index of the column we want to move, and the to``ColumnIndex
which is the index where we want to move our column. Then we’ll add to our code in our Board component so that our columns become draggable.
/src/views/Board.vue
<div class="column"
v-for="(column, $columnIndex) of board.columns"
:key="$columnIndex"
draggable
@drop="moveTaskOrColumn($event, column.tasks, $columnIndex)"
@dragover.prevent
@dragenter.prevent
@dragstart.self="pickupColumn($event, $columnIndex)"
>
Again, similar to what we did with the tasks, we need to make all columns draggable
. Since now the we will be dropping both tasks and columns on other columns, we need to replace our @drop="moveTask"
handler with a new one, that can decide which action to take. We’ll name it moveTaskOrColumn
. Since our column is now draggable, we need to add a dragstart
handler named pickupColumn
.
We’ve also made a bunch of tweaks to the code in here, to add functionality for dragging and dropping in different places.
/src/views/Board.vue
pickupTask (e, taskIndex, fromColumnIndex) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.setData('from-task-index', taskIndex)
e.dataTransfer.setData('from-column-index', fromColumnIndex)
e.dataTransfer.setData('type', 'task') // <--- New code to identify task
},
pickupColumn (e, columnIndex) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.setData('from-column-index', fromColumnIndex)
e.dataTransfer.setData('type', 'column')
},
moveTaskOrColumn (e, toTasks, toColumnIndex, toTaskIndex) {
const type = e.dataTransfer.getData('type')
if (type === 'task') {
this.moveTask(e, toTasks, toTaskIndex !== undefined ? toTaskIndex : toTasks.length)
} else {
this.moveColumn(e, toColumnIndex)
}
},
moveColumn (e, toColumnIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
this.$store.commit('MOVE_COLUMN', {
fromColumnIndex,
toColumnIndex
})
}
Okay, we can now move both columns and tasks between different columns, but tasks always end up at the very end of the list. That’s hardly useful. So let’s add a way to move the tasks into specific positions within a column. We need to update our template.
/src/views/Board.vue
<div
class="task"
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
draggable
@dragstart="pickupTask($event, $taskIndex, $columnIndex)"
@click="goToTask(task)"
@dragover.prevent
@dragenter.prevent
@drop.stop="moveTaskOrColumn($event, column.tasks, $columnIndex, $taskIndex)"
>
Similar to the column template, we need to add a drop
event listener on every task, so that we can catch other tasks being dropped on top of it.
Now we need to modify moveTask
so that it keeps track of where we drop our task, the task indexes.
moveTask (e, toTasks, toTaskIndex) { // <--- Added toTaskIndex
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
const fromTasks = this.board.columns[fromColumnIndex].tasks
const fromTaskIndex = e.dataTransfer.getData('from-task-index')
this.$store.commit('MOVE_TASK', {
fromTasks,
fromTaskIndex, // <-- added index
toTasks,
toTaskIndex // <-- added index
})
},
Then we’ll update our MOVE_TASK
mutation to move to the proper index.
/src/store.js
// now also accepts `targetIndex`
MOVE_TASK (state, { fromTasks, toTasks, fromTaskIndex, toTaskIndex }) {
const taskToMove = fromTasks.splice(fromTaskIndex, 1)[0]
toTasks.splice(toTaskIndex, 0, taskToMove)
},
With these changes, we can now move tasks to a selected position within the tasks list, drag columns around, and drag tasks within columns.
Adding New Columns
There’s one thing we missed along the way – adding new columns. Let’s quickly fix that starting with the Vuex mutation.
/src/store.js
CREATE_COLUMN (state, { name }) {
state.board.columns.push({
name,
tasks: []
})
},
Now let’s add an “fake” column just after the v-for
with the columns list. This is where we will have a small input to name our new column. This goes right after the div where we iterate through all the columns:
/src/views/Board.vue
<div class="column flex">
<input
class="p-2 mr-2 flex-grow"
placeholder="New column name"
v-model="newColumnName"
@keyup.enter="createColumn"
/>
</div>
The template includes two new things: newColumnName
from local state and createNewColumn
method.
export default {
data () {
return {
newColumnName: ''
}
},
// ...
methods: {
createColumn () {
this.$store.commit('CREATE_COLUMN', {
name: this.newColumnName,
})
this.newColumnName = ''
},
// ...
}
}
And now we can create new columns in our app!
Extracting Components
It’s finally time to clean up our code, and it’s really impressive how detail-oriented Damian gets when it comes to refactoring. One of the most common places where we can look for refactoring material are v-for
loops, and there’s a bunch inside our Board.vue
.
In the video we take the following steps to refactor the column out of the board:
- Create the
/src/components/BoardColumn.vue
file and move the template & CSS code for printing out each column into this file. - Use this new component
<BoardColumn />
inside theBoard.vue
- Add the appropriate props, to send in
column
, andcolumnIndex
. - Look for the methods we need to move into the new component, and copy them over (without deleting them from the
Board.vue
. - Search inside
Board.vue
to see which methods are no longer used and remove the ones that aren’t. - Try things out in the browser, to make sure things work.
- Realize we need to pass in the board into the
BoardColumn.vue
as a prop. - Try things out, and see it working!
Then we extract the tasks out of the column with the following steps:
- We create a
/src/component/ColumnTask.vue
file and move the template & CSS task code out ofBoardColumn.vue
. - Send in
task
,taskIndex
, andcolumnIndex
as a prop to this new file. - Import the
ColumnTask
, register it as a component, and use it inside ofBoardColumn
to loop through all tasks. - Find the methods that need to be copied to
ColumnTask
. - See if there are methods in
BoardColumn
we can remove. - Remove the $ in front of the
$taskIndex
. - Add
column
andboard
to the props, we forgot! - Try it in the browser.
You can checkout our revised code here, but I’ve also listed it below:
/src/views/Board.vue
<template>
<div class="board">
<div class="flex flex-row items-start">
<BoardColumn
v-for="(column, $columnIndex) of board.columns"
:key="$columnIndex"
:column="column"
:columnIndex="$columnIndex"
:board="board"
/>
<div class="column flex">
<input
type="text"
class="p-2 mr-2 flex-grow"
placeholder="New Column Name"
v-model="newColumnName"
@keyup.enter="createColumn"
>
</div>
</div>
<div
class="task-bg"
v-if="isTaskOpen"
@click.self="close"
>
<router-view/>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import BoardColumn from '@/components/BoardColumn'
export default {
components: { BoardColumn },
data () {
return {
newColumnName: ''
}
},
computed: {
...mapState(['board']),
isTaskOpen () {
return this.$route.name === 'task'
}
},
methods: {
close () {
this.$router.push({ name: 'board' })
},
createColumn () {
this.$store.commit('CREATE_COLUMN', {
name: this.newColumnName
})
this.newColumnName = ''
}
}
}
</script>
<style lang="css">
.board {
@apply p-4 bg-teal-dark h-full overflow-auto;
}
.task-bg {
@apply pin absolute;
background: rgba(0,0,0,0.5);
}
</style>
/src/components/BoardColumn.vue
<template>
<div
class="column"
draggable
@drop="moveTaskOrColumn($event, column.tasks, columnIndex)"
@dragover.prevent
@dragenter.prevent
@dragstart.self="pickupColumn($event, columnIndex)"
>
<div class="flex items-center mb-2 font-bold">
{{ column.name }}
</div>
<div class="list-reset">
<ColumnTask
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
:task="task"
:taskIndex="$taskIndex"
:column="column"
:columnIndex="columnIndex"
:board="board"
/>
<input
type="text"
class="block p-2 w-full bg-transparent"
placeholder="+ Enter new task"
@keyup.enter="createTask($event, column.tasks)"
/>
</div>
</div>
</template>
<script>
import ColumnTask from './ColumnTask'
export default {
components: { ColumnTask },
props: {
column: {
type: Object,
required: true
},
columnIndex: {
type: Number,
required: true
},
board: {
type: Object,
required: true
}
},
methods: {
moveTaskOrColumn (e, toTasks, toColumnIndex, toTaskIndex) {
const type = e.dataTransfer.getData('type')
if (type === 'task') {
this.moveTask(e, toTasks, toTaskIndex !== undefined ? toTaskIndex : toTasks.length)
} else {
this.moveColumn(e, toColumnIndex)
}
},
moveTask (e, toTasks, toTaskIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
const fromTasks = this.board.columns[fromColumnIndex].tasks
const fromTaskIndex = e.dataTransfer.getData('from-task-index')
this.$store.commit('MOVE_TASK', {
fromTasks,
fromTaskIndex,
toTasks,
toTaskIndex
})
},
moveColumn (e, toColumnIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
this.$store.commit('MOVE_COLUMN', {
fromColumnIndex,
toColumnIndex
})
},
pickupColumn (e, fromColumnIndex) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.setData('from-column-index', fromColumnIndex)
e.dataTransfer.setData('type', 'column')
},
createTask (e, tasks) {
this.$store.commit('CREATE_TASK', {
tasks,
name: e.target.value
})
e.target.value = ''
}
}
}
</script>
<style lang="css">
.column {
@apply bg-grey-light p-2 mr-4 text-left shadow rounded;
min-width: 350px;
}
</style>
/src/components/ColumnTask.vue
<template>
<div
class="task"
draggable
@dragstart="pickupTask($event, taskIndex, columnIndex)"
@click="goToTask(task)"
@dragover.prevent
@dragenter.prevent
@drop.stop="moveTaskOrColumn($event, column.tasks, columnIndex, taskIndex)"
>
<span class="w-full flex-no-shrink font-bold">
{{ task.name }}
</span>
<p
v-if="task.description"
class="w-full flex-no-shrink mt-1 text-sm"
>
{{ task.description }}
</p>
</div>
</template>
<script>
export default {
props: {
task: {
type: Object,
required: true
},
taskIndex: {
type: Number,
required: true
},
column: {
type: Object,
required: true
},
columnIndex: {
type: Number,
required: true
},
board: {
type: Object,
required: true
}
},
methods: {
pickupTask (e, taskIndex, fromColumnIndex) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.setData('from-task-index', taskIndex)
e.dataTransfer.setData('from-column-index', fromColumnIndex)
e.dataTransfer.setData('type', 'task')
},
goToTask (task) {
this.$router.push({ name: 'task', params: { id: task.id } })
},
moveTaskOrColumn (e, toTasks, toColumnIndex, toTaskIndex) {
const type = e.dataTransfer.getData('type')
if (type === 'task') {
this.moveTask(e, toTasks, toTaskIndex !== undefined ? toTaskIndex : toTasks.length)
} else {
this.moveColumn(e, toColumnIndex)
}
},
moveTask (e, toTasks, toTaskIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
const fromTasks = this.board.columns[fromColumnIndex].tasks
const fromTaskIndex = e.dataTransfer.getData('from-task-index')
this.$store.commit('MOVE_TASK', {
fromTasks,
fromTaskIndex,
toTasks,
toTaskIndex
})
},
moveColumn (e, toColumnIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
this.$store.commit('MOVE_COLUMN', {
fromColumnIndex,
toColumnIndex
})
}
}
}
</script>
<style lang="css">
.task {
@apply flex items-center flex-wrap shadow mb-2 py-2 px-2 rounded bg-white text-grey-darkest no-underline;
}
</style>
You might notice that our BoardColumn
and our ColumnTask
components share duplicate data and methods, which we can eliminate by using a Mixin. We’ll see that in the next lesson.
Creating a Mixin
In the previous lesson we separated our code one step at a time into a BoardColumn
and ColumnTask
component. However, these components are sharing duplicate data and methods, which makes them prime candidates for creating a Mixin.
- Create a
/mixins/movingTasksAndColumnsMixin.js
file - Move the common props of
column
,columnIndex
, andboard
- Move the methods
moveTaskOrColumn
,moveTask
, andmoveColumn
. - Import the Mixin into each of the files and configure it.
We also walk through the code base to see the work we’ve done, and discuss when it is appropriate to use a Mixin and when it might not.
Our mixin ends up looking like this:
/src/mixins/movingTasksAndColumnsMixin.js
export default {
props: {
column: {
type: Object,
required: true
},
columnIndex: {
type: Number,
required: true
},
board: {
type: Object,
required: true
}
},
methods: {
moveTaskOrColumn (e, toTasks, toColumnIndex, toTaskIndex) {
const type = e.dataTransfer.getData('type')
if (type === 'task') {
this.moveTask(e, toTasks, toTaskIndex !== undefined ? toTaskIndex : toTasks.length)
} else {
this.moveColumn(e, toColumnIndex)
}
},
moveTask (e, toTasks, toTaskIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
const fromTasks = this.board.columns[fromColumnIndex].tasks
const fromTaskIndex = e.dataTransfer.getData('from-task-index')
this.$store.commit('MOVE_TASK', {
fromTasks,
fromTaskIndex,
toTasks,
toTaskIndex
})
},
moveColumn (e, toColumnIndex) {
const fromColumnIndex = e.dataTransfer.getData('from-column-index')
this.$store.commit('MOVE_COLUMN', {
fromColumnIndex,
toColumnIndex
})
}
}
}
With these pieces of code moved into a Mixin we can now shorted our BoardColumn and ColumnTask components:
/src/components/BoardColumn.vue
...
<script>
import ColumnTask from './ColumnTask'
import movingTasksAndColumnsMixin from '@/mixins/movingTasksAndColumnsMixin'
export default {
components: { ColumnTask },
mixins: [movingTasksAndColumnsMixin],
methods: {
pickupColumn (e, fromColumnIndex) {
...
},
createTask (e, tasks) {
...
}
}
}
</script>
/src/components/ColumnTask.vue
...
<script>
import movingTasksAndColumnsMixin from '@/mixins/movingTasksAndColumnsMixin'
export default {
mixins: [movingTasksAndColumnsMixin],
props: {
task: {
type: Object,
required: true
},
taskIndex: {
type: Number,
required: true
}
},
methods: {
pickupTask (e, taskIndex, fromColumnIndex) {
...
},
goToTask (task) {
this.$router.push({ name: 'task', params: { id: task.id } })
}
}
}
</script>
And that’s all there is to it. In the next and final lesson we will extract drag and drop functionality into reusable components.
Extracting Drag & Drop
Using the drag and drop API has made our code pretty complicated. In this lesson we turn this API into a reusable set of components that we can use whenever we need the ability to drag and drop. This will help us simplify our code.
We’ll start by creating basic components for our Drag and Drop functionality.
src/components/ App Drag.vue
<template>
<div
draggable
@dragstart.self="onDrag"
@dragover.prevent
@dragenter.prevent
>
<slot/>
</div>
</template>
<script>
export default {
props: {
transferData: {
type: Object,
required: true
}
},
methods: {
onDrag (e) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.setData('payload', JSON.stringify(this.transferData))
}
}
}
</script>
<style lang="css" scoped>
</style>
src/components/ App Drag.vue
<template>
<div
@drop.stop="onDrop"
@dragover.prevent
@dragenter.prevent
>
<slot/>
</div>
</template>
<script>
export default {
methods: {
onDrop (e) {
const transferData = JSON.parse(e.dataTransfer.getData('payload'))
this.$emit('drop', transferData)
}
}
}
</script>
<style lang="css" scoped>
</style>
Now that we should use these two components. First, let’s refactor the BoardColumn to use them.
src/components/ BoardColumn .vue
<template>
<AppDrop
@drop="moveTaskOrColumn"
>
<AppDrag
class="column"
:transferData="{
type: 'column',
fromColumnIndex: columnIndex
}"
>
<div class="flex items-center mb-2 font-bold">
{{ column.name }}
</div>
<div class="list-reset">
<ColumnTask
v-for="(task, $taskIndex) of column.tasks"
:key="$taskIndex"
:task="task"
:taskIndex="$taskIndex"
:column="column"
:columnIndex="columnIndex"
:board="board"
/>
<input
type="text"
class="block p-2 w-full bg-transparent"
placeholder="+ Enter new task"
@keyup.enter="createTask($event, column.tasks)"
/>
</div>
</AppDrag>
</AppDrop>
</template>
<script>
import ColumnTask from './ColumnTask'
import AppDrag from './AppDrag'
import AppDrop from './AppDrop'
import movingTasksAndColumnsMixin from '@/mixins/movingTasksAndColumnsMixin'
export default {
components: {
ColumnTask,
AppDrag,
AppDrop
},
mixins: [movingTasksAndColumnsMixin],
...
In order to make this work the moveTaskOrColumn
function needs to be updated inside our mixin, since now only the transferData
is being passed in as an argument.
src/ mixins / movingTasksAndColumnsMixin.js
export default {
...
methods: {
moveTaskOrColumn (transferData) {
if (transferData.type === 'task') {
this.moveTask(transferData)
} else {
this.moveColumn(transferData)
}
},
moveTask ({ fromColumnIndex, fromTaskIndex }) {
const fromTasks = this.board.columns[fromColumnIndex].tasks
this.$store.commit('MOVE_TASK', {
fromTasks,
fromTaskIndex,
toTasks: this.column.tasks,
toTaskIndex: this.taskIndex
})
},
moveColumn ({ fromColumnIndex }) {
this.$store.commit('MOVE_COLUMN', {
fromColumnIndex,
toColumnIndex: this.columnIndex
})
}
}
}
We then can simplify the ColumnTask.vue component to use our drag and drop component:
src/components/ ColumnTask .vue
<template>
<AppDrop
@drop="moveTaskOrColumn"
>
<AppDrag
class="task"
:transferData="{
type: 'task',
fromColumnIndex: columnIndex,
fromTaskIndex: taskIndex
}"
@click="goToTask(task)"
>
<span class="w-full flex-no-shrink font-bold">
{{ task.name }}
</span>
<p
v-if="task.description"
class="w-full flex-no-shrink mt-1 text-sm"
>
{{ task.description }}
</p>
</AppDrag>
</AppDrop>
</template>
<script>
import movingTasksAndColumnsMixin from '@/mixins/movingTasksAndColumnsMixin'
import AppDrag from './AppDrag'
import AppDrop from './AppDrop'
export default {
components: { AppDrag, AppDrop },
mixins: [movingTasksAndColumnsMixin],
props: {
task: {
type: Object,
required: true
},
taskIndex: {
type: Number,
required: true
}
},
methods: {
goToTask (task) {
this.$router.push({ name: 'task', params: { id: task.id } })
}
}
}
</script>
As you can see, we were able to remove the whole pickTask
method since it has become redundant.
Now we have two components, AppDrag and AppDrop, we can easily use whenever we need to implement any sort of drag and drop functionality in the future.
You made it to the end, congratulations for that!