Skip to content

Lesson 6 — KanbanFlow: domain, Tailwind, and first views

This is the payoff lesson. We scaffold the KanbanFlow domain, apply Tailwind styling to Phlex::UI, and build the first real views.

The domain

KanbanFlow has five models. Generate them in this order so references resolve correctly:

1
2
3
4
5
6
bin/rails generate model User email:string name:string
bin/rails generate model Board name:string user:references
bin/rails generate model Column name:string position:integer board:references
bin/rails generate model Card title:string position:integer column:references
bin/rails generate model Membership role:integer user:references board:references
bin/rails db:migrate

Note: We’re creating a minimal User model here with just email and name. Authentication — login, logout, password handling, sessions — is covered in Module 10. For now User is a plain ActiveRecord model that gives our associations the correct shape.

Add associations and validations to each model:

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
  validates :name,  presence: true

  has_many :memberships, dependent: :destroy
  has_many :boards, through: :memberships
end
1
2
3
4
5
6
7
8
# app/models/board.rb
class Board < ApplicationRecord
  belongs_to :user

  has_many :memberships, dependent: :destroy
  has_many :members, through: :memberships, source: :user
  has_many :columns, -> { order(:position) }, dependent: :destroy
end
1
2
3
4
5
# app/models/column.rb
class Column < ApplicationRecord
  belongs_to :board
  has_many :cards, -> { order(:position) }, dependent: :destroy
end
1
2
3
4
# app/models/card.rb
class Card < ApplicationRecord
  belongs_to :column
end
1
2
3
4
5
6
7
# app/models/membership.rb
class Membership < ApplicationRecord
  belongs_to :user
  belongs_to :board

  enum :role, { member: 0, admin: 1 }
end

Note the distinction between board.user and board.members:

  • board.user — the owner who created the board
  • board.members — everyone with access including the owner, via memberships

This is a deliberate design — ownership and membership are separate concepts. A board has one owner but can have many members. In Module 10 we use this distinction to implement access control.

Seed data

With the models in place, add some seed data so every subsequent lesson has realistic content to work with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# db/seeds.rb
alice = User.create!(name: "Alice", email: "alice@example.com")
bob   = User.create!(name: "Bob",   email: "bob@example.com")

board = Board.create!(name: "KanbanFlow Development", user: alice)
Membership.create!(user: alice, board: board, role: :admin)
Membership.create!(user: bob,   board: board, role: :member)

todo  = board.columns.create!(name: "To Do",        position: 0)
doing = board.columns.create!(name: "In Progress",  position: 1)
done  = board.columns.create!(name: "Done",         position: 2)

todo.cards.create!(title: "Set up Rails app",     position: 0)
todo.cards.create!(title: "Install Phlex",        position: 1)
todo.cards.create!(title: "Build component library", position: 2)
doing.cards.create!(title: "Build AppLayout",     position: 0)
done.cards.create!(title: "Create Rails app",     position: 0)

Run it:

1
bin/rails db:seed

Verify in the console:

1
2
3
Board.first.name          # => "KanbanFlow Development"
Board.first.members.count # => 2
Board.first.columns.count # => 3

Restyling Phlex::UI with Tailwind

The component structure from Module 3 is unchanged. We only update the class strings. Here is Components::Button as an example — compare with the Pico version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# app/components/button.rb
class Components::Button < Components::Base
  BASE_CLASSES = "inline-flex items-center justify-center rounded-md " \
                 "font-medium transition-colors focus:outline-none " \
                 "focus:ring-2 focus:ring-offset-2 disabled:opacity-50 " \
                 "disabled:pointer-events-none"

  VARIANTS = {
    primary:   "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
    secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500",
    danger:    "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
    ghost:     "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500",
  }.freeze

  SIZES = {
    sm: "px-3 py-1.5 text-sm",
    md: "px-4 py-2 text-base",
    lg: "px-6 py-3 text-lg",
  }.freeze

  prop :label,    String
  prop :variant,  Symbol,   default: :primary
  prop :size,     Symbol,   default: :md
  prop :disabled, _Boolean, default: -> { false }
  prop :type,     String,   default: -> { "button" }

  def view_template
    button(
      type:     @type,
      class:    class_names(BASE_CLASSES, VARIANTS[@variant], SIZES[@size]),
      disabled: @disabled,
      aria:     { disabled: @disabled.to_s }
    ) { @label }
  end
end

Rather than restyling each component manually — which is mechanical work that teaches nothing new about Phlex — download the pre-styled components and drop them into app/components/. The prop declarations, slot methods, and view_template structure are identical to Module 3. Only the Tailwind class strings have changed.

Download phlex-ui-tailwind.zip

Copy the contents into your Rails app, run the tests to confirm everything is working.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
% bin/rails test test/components/
Running 53 tests in parallel using 12 processes
Run options: --seed 9306

# Running:

.....................................................

Finished in 0.211445s, 250.6562 runs/s, 595.8996 assertions/s.
53 runs, 126 assertions, 0 failures, 0 errors, 0 skips

This is a good point to finish off Module 04. We have a basic structure in place that we can build on. Coming up in Module 05 we will investigate Tailwind in more detail and start to create some more complex views.