Lesson 3 — The full board view
With morphing enabled, the board view is straightforward. The server
renders columns and cards; Turbo morphs any changes. We don’t need
explicit IDs for Turbo targeting — though we’ll use dom_id as good
practice for Stimulus targeting and debugging.
dom_id in Phlex
dom_id generates stable, predictable IDs from ActiveRecord objects:
|
|
Include the helper in Components::Base:
|
|
With morphing, dom_id is primarily useful for Stimulus controllers to
target specific elements, and for debugging — seeing id="card_42" in
the DOM tells you exactly which record you’re looking at. It’s good
practice, not a Turbo requirement.
Components::KanbanCard
|
|
Components::KanbanColumn
|
|
Views::Boards::Show
|
|
Routes
|
|
Shallow nesting means column routes use /columns/:id rather than
/boards/:board_id/columns/:id, and card routes use /cards/:id
rather than the fully nested path. This keeps URLs clean and controllers
simple.
Before we write any code
The board view is a composition of three components:
Views::Boards::Show
└── KanbanColumn (one per column)
└── KanbanCard (one per card)
└── Dropdown (actions menu)Show is responsible for the page structure and the “Add column” link.
KanbanColumn owns a single column — its header, its list of cards, and
the “Add card” link.
KanbanCard owns a single card — its content and
its actions dropdown.
Each component knows only about its own data; it receives what it needs as props and renders its own slice of the UI.
KanbanColumn and KanbanCard live in app/components/ — they are
app-specific rendering components, but they don’t handle forms or
submission logic.
Form components (CardForm, ColumnForm, BoardForm)
live in app/views/ alongside the views that use them, following the
same convention established with BoardForm in Module 4.
They inherit Components::Base rather than Views::Base since they render as
fragments, not full pages.
A note on constant lookup in namespaced components
When you declare a prop inside Components::KanbanCard, Ruby resolves
constant names relative to the current namespace. This means:
|
|
If Components::Card exists (our UI Card component does), Literal finds
it instead of the Card ActiveRecord model and raises a type mismatch.
The fix is to anchor the constant to the root namespace with :::
|
|
Apply :: to any prop that references an ActiveRecord model from within
a namespaced component.
This includes ::Column, ::Board, and any
other model props you add. It becomes second nature quickly — any time
you’re inside Components:: and referencing a model, prefix it.
Model scopes
The board view orders columns and cards. Add scopes to the models:
|
|
Users, boards, and membership
Before we wire up board creation, it’s worth understanding how the user-board relationship is modelled in KanbanFlow.
A user can relate to a board in two ways:
- Ownership — the user created the board. Stored as
user_idon theboardstable. - Membership — the user has been invited to the board. Stored in the
membershipsjoin table.
These are different relationships and need different associations. The User model needs both:
|
|
current_user.owned_boards — boards this user created.
current_user.boards — all boards this user is a member of (the full set, used in Module 11).
The Board model needs a direct belongs_to — not optional: true, because every board must have an owner:
|
|
Your database should already have a NOT NULL constraint on boards.user_id. If you previously added optional: true to work around an error, remove it now — the constraint is correct and optional: true was masking the real problem, which was the missing owned_boards association.
In the boards controller, use owned_boards when creating:
|
|
This sets user_id correctly through the direct association. Using current_user.boards would attempt to build through the memberships join table and leave user_id as nil — which is what causes the NOT NULL constraint failure.
The current_user method itself is stubbed in ApplicationController until Module 11:
Stubbing current_user
|
|
current_user is stubbed for now — it returns User.first until Module 11 wires up real authentication. The stub lives in ApplicationController so it’s available everywhere the real implementation will be.
What current_user.boards returns
There’s an important distinction between the two board associations on User that affects what appears in the boards index:
current_user.owned_boards — boards where boards.user_id = current_user.id. This is the direct ownership association.
current_user.boards — boards the user is a member of, via the memberships join table. A board only appears here if a Membership record exists linking the user to that board.
When you create a board via current_user.owned_boards.build, the board gets user_id set correctly — but no Membership record is created. So current_user.boards won’t include it.
This means the boards index must use owned_boards for now:
|
|
In Module 11, when membership is properly wired, board creation will also create a membership record for the owner. At that point the index can show all boards the user has access to — both owned and invited. Until then, owned_boards is correct and honest.
This is a good example of a design decision that looks minor early on but has real consequences. The two associations serve different purposes: owned_boards for ownership and admin rights, boards for access. Both will be used in Module 11.
Boards controller
|
|
Columns controller
|
|
Cards controller
|
|
Form components
|
|
|
|
Views for columns and cards
|
|
|
|
|
|
|
|
What we have so far
With morphing enabled and these views in place, KanbanFlow already handles the full CRUD lifecycle for boards, columns, and cards. Creating a card redirects back to the board — Turbo morphs the new card into the column. Deleting a card redirects back — Turbo morphs it away. No Frames, no Streams, no explicit wiring.
The board view navigates to separate pages for creating and editing cards and columns. That’s intentional for now — in Lesson 6 we’ll bring the creation forms inline using Turbo Frames, which is one of the two things Frames still do well.