Skip to content

Instantly share code, notes, and snippets.

@ftes
Created November 20, 2019 21:26
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ftes/f1d5ef2fb99031856bf354e99ea8e3ee to your computer and use it in GitHub Desktop.
Save ftes/f1d5ef2fb99031856bf354e99ea8e3ee to your computer and use it in GitHub Desktop.
Drag and drop in CSS grid with rails 6, stimulus and rails-ujs
-# 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
# 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
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
@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;
}
}
}
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')
}
}
@ftes
Copy link
Author

ftes commented Nov 20, 2019

dnd-css-grid-rails-stimulus-ujs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment