Your group project will be to create a 'Task List' application similar to this (note you are free to design the UI however you like!):
You may therefore want to use drag and drop
to move tasks from one list (e.g. To Do) to another (e.g. In Progress) hence today we will discover how to implement this using JavaScript.
ondragstart
, ondragover
, ondrop
and the HTML attribute draggable
.To make an element 'draggable', you simply set the draggable
attribute to true. For example:
<img id="myImageElement" src="image.png" draggable="true">
When you start dragging, an dragstart
event is triggered. We want to intercept this event and keep track of which element is being dragged.
<img id="myImageElement" src="image.png" draggable="true"> ondragstart="drag(event)">
function drag(event) {
<!-- record which element is being dragged -->
event.dataTransfer.setData("text", event.target.id);
}
To allow an element to be dragged into a second element, we need to disable the default behaviour of the second element on dragover
to allow this:
<div id="myTargetElement"
ondrop="drop(event)"
ondragover="allowDrop(event)">
</div>
function allowDrop(event) {
event.preventDefault();
}
On drop
we retrieve the data from the dragged element and add it as a child element of the target:
function drop(event) {
<!-- determine the element which was dragged -->
const data = event.dataTransfer.getData("text");
<!-- add it to our HTML -->
event.target.appendChild(document.getElementById(data));
}
By using Chrome's Developer Tools (Elements) we can see how the drag and drop has affected the HTML:
Note that there are a number of Drag and Drop libraries for JavaScript for example SortableJS.
index.html
in the Day 2 solution.ondragstart
, ondrop
and the html attribute draggable
.To take on this weeks coding challenge you will need to know how to drag and drop. Lets start by making our HTML element 'draggable'. Add the 'draggable' HTML attribute to your task element. What effect has this had?
When you start dragging an event is triggered. We want to intercept this event and keep track of which element is being dragged.
`<li id="${task.id}" draggable="true" ondragstart="app.run('ondragstart', event)">`
Make the task.id
the id of the HTML element. Add the update function onDragStart
. Notice we are going to pass in the event object.
onDragStart: (state, event) => {
event.dataTransfer.setData('text', event.target.id)
return state
}
When we start dragging we set the drag event up to transfer some data, the id of the task being dragged. Can you see how we read the id of the element being dragged from the event
object and in particular the event.target
object.
Lets make it so if you drag a task from the list it will delete it. For that we need to detect the drop event, and we need to make a drop zone. Can you put another element next to your list? Get them to sit next to each other on the page.
The hotpink section is the drop zone. HTML elements by default are NOT droppable. To make a drop zone, we need to override the default behavior of the browser. To do this you will have to add a ondragover
event listener to your drop zone element. You can also add the ondrop
event listener, and add the event handler to ondrop
.
<div ondragover="event.preventDefault()" ondrop="app.run('onDrop', event)"></div>
Here we are calling event.preventDefault()
whenever an element is dragged over this element. By doing this we will enable dropping on that element. Then we add our ondrop
event handler and capture the drop event. We need the event
object from the drop to get the data values we passed from the dragging element.
Delete the item from the array in state.tasks
.
onDrop: (state, event) => {
event.preventDefault()
const id = event.dataTransfer.getData('text')
const index = state.tasks.findIndex(task => task.id == id)
state.tasks.splice(index, 1)
return state
}
What happens if we have nested drop zones? For now imagine I want to either 'highlight' a task or delete it.
I'm going to add a different update function that will add a 'highlight' class to my task. The behavior I am looking for is when I drag and drop on the highlight zone, my task should become highlighted. However! It doesn't work. My task just gets deleted. Why is that?
The drop event will cause BOTH event handlers to be triggered; onDrop
and my onHighlight
. The event propagates up through the DOM. This behavior is called bubbling. I don't want this. When the ondrop
listener triggers my onHighlight
event handler I want the event propagation to stop. I will have to call event.stopPropagation()
to stop the event triggering other event handlers.
onHighlight: (state, event) => {
event.preventDefault()
event.stopPropagation()
const id = event.dataTransfer.getData('text')
const index = state.tasks.findIndex(task => task.id === id)
state.tasks[index].highlight = 'highlight'
return state
}
There are some important concepts we have looked at in this lesson. First of all we have learnt more about the event object. We have seen how it can be used to:
XMLHTTPRequest
and fetch
The ajax pattern lead to a different kind of web site. A site that was closer to an app, one HTML page is loaded, then the following pages, and interactions are all driven by javascript. It is this pattern that we are going to implement in our group projects. To follow along we'll make a simple server that will store our tasks.
This express server will server our single HTML page, and then all Consequential requests will be for the data it needs to run. Create a server.js
file in your project folder. Create a public
folder and put your index.html
, style.css
and main.js
in that folder. Set express up to serve static assets from the public
folder, and deal with json requests, finally run your server on port 3000.
const express = require('express')
const app = express()
app.use(express.static('public'))
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
const tasks = [
{
id: 1,
text: 'Fetch all the tasks',
status: 0
},
{
id: 2,
text: 'Create a new task',
status: 0
}
]
app.listen(3000, () => {
console.log('app server running on port', 3000)
})
I have added some tasks on the server. Our first job will be to fetch these once the page has loaded.
We are going to use the fetch
API that comes bundled in the javascript runtime in the browser. We need an update function in our AppRun update object that is going to get all the tasks on the server.
getTasks: async (state) => {
state.tasks = await fetch('/tasks').then(res => res.json())
return state
}
What kind of function is getTasks? We can make a simple 'get' request using fetch
it uses the Promise
object. fetch
will return an HTTP response object, we then need to transform that into usable JSON. In the example above we can expect the /tasks
endpoint to return an Array of tasks. We assign that into state and that will trigger a re-render.
On the server we can add the /tasks
endpoint:
app.get('/tasks', (req, res) => {
res.send(tasks)
})
The key take away here is - no templates. We don't do templating on the server, we just respond with the data our single page app needs. The next refactor is to create a task on the server, not just on the frontend. Update your add
update function, don't push the new task into state.tasks
. Instead post it to the server, and then trigger app.run('getTasks')
.
add: (state, form) => {
const data = new FormData(form)
const task = {
id: window.crypto.getRandomValues(new Uint8Array(2)).join(''),
text: data.get('text'),
highlight: '',
status: 0
}
const postRequest = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(task)
}
fetch('/tasks', postRequest).then(() => app.run('getTasks'))
return state
},
Making a POST request is similar to a GET request, only now we include a javascript object with the settings for;
On the server we can create an endpoint to receive the postRequest
;
app.post('/tasks', (req, res) => {
tasks.push(req.body)
res.send()
})
here we reply with res.send()
on the frontend we are waiting for this, we can then make a request for the updated tasks by calling app.run('getTasks')
. This is a design choice to demonstrate how one AppRun update function can call another update function, like a chain of events. You could return all the tasks from the POST request above, and then update the state with the new task list all within the 'add' update function.
fetch
fetch