Drag and Drop with Interact.js

#75 Drag and Drop with Interact.js
4/9/2017

Summary

Using Interact.js to create draggable and droppable items in our view, we can use AJAX callbacks on events to interact with our Ruby on Rails application. Also, learn how to use Ruby Assets to manage our Javascript Libraries.
10
rails javascript assets ajax view 15:20 min

Resources

Interact.js Website - http://interactjs.io/
Interact.js Github - https://github.com/taye/interact.js
Rails Assets - https://rails-assets.org/
Source - https://github.com/driftingruby/075-drag-and-drop-with-interact-js

A better way to write the original lookup for which foods can be created

@foods = Food.joins(recipes: :ingredient).where(ingredients: {id: params[:ingredients]})

Summary

Gemfilesource 'https://rails-assets.org' do
  gem 'rails-assets-interact'
end
application.js//= require interact
...
var dragMoveListener;

dragMoveListener = function(event) {
  var target, x, y;
  target = event.target;
  x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
  y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
  target.style.webkitTransform = target.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
  target.setAttribute('data-x', x);
  return target.setAttribute('data-y', y);
};

window.dragMoveListener = dragMoveListener;

interact('*[data-draggable="true"]').draggable({
  inertia: true,
  autoScroll: true,
  onmove: dragMoveListener
});


$(document).on('turbolinks:load', function(){
  interact('#favorite_foods').dropzone({
    accept: '*[data-draggable="true"]',
    overlap: 0.75,
    ondropactivate: function(event) {},
    ondragenter: function(event) {
      event.target.classList.add('drop-target');
      event.relatedTarget.classList.add('can-drop');
      return $.get(event.relatedTarget.attributes['data-url'].value, {
        favorite: true
      });
    },
    ondragleave: function(event) {
      event.target.classList.remove('drop-target');
      event.relatedTarget.classList.remove('can-drop');
      return $.get(event.relatedTarget.attributes['data-url'].value, {
        favorite: false
      });
    },
    ondrop: function(event) {},
    ondropdeactivate: function(event) {
      event.target.classList.remove('drop-active');
      return event.target.classList.remove('drop-target');
    }
  });


  var ingredients = [];

  interact('#have_ingredients').dropzone({
    accept: '*[data-draggable="true"]',
    overlap: 0.75,
    ondropactivate: function(event) {},
    ondragenter: function(event) {
      event.target.classList.add('drop-target');
      event.relatedTarget.classList.add('can-drop');
      ingredients.push(event.relatedTarget.attributes['data-ingredient'].value);
      return $.get(event.relatedTarget.attributes['data-url'].value, { ingredients: ingredients });
    },
    ondragleave: function(event) {
      event.target.classList.remove('drop-target');
      event.relatedTarget.classList.remove('can-drop');
      ingredients = jQuery.grep(ingredients, function(value) {
        return value != event.relatedTarget.attributes['data-ingredient'].value;
      });
      return $.get(event.relatedTarget.attributes['data-url'].value, { ingredients: ingredients });
    },
    ondrop: function(event) {},
    ondropdeactivate: function(event) {
      event.target.classList.remove('drop-active');
      return event.target.classList.remove('drop-target');
    }
  });
});
coffeescript equivalentdragMoveListener = undefined

dragMoveListener = (event) ->
  target = undefined
  x = undefined
  y = undefined
  target = event.target
  x = (parseFloat(target.getAttribute('data-x')) or 0) + event.dx
  y = (parseFloat(target.getAttribute('data-y')) or 0) + event.dy
  target.style.webkitTransform = target.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
  target.setAttribute 'data-x', x
  target.setAttribute 'data-y', y

window.dragMoveListener = dragMoveListener
interact('*[data-draggable="true"]').draggable
  inertia: true
  autoScroll: true
  onmove: dragMoveListener
$(document).on 'turbolinks:load', ->
  interact('#favorite_foods').dropzone
    accept: '*[data-draggable="true"]'
    overlap: 0.75
    ondropactivate: (event) ->
    ondragenter: (event) ->
      event.target.classList.add 'drop-target'
      event.relatedTarget.classList.add 'can-drop'
      $.get event.relatedTarget.attributes['data-url'].value, favorite: true
    ondragleave: (event) ->
      event.target.classList.remove 'drop-target'
      event.relatedTarget.classList.remove 'can-drop'
      $.get event.relatedTarget.attributes['data-url'].value, favorite: false
    ondrop: (event) ->
    ondropdeactivate: (event) ->
      event.target.classList.remove 'drop-active'
      event.target.classList.remove 'drop-target'
  ingredients = []
  interact('#have_ingredients').dropzone
    accept: '*[data-draggable="true"]'
    overlap: 0.75
    ondropactivate: (event) ->
    ondragenter: (event) ->
      event.target.classList.add 'drop-target'
      event.relatedTarget.classList.add 'can-drop'
      ingredients.push event.relatedTarget.attributes['data-ingredient'].value
      $.get event.relatedTarget.attributes['data-url'].value, ingredients: ingredients
    ondragleave: (event) ->
      event.target.classList.remove 'drop-target'
      event.relatedTarget.classList.remove 'can-drop'
      ingredients = jQuery.grep(ingredients, (value) ->
        value != event.relatedTarget.attributes['data-ingredient'].value
      )
      $.get event.relatedTarget.attributes['data-url'].value, ingredients: ingredients
    ondrop: (event) ->
    ondropdeactivate: (event) ->
      event.target.classList.remove 'drop-active'
      event.target.classList.remove 'drop-target'
  return
visitors.scss.dropzone {
  height: 180px;
  background-color: #ccc;
  border: dashed 4px transparent;
  border-radius: 4px;
  margin: 10px auto 30px;
  padding: 10px;
  width: 80%;
  transition: background-color 0.3s;
}

.drop-active {
  border-color: #aaa;
}

.drop-target {
  background-color: #29e;
  border-color: #fff;
  border-style: solid;
}

.drag-drop {
  display: inline-block;
  min-width: 40px;
  padding: 1em 0.75em;

  color: #fff;
  background-color: #29e;
  border: solid 2px #fff;

  -webkit-transform: translate(0px, 0px);
          transform: translate(0px, 0px);

  transition: background-color 0.3s;
}

.drag-drop.can-drop {
  color: #000;
  background-color: #4e4;
}
foods_controller.rbclass FoodsController < ApplicationController
  def opinion_on_food
    @food = Food.find(params[:id])
    @food.update_attribute(:favorite, params[:favorite])
    @food.save
    head :ok
  end

  def what_to_cook
    @foods = Food.includes(:recipes).all.select {|i| i.recipes.map(&:ingredient_id).to_set.subset?(params[:ingredients].to_a.map(&:to_i).to_set) }
  end
end
models/food.rbclass Food < ApplicationRecord
  has_many :recipes, dependent: :destroy, class_name: 'Recipe'
  has_many :ingredients, through: :recipes

  def self.favorite_foods
    where(favorite: true)
  end

  def self.no_opinion_foods
    where(favorite: false)
  end
end
models/ingredient.rbclass Ingredient < ApplicationRecord
  has_many :recipes, dependent: :destroy, class_name: 'Recipe'
  has_many :foods, through: :recipes
end
models/recipe.rbclass Recipe < ApplicationRecord
  belongs_to :food
  belongs_to :ingredient
end
visitors_controller.rbclass VisitorsController < ApplicationController
  def favorite_foods
  end

  def ingredients
    @ingredients = Ingredient.all
  end
end
foods/_food.html.erb

<li class='list-group-item'>   <strong><%= food.name %></strong>   <%= content_tag :span, 'and it is one of your favorites' if food.favorite? %> </li>

foods/what_to_cook.js.erb

<% if @foods.size > 0 %> $('#foods').html('<%= j render @foods %>'); <% else %> $('#foods').html('<li class="list-group-item">Sorry, you may go hungry...</li>'); <% end %>

visitors/favorite_foods.html.erb

<% Food.no_opinion_foods.each do |food| %>   <%= content_tag :div, food.name, class: 'drag-drop', data: { draggable: true, url: opinion_on_food_path(food) } %> <% end %> <div id="favorite_foods" class="dropzone">   <h1>Favorite Foods</h1>   <% Food.favorite_foods.each do |food| %>     <%= content_tag :div, food.name, class: 'drag-drop can-drop', data: { draggable: true, url: opinion_on_food_path(food) } %>   <% end %> </div>

visitors/ingredients.html.erb

<% @ingredients.each do |ingredient| %>   <%= content_tag :div, ingredient.name, class: 'drag-drop', data: { draggable: true, url: what_to_cook_path, ingredient: ingredient.id } %> <% end %> <div id="have_ingredients" class="dropzone">   <legend>Ingredients I have</legend> </div> <div class="panel panel-primary">   <div class="panel-heading">     <h3 class="panel-title">You have the ingredients to make</h3>   </div>   <ul id='foods' class='list-group'>     <li class="list-group-item">Sorry, you may go hungry...</li>   </ul> </div>


7865030?v=3&s=64
Schwad said 4 months ago:

This episode is amazing, great work. Lately I have been thinking about better ways to manage javascript and also venturing out into some of the more responsive front end stuff (really not wanting to do the full dive into Angular or React, etc), so this was a very valuable episode. Thank you!

Login to Comment