First a little background for this article. When I was starting as junior dev I had a task to implement real time drag and drop. Back then we used vue.js on front end and asp mvc on backend with signalR implementation of socket.io that was synchronizing items in the grid among the all conntected users in the same group. I somehow managed to make it work but as far as i remember solution was rather ugly and not so reliable.
This occantionaly came to mine mind because I know that i can sure do much better solution now. I wanted to do it again, just for mine personal satisfaction. I just needed something simple, just enough for proof of concept so i can finally get away. After few years it turns out to be super simple..
How it works
We just get Ids of items in exact order as they are displayed for the user that triggered and finished the ordering. Then push that order for all of the connected users. In last step reorder items in DOM based on that order we received. It is possible to store order of Ids in database in case we would like to persist the order of items.
You can check out working example on github
Prerequisites
- node.js
- socket.io and jQuery libaries, that is acctually all we need.
Setting up
Create new folder and cd to that folder.
Install required packages.
– npm install –save express@4.15.2
– npm install –save socket.io
Server side
To be able to run socket.io we will need to have server running. Socket.io will emit events with payload to browser of all connected users. We will hook socket.io to express node module, which will act as server and listen to the incoming requests. Implementation is pretty straightforward.
Create server.js file in root directory and copy paste following code.
var express = require('./node_modules/express'); // Create a express application var app = express(); // Serve static files from public directory e.g. scripts, pages, css... app.use(express.static('public')); /* Pass express application to HTTP server module, and then pass http server to the socket.io Http server is needed because if you try to pass express directly to socket.io you will recieve Error: You are trying to attach socket.io to an express request handler function. Please pass a http.Server instance */ var http = require('http').Server(app); var io = require('./node_modules/socket.io')(http); // Render index.html as startup page app.get('/', function (req, res) { res.sendFile(__dirname + '/public/index.html'); }); // This is event will be triggered once any of the users will load the page io.on('connection', function (socket) { // When event to change order is triggered from sender socket.on('changed_order', function (itemIds) { // Then emit event to every user that is connected with payload of sender (that is itemIds) io.emit('changed_order', itemIds); }); }); // Bind server to listen on default socket.io port http.listen(3000, function () { console.log('listening on *:3000'); });
Front end
First we will create html page that will be served to the client. Create new folder in root directory called public, create new index.html file in that folder and copy paste following html.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Sortable demo</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script> <script src="/socket.io/socket.io.js"></script> <!-- Custom scripts --> <script src="/socket_client.js"></script> <!-- Styles --> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <ul id="sortable"> <li id="ef7002fe-5611-4235-a6b3-3f8ee02caada">Item 1</li> <li id="f1855aff-1f4b-442f-aa55-ee30bd7f347f">Item 2</li> <li id="54c22255-c0fe-4a24-aa2c-d228df3f438d">Item 3</li> <li id="0fea02ea-772b-40e7-bf1c-d12f86bb11ca">Item 4</li> <li id="80ddede4-8397-423d-9238-84c093312ebf">Item 5</li> </ul> </body> </html>
Once we have page created, we will need to connect socket.io client to the server so they can transmit the messages between each other. Actually this one-liner do all the magic. You don’t have to even specify url to connect to, by default it will use the url of server that is rendering the page.
const socket = io();
Last step is to create server.js file in public directory. We will put all the logic of reordering items and transmitting events there.
$(document).ready(function () { // Connect socket client to the server that served the page const socket = io(); socket.on('changed_order', function (itemIds) { orderItemsInDom(itemIds); }); $('#sortable').sortable({ // Ensure items can be dragged only verticaly axis: 'y', // Event is triggered when the user stopped sorting and the DOM position has changed. update: function (event, ui) { const DOMItems = $('#sortable').find('li'); const itemIds = getIds(DOMItems); // When one user changed order of items, reorder items in DOM for all connected users socket.emit('changed_order', itemIds); } }); function getIds(items) { return $.map(items, function (item) { return $(item).attr("id"); }); } // Order items in DOM based on the order of Ids arg function orderItemsInDom(itemIds) { let lastSeenItem = null; for (let itemId of itemIds) { // Select item in DOM by ID let item = $("#" + itemId)[0]; // Remove that item from DOM $(item).remove(); if (lastSeenItem === null) { // Add item to the top of the grid because we know that if lastSeenItem is null item is first on the list $('#sortable').prepend(item); } else { $(item).insertAfter(lastSeenItem); } lastSeenItem = item; } } });
Optionally we can add a little bit of css to make sortable list look more polished. Create new style.css file in public directory, reference it in index.html and copy following css.
#sortable li { width: 300px; margin-top: 10px; padding: 5px; border: 1px solid #95979b; text-align: center; list-style: none; }