Challenge - Recreating Clear with Pure JavaScript

If you don’t already follow @wesbos on twitter, you probably should. Recently, he has been posting some interesting javascript challenges - see the first one and the second one. I found those challenges really helpful, so I decided to create one myself.

I use the todo application, Clear, a lot and thought it would be an interesting challenge to recreate the core interactions in the browser.

The Challenge #

This is what the final product looks like -

Clear with JavaScript

Start the Challenge | Live Demo with My Solution

The key interactions -

  • Drag a list item right to mark it as complete - Once the user has dragged the item past a certain point and releases the mouse, the item should swipe away fully.
  • Completed items appear at the bottom - The completed item should reappear at the bottom of the list.
  • Drag a list item left to delete it - Deleted items are removed from the HTML completely.
  • Drag left on a completed item to delete it - The user can drag a completed item left to delete it. Dragging right should not do anything.

If you want to try out this challenge, you can get the boilerplate HTML and CSS here. The aim of the challenge is to use only JavaScript, but if you’re a beginner you can use jQuery as well.

My Solution #

If you're going to try the challenge yourself, don't look past here. Otherwise, this is the solution that I came up with. It can be summarised in four key functions.

addMouseDownEventListener() #

In this function, I add an event listener on mousedown for all of the list items. When this event is fired, 2 other event listeners are added - mousemove and mouseup.

function addMouseDownEventListener() {

// Reset values to use in handleMouseMove()
cursorXPosition = 0;
cursorXPositionDiff = 0;

// Loop through all tasks and add the event listener to each
for ( var i = 0; i < tasks.length; i++ ) {
tasks[i].addEventListener("mousedown", function(event) {
this.addEventListener("mousemove", handleMouseMove, false);
this.addEventListener("mouseup", handleMouseUp, false);
})
}
}

handleMouseMove() #

This is the function that is called when the user has clicked and moved the mouse together (so essentially, dragging). It does two things -

  1. Drags the list item along with the mouse
  2. Adds a class of .completing or .deleting depending on if the list item has been dragged right/left by a certain threshold
var handleMouseMove = function(event) {

if ( cursorXPosition === 0 ) { cursorXPosition = event.x; }

// Set the margin-left of the list item to move with the mouse
cursorXPositionDiff = event.x - cursorXPosition;
event.target.style.marginLeft = cursorXPositionDiff + 'px';

var taskIsNotCompleted = !(event.target.className.indexOf("completed") > -1);

// Add class if the cursorXPositionDiff gets to a certain amount (40px)
if ( cursorXPositionDiff > 40 && taskIsNotCompleted ) {
event.target.classList.add('completing');

} else if ( cursorXPositionDiff < -40 ) {
event.target.classList.add('deleting');

} else {
event.target.classList.remove('completing');
event.target.classList.remove('deleting');
}
}

handleMouseUp() #

This is the function that is called after the user releases the grip on the list item. It checks if the item has been marked as .completing, .deleting, or neither, and handles each case -

var handleMouseUp = function(event) {

var className = event.target.className;

if ( className.indexOf('completing') > -1 ) {
// SWIPE AWAY CURRENT LIST ITEM (RIGHT)
swipeElement(event.target.id, true);

} else if ( className.indexOf('deleting') > -1 ) {
// SWIPE AWAY CURRENT LIST ITEM (LEFT)
swipeElement(event.target.id, false);

} else {
// REPOSITION LIST ITEM BACK TO NORMAL POSITION
event.target.style.marginLeft = "";
}

// Remove mousemove listener, and re-add mousedown listeners to all list items
this.removeEventListener("mousemove", handleMouseMove, false);
addMouseDownEventListener();
}

swipeElement() #

This function handles the swiping away/in of the list items -

function swipeElement(elementID, swipingRight, swipingIn) {

var element = document.getElementById(elementID);

// Swiping an element out - marking as completed or done
function swipeElementOut() {
var currentMargin = getCurrentMargin(element),
currentMargin = swipingRight ? currentMargin += 1 : currentMargin -= 1;

element.style.marginLeft = currentMargin + '%';

if ( currentMargin == 100 | currentMargin == -100 ) {
clearInterval(interval);
// Either append completed item to bottom or delete item
swipingRight ? appendCompletedItem(element) : element.remove();
}
}

// Swiping an element in - appending completed item to list
function swipeElementIn() {
var currentMargin = getCurrentMargin(element);
currentMargin -= 1;

element.style.marginLeft = currentMargin + '%';
if ( currentMargin == 0 ) { clearInterval(interval); }
}

// Choose which function to use
var interval = swipingIn ? setInterval(swipeElementIn, 5) : setInterval(swipeElementOut, 5);
}

There's still a lot I can do to improve this, but this is the solution I have found for now. You can view my full solution here.

The real value in doing these challenges is seeing how others tackle the problem. So, if you do this, do leave a comment with a link to your codepen/github and I’ll add a list of everyone’s solutions.

Other Solutions #

Keep in touch KeepinTouch

Subscribe to my Newsletter 📥

Receive quality articles and other exclusive content from myself. You’ll never receive any spam and can always unsubscribe easily.

Elsewhere 🌐