Episodes

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

# Gemfile
source '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 equivalent
dragMoveListener = 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.rb
class 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.rb
class 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.rb
class Ingredient < ApplicationRecord
  has_many :recipes, dependent: :destroy, class_name: 'Recipe'
  has_many :foods, through: :recipes
end

# models/recipe.rb
class Recipe < ApplicationRecord
  belongs_to :food
  belongs_to :ingredient
end

# visitors_controller.rb
class 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>