Drop a like

Sept. 20, 2018, 10:30 a.m.

Last night I managed to do something that I'm really excited about. Ever since I developed Makro I've wanted it to have a feature which I got working for the next version. The feature in question is drag and drop. More specifically being able to drag a food that has been added to a meal to a new meal. It really does not sound anything special but like a year ago I just couldn't wrap my head around it. I mean I didn't even try properly but to be honest I don't think I would have managed to do it anyways.

So let me set up the scenario. Each meal is its own component and those are dynamically created based on if the user adds food to that meal. So basically it's just a ngFor-loop. Then the user realizes that he/she added the food to a wrong meal and before the only way to do it was to add it again to the correct meal and remove it from the other one. Now the user is going to be able to just drag the food to another meal-component. And that is quite cool in my very subjective opinion.

First I added (drop)="drop($event)" and (dragover)="allowDrop($event)" to the table and draggable="true" (dragstart)="drag($event, food)" to the row which contains the food information. When I got it working my wife immediately noticed a bug. When dragging the food and letting go while still inside the same table (=meal component) it would delete the food. That happened because when moving the food I had add it to the new array, then remove it from the array where it was and calculate totals. While doing that I did not make sure that the source and target is not the same because then the food would just be removed from the food array and not really being added to anywhere. The source and target were decided based on the row id which was set up with [attr.id]="i" (i being index of ngFor-loop).

That was an easy fix though. Just check if the source and target (still based on the row id) and do nothing if that is the case. But my wife already had submitted a new request regarding to that. She wanted to be able to rearrange the foods inside the meal. So now I had to come up a way to find out where exactly the dragged food was dropped. Enter a new event handler: (mouseenter)="setTarget(i)". So basically when the mouse enters a row it sets the dropTargetIndex to be the index of the food inside the array. However that wasn't enough though because when you are dragging something the mouse enter event is not triggered until you release the food you were dragging. I worked around this by inserting a timeout of 10ms which is enough for the dropTargetIndex to be updated to new value but not enough for user to move their mouse.

In the end it looks like this:

HTML:

<table *ngIf="meal?.foods.length > 0" (drop)="drop($event)" (dragover)="allowDrop($event)">

<tr *ngFor="let food of meal.foods; let i = index">

<th class="t-head drag" scope="row" draggable="true" (dragstart)="drag($event, food)" [attr.id]="i" (mouseenter)="setTarget(i)">

Component.ts

setTarget(index) {

  this.dropTargetIndex = index;

}

 

drag(ev, food) {

  ev.dataTransfer.setData('food', JSON.stringify(food));

  ev.dataTransfer.setData('index', this.componentIndex);

  ev.dataTransfer.setData('start', ev.target.id);

}

 

drop(ev) {

  const food = JSON.parse(ev.dataTransfer.getData('food'));

  const mealName = this.meal.name;

  const index = ev.dataTransfer.getData('index');

  const start = ev.dataTransfer.getData('start');

  if (parseInt(index) === parseInt(this.componentIndex)) {

    setTimeout(() => {

      this.meal.foods.splice(start, 1);

      this.meal.foods.splice(this.dropTargetIndex, 0, food);

      this.addedFoodsService.updateMealsInLocalStorage(this.meal);

    }, 10);

  return;

  } else {

    this.addedFoodsService.moveFoodToNewMeal(food, mealName, index);

  }

}

 

allowDrop(ev) {

  ev.preventDefault();

}