-
-
Save tbcooney/fcc545c690a112a47b4dd55aed1ccde1 to your computer and use it in GitHub Desktop.
Drag and drop in CSS grid with rails 6, stimulus and rails-ujs
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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') | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Set time aside and try to extended this for an interactive seat picker.