How to Approach Modelling: a Kanban board in Rails
Last week, Adrian Marin asked for my take on how to model a Kanban board since they're about to write that for Avo.
You can read their blog post on what they're up to here.
I'm just going to model this for a Rails app and skip Avo's constraint that they don't want to ship migrations into their users' apps (it's a fine design constraint, I'm just skipping it here).
Let's dive in.
The initial riff
When modelling something, my first step is to use my riffing design process to dissect and get an overview of the problem quickly.
So I open up a blank Ruby file and start tossing around ideas, names and relationships.
It goes fast and I don't remember all the details. I've recorded a replay of my actual edit history to show it.
Pro Tip: pause, rewatch parts and/or slow the speed down to better see what's going on.
Stuff to notice
I'm adding some callouts for what's going on in the riffing above.
Notice how I'm:
- Playing around with the names of variables and methods.
- Playing around with the
Row#position
being a float that we can subdivide endlessly. - Adding the
Column#column
polymorphism to return self, sonew_adjacent_position
can have different semantics, but the calling code is still straightforward around ~1:05. - Adding comments for attributes like
title
andposition
.
Revising as I go: I'm generally writing to get an idea out, but once I've seen it, I move on and start revising it.
This process helps me get things out of my head first, then settle a bit and once I'm ready I can start the next bit.
Other times I end up revising it immediately, like Column#re_place(row)
that became Row#reposition_onto(new_column)
around ~26s.
When I'm stuck or thinking I:
- Add details or syntax later, like
dependent: :destroy
- Move my edit cursor around (I tend to click around, but because this is a replay it looks like my mouse is static)
There's typos! They're actually useful since they give me moments to get ready for the next thought to pop in.
What I was thinking as I was riffing
The Board, Column and Row structure more or less came immediately. It's pretty standard Active Record modelling so it didn't require much thought.
Over time, I was picturing a Stimulus controller that would handle dragging and dropping rows onto columns. It would seek upwards to the row above if any, or the column itself.
Those then become the cursor that we're positioning our dropped row after.
Here's the video code for reference.
class Kanban::Board < ActiveRecord::Base
has_many :columns, dependent: :destroy
# title
end
class Kanban::Column < ActiveRecord::Base
belongs_to :board
has_many :rows, dependent: :destroy
def column = self # Mask as a row for positioning
def new_adjacent_position
rows.pick(:position) - positioning_fragment
end
def positioning_fragment = 0.000001
end
# Or maybe Kanban::Column::Placement?
class Kanban::Column::Row < ActiveRecord::Base
belongs_to :column
delegate :positioning_fragment, to: :column
belongs_to :record, polymorphic: true # I wouldn't do Delegated Type since you want more flexibility here.
# position, float
# cursor can be both a Column and a Row.
def reposition_onto(cursor)
update! column: cursor.column, position: cursor.new_adjacent_position
end
def new_adjacent_position
position + positioning_fragment
end
end
Next steps
Picking more purposeful names
I know that Column
and Row
are probably the better more sensible choice, but also this is my newsletter and I want names that are more fun and differentiated.
I want names that fit this domain better in my mind.
So now I'm going with Kanban::Stage
and Kanban::Stage::Placement
from here on out.
Knowing early: our positioning structure won't work
There's a slight issue with my initial idea for positioning.
It won't actually work. Womp-womp.
Well, now we know early! And we didn't commit to this code, so we're safe.
I like to describe these early insights as me being incompetent. It's often my worst fear, so deliberately naming it makes it almost a non-issue. The truth is, it's not that severe: we're just figuring things out.
Labelling it as incompetence also helps me realize that I can't continue on the path we're on and I need to come up with an alternative.
Anyway, there's probably multiple reasons why it's not the right structure, but notice how if we drop a placement onto the same placement twice, the two dropped placements would end up with the same position
.
Managing positions correctly
There's a bunch of complexity needed to handle positions correctly. Look at ranked-model and Positioning for some examples.
I don't want to yank in a gem for this, if I can help it. But I also don't want to maintain incidental complexity here.
Most positioning approaches require calculating a position relative to other elements and save that in a column, so the database can do an efficient order by clause.
But programming languages already have an Array data structure that knows the element ordering. Ruby and JavaScript both do.
Ruby's Array#insert
can insert another element at a passed index:
[:a, :b].insert(1, :sup) # => [:a, :sup, :b]
Then in Postgres, any column can also be an Array.
Rails supports migrations for it with t.integer :placements_positions, array: true
.
Combining these exact features, I've got a version that I think will work. Read the gist here.
The gist also shows how to make executable single-file scripts with Postgres specific features.
The idea is that with placements_positions
storing the ordering of the stage's placements by ids, we could render something like this:
<%# app/views/kanban/stages/_stage.html.erb %>
<section id="<%= dom_id(stage) %>"
data-controller="positioned" data-positioned-ids-value="<%= stage.placements_positions" %>">
<% stage.placements.each do |placement| %>
<%= tag.article placement.title, data: { id: placement.id } %>
<% end %>
</section>
The Stimulus controller would use the ids to reorder the placements. A mutation observer could detect the insertion of a new placement and send a request to the server to update the position via stage.place
.
This post is running long, so I'm skipping some details. Hope the overall idea makes sense!
Summary
We looked at some sketch modelling for Kanban boards in Rails.
We looked at some failings within that and responded to them early.
We came up with a potential alternative for positioning that warrants further investigation.
Alternatives
- Sean Doyle and I discussed this yesterday and he showed me his progressively enhanced version of Kanban boards. Definitely worth checking out!
- Jeremy Smith posted a video of an alternative approach to drag and drop sorting with acts_as_list that's also worth a watch.
That's it. If you found this post interesting, can I ask that you share it with your coworkers? Thanks for reading!