Created
November 20, 2019 21:26
Drag and drop in CSS grid with rails 6, stimulus and rails-ujs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-# https://johnbeatty.co/2018/03/09/stimulus-js-tutorial-how-do-i-drag-and-drop-items-in-a-list/ | |
.grid--draggable{ 'data-controller': 'seating-plan', | |
'data-seating-plan-endpoint': endpoint, | |
'data-action': 'dragstart->seating-plan#onDragStart dragover->seating-plan#onDragOver dragenter->seating-plan#onDragEnter drop->seating-plan#onDrop dragend->seating-plan#onDragEnd' } | |
- seating_plan.each do |seat| | |
- if seat[:is_empty] | |
.grid__item.grid__item--empty{ 'data-row': seat[:row], | |
'data-col': seat[:col], | |
style: "grid-row: #{seat[:row]}; grid-column: #{seat[:col]};", | |
class: ('grid__item--border' if seat[:is_border]) } | |
- else | |
.grid__item{ draggable: 'true', | |
'data-item-id': seat[:student].id, | |
'data-row': seat[:row], | |
'data-col': seat[:col], | |
style: "grid-row: #{seat[:row]}; grid-column: #{seat[:col]};" } | |
.card | |
.card-content | |
%p.title.has-text-centered= seat[:student].name | |
%footer.card-footer | |
%p.card-footer-item.has-text-weight-bold | |
-# TODO Add sum | |
4 | |
%p.card-footer-item.has-text-grey | |
-# TODO Add sum | |
10 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# models/school_class.rb | |
class SchoolClass < ApplicationRecord | |
strip_attributes | |
has_many :students | |
def seating_plan | |
row_min, row_max = students.map { |s| s.seat_row }.minmax | |
col_min, col_max = students.map { |s| s.seat_col }.minmax | |
row_offset = row_min - 2 | |
col_offset = col_min - 2 | |
# Can't use double splat operator { **tmp, s.seat_hash => s } with non-symbol (object) keys | |
students_index = students.reduce({}) { |tmp, s| tmp.merge({seat_hash(s.seat_row, s.seat_col) => s}) } | |
seat_coordinates = (row_min - 1..row_max + 1).to_a.product((col_min - 1..col_max + 1).to_a) | |
return seat_coordinates.map do |row, col| | |
{ | |
row: row - row_offset, | |
col: col - col_offset, | |
is_empty: !students_index.has_key?(seat_hash(row, col)), | |
student: students_index[seat_hash(row, col)], | |
is_border: row < row_min || row > row_max || col < col_min || col > col_max, | |
} | |
end | |
end | |
def seating_plan=(seats) | |
# TODO exception handling | |
transaction do | |
seats.each(&(method :update_seat)) | |
end | |
end | |
private | |
def seat_hash(row, col) | |
{row: row, col: col}.freeze | |
end | |
def update_seat(student_id:, row:, col:) | |
student = students.find(student_id) | |
# Bypass validation (when swapping seats, one seat is briefly occupied twice) | |
student.attributes = { seat_row: row, seat_col: col } | |
student.save! validate: false | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SchoolClassesController < ApplicationController | |
before_action :set_school_class, only: [:seating_plan] | |
def seating_plan | |
seating_plan = seating_plan_params[:students].map { |s| s.to_h.symbolize_keys } | |
puts "Plan: #{seating_plan.inspect}" | |
@school_class.seating_plan = seating_plan | |
respond_to do |format| | |
format.html { redirect_back fallback_location: school_classes_path, notice: t('.notice') } | |
end | |
end | |
private | |
# Use callbacks to share common setup or constraints between actions. | |
def set_school_class | |
@school_class = SchoolClass.find(params[:id]) | |
end | |
# Never trust parameters from the scary internet, only allow the white list through. | |
def seating_plan_params | |
params.require(:school_class).permit(students: [:student_id, :row, :col]) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import 'bulma/bulma'; | |
.grid--draggable { | |
display: grid; | |
grid-gap: 10px; | |
grid-auto-rows: 1fr; | |
grid-auto-columns: 1fr; | |
padding: 20px 0; | |
[draggable] { | |
cursor: move; /* fallback if grab is not supported */ | |
cursor: grab; | |
} | |
.grid__item--border { | |
visibility: hidden; | |
} | |
&.grid--dragging { | |
.grid__item--border { | |
visibility: visible | |
} | |
.grid__item--empty { | |
background-color: $grey-lightest; | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Controller} from 'stimulus' | |
import Rails from '@rails/ujs' | |
// https://johnbeatty.co/2018/03/09/stimulus-js-tutorial-how-do-i-drag-and-drop-items-in-a-list/ | |
export default class extends Controller { | |
onDragStart(event) { | |
this.element.classList.add('grid--dragging') | |
const data = JSON.stringify({ | |
row: event.target.getAttribute('data-row'), | |
col: event.target.getAttribute('data-col'), | |
}) | |
event.dataTransfer.setData('application/drag-key', data) | |
event.dataTransfer.effectAllowed = 'move' | |
console.log('start') | |
} | |
onDragOver(event) { | |
event.preventDefault() | |
console.log('over') | |
return true | |
} | |
onDragEnter(event) { | |
event.preventDefault() | |
console.log('enter') | |
} | |
getPosition(el) { | |
return [el.getAttribute('data-row'), el.getAttribute('data-col')] | |
} | |
getPositions() { | |
const items = [...this.element.querySelectorAll('.grid__item[draggable]')] | |
return items.map(el => ({ | |
student_id: el.getAttribute('data-item-id'), | |
row: this.getPosition(el)[0], | |
col: this.getPosition(el)[1], | |
})) | |
} | |
setPosition(el, [row, col]) { | |
el.setAttribute('data-row', row) | |
el.setAttribute('data-col', col) | |
el.style['grid-row'] = row | |
el.style['grid-column'] = col | |
} | |
swap(el1, el2) { | |
const el1Pos = this.getPosition(el1) | |
const el2Pos = this.getPosition(el2) | |
this.setPosition(el1, el2Pos) | |
this.setPosition(el2, el1Pos) | |
} | |
onDrop(event) { | |
const { row, col } = JSON.parse(event.dataTransfer.getData("application/drag-key")) | |
const targetEl = event.target.closest('[data-row][data-col]') | |
const sourceEl = this.element.querySelector(`[data-row='${row}'][data-col='${col}']`) | |
this.swap(sourceEl, targetEl) | |
event.preventDefault() | |
console.log('drop') | |
} | |
submit(data) { | |
Rails.ajax({ | |
url: this.data.get('endpoint'), | |
type: 'put', | |
beforeSend(xhr, options) { | |
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8') | |
// Workaround: add options.data late to avoid Content-Type header to already being set in stone | |
// https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee#L53 | |
options.data = JSON.stringify(data) | |
return true | |
}, | |
}); | |
} | |
onDragEnd(event) { | |
this.element.classList.remove('grid--dragging') | |
this.submit({ | |
school_class: { | |
students: this.getPositions() | |
} | |
}) | |
console.log('end') | |
} | |
} |
Author
ftes
commented
Nov 20, 2019
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment