A Vue.js powered Todo list with drag-and-drop reordering.
A Pen by Carson Ford on CodePen.
<div id="app"> | |
<div class="container"> | |
<h1 class="title">{{ title }}</h1> | |
<div class="card"> | |
<div class="card__inner"> | |
<form> | |
<input type="text" placeholder="Create a new todo" v-model="newItem" /> | |
<button @click="addItem" :disabled="newItem.length === 0">Add Todo</button> | |
</form> | |
<p v-if="items.length === 0">No todos!</p> | |
<ul class="todo-list"> | |
<li class="todo-list__list-item" :class="{'todo-list__list-item--completed': item.completed}" :id="item.id" draggable="true" @dragStart="dragStart($event)" @dragend="dragEnd($event)" @drop="onDrop($event, item)" @dragover.prevent @dragenter.prevent v-for="item in items"> | |
<div class="todo-list__move"></div> | |
<div class="todo-list__toggle" @click="toggleComplete(item)"></div> | |
<span class="todo-list__name">{{ item.name }}</span> | |
<div class="todo-list__remove" @click="removeItem(item.id)">+</div> | |
</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
</div> |
const { createApp } = Vue | |
createApp({ | |
data() { | |
return { | |
title: 'Vue Todo List', | |
newItem: '', | |
items: [ | |
{ id: 1, name: 'Write', completed: true }, | |
{ id: 2, name: 'Draw', completed: false }, | |
{ id: 3, name: 'Read', completed: false }, | |
], | |
} | |
}, | |
methods: { | |
addItem() { | |
this.items.push({ | |
id: Date.now(), | |
name: this.newItem, | |
completed: false, | |
}); | |
this.newItem = ''; | |
}, | |
dragStart(event) { | |
event.target.classList.add('todo-list__list-item--dragging'); | |
}, | |
dragEnd(event) { | |
event.target.classList.remove('todo-list__list-item--dragging'); | |
}, | |
onDrop(event, item) { | |
// check if the item is being dropped on a different one | |
let selectedElement = document.querySelector('.todo-list__list-item--dragging'); | |
let selectedElementId = parseInt(selectedElement.getAttribute('id')); | |
if (selectedElementId !== item.id) { | |
// determine the order of the drop target | |
let selectedIndex = this.items.findIndex(obj => { return obj.id === selectedElementId; }); | |
let targetIndex = this.items.findIndex(obj => { return obj.id === item.id }); | |
// insert selected before target | |
let selectedObject = this.items[selectedIndex]; | |
this.items.splice(selectedIndex, 1); | |
this.items.splice(targetIndex, 0, selectedObject); | |
} | |
}, | |
toggleComplete(item) { | |
item.completed = !item.completed | |
}, | |
removeItem(itemID) { | |
this.items = this.items.filter(item => item.id !== itemID) | |
} | |
} | |
}).mount('#app') |
<script src="https://unpkg.com/vue@3"></script> |
$color-primary: #1aa7f5; | |
$color-primary-dark: #1a4d7e; | |
$color-secondary: #e65e75; | |
$color-neutral-light: #a49ebd; | |
$color-neutral: #2f2a43; | |
$color-neutral-dark: #222031; | |
$color-white: #ffffff; | |
$color-black: #0f0e13; | |
* { | |
box-sizing: border-box; | |
} | |
body { | |
background-color: $color-neutral-dark; | |
background: linear-gradient($color-neutral, $color-neutral-dark); | |
color: $color-white; | |
font-family: 'Poppins', sans-serif; | |
height: 100vh; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
h1, h2, h3, h4, h5, h6, p { | |
margin-top: 0; | |
} | |
form { | |
margin-bottom: 24px; | |
position: relative; | |
} | |
input { | |
background: $color-neutral-dark; | |
border-radius: 40px; | |
border: 1px solid transparent; | |
box-shadow: inset 0 2px 4px rgba($color-black, 0.25); | |
color: $color-white; | |
display: block; | |
font-size: 16px; | |
height: 100%; | |
outline: none; | |
padding: 14px 120px 14px 16px; | |
width: 100%; | |
&::placeholder { | |
color: $color-neutral-light; | |
font-style: italic; | |
} | |
} | |
button { | |
background: $color-primary; | |
border: none; | |
border-radius: 40px; | |
box-shadow: 0 4px 8px -6px rgba($color-black, 0.5); | |
border-top: 1px solid rgba($color-white, 0.05); | |
color: $color-white; | |
cursor: pointer; | |
font-size: 16px; | |
font-weight: bold; | |
height: 32px; | |
padding: 0 12px; | |
position: absolute; | |
right: 8px; | |
transition: background-color 0.2s ease; | |
top: 8px; | |
&:hover { | |
background-color: $color-primary-dark; | |
} | |
&:disabled { | |
background-color: $color-neutral; | |
color: $color-neutral-light; | |
cursor: not-allowed; | |
} | |
} | |
.title { | |
text-align: center; | |
} | |
.container { | |
margin: 0 auto; | |
max-width: 700px; | |
padding: 40px; | |
} | |
.card { | |
align-items: center; | |
backdrop-filter: blur(80px) brightness(1.25); | |
border-radius: 8px; | |
box-shadow: 0 8px 16px -12px rgba($color-black, 0.5); | |
border-top: 1px solid rgba($color-white, 0.05); | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 24px; | |
&__inner { | |
padding: 32px 24px; | |
width: 100%; | |
} | |
p:last-of-type { | |
margin-bottom: 0; | |
} | |
} | |
.todo-list { | |
clip-path: polygon(0 1px, 100% 1px, 100% calc(100% - 1px), 0 calc(100% - 1px)); | |
display: flex; | |
flex-direction: column; | |
list-style: none; | |
margin: 0; | |
padding: 0; | |
&__list-item { | |
align-items: center; | |
border-bottom: 1px solid $color-neutral-dark; | |
border-top: 1px solid $color-neutral-dark; | |
display: flex; | |
justify-content: flex-start; | |
margin-bottom: -1px; | |
padding: 8px 0; | |
&--completed { | |
.todo-list__name { | |
color: $color-neutral-light; | |
text-decoration: line-through; | |
} | |
.todo-list__toggle { | |
background-color: $color-neutral-light; | |
border-color: $color-neutral-light; | |
} | |
} | |
&--dragging { | |
opacity: 0.25; | |
} | |
} | |
&__move { | |
background: radial-gradient(#222031 25%, transparent 25%) 0 0 / 6px 6px; | |
cursor: ns-resize; | |
height: 16px; | |
margin-right: 8px; | |
width: 12px; | |
} | |
&__toggle { | |
border: 1px solid $color-neutral-light; | |
border-radius: 999px; | |
cursor: pointer; | |
height: 20px; | |
margin-right: 8px; | |
width: 20px; | |
} | |
&__name { | |
line-height: 1; | |
} | |
&__remove { | |
color: $color-neutral-light; | |
cursor: pointer; | |
font-size: 24px; | |
margin-left: auto; | |
margin-top: -2px; | |
text-decoration: none !important; | |
transform: rotate(45deg); | |
&:hover { | |
color: $color-secondary; | |
} | |
} | |
} |
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" /> |
A Vue.js powered Todo list with drag-and-drop reordering.
A Pen by Carson Ford on CodePen.