Skip to content

Lesson 7 — Composing views from components

The boards index already composes EmptyState and the board card. This lesson looks at composition more deliberately — how to decide when to extract a component, how to pass data through a composition chain, and how to keep views readable as complexity grows.

When to extract a component

The board card in Views::Boards::Index is currently a private method:

1
2
3
4
5
6
def render_board_card(board)
  a(href: board_path(board), class: "block p-6 ...") do
    h2 { board.name }
    p  { "#{board.columns.count} columns" }
  end
end

When should this become Components::BoardCard? Ask:

  • Is it reused? If the board card appears in multiple views, extract it.
  • Does it have meaningful variants? If the card needs a compact mode, an archived state, or a loading skeleton, extract it.
  • Does it have testable logic? If the card makes decisions — show a badge if the board is archived, truncate the name at a certain length — extract it so that logic can be tested in isolation.

For now the board card is simple and appears in one place. Leave it as a private method. We will extract it in Module 9 when the board show page needs a more complex version.

Passing data through a composition chain

A view passes data to a component which passes data to a sub-component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# View passes board to a component
render_board_card(board)

# Component uses board data to compose Badge
class Components::BoardCard < Components::Base
  prop :board, Board

  def view_template
    article do
      h2 { @board.name }
      Badge(label: member_count, variant: :default)
    end
  end

  private

  def member_count
    "#{@board.members.count} members"
  end
end

The data flows in one direction: view → component → sub-component. Each level only knows about its own props — BoardCard knows about board, Badge knows about label and variant. Neither knows about the other.

Keeping views readable

As views grow, private methods become the primary tool for keeping view_template readable. The pattern we use throughout KanbanFlow:

  • view_template reads like a table of contents — high-level structure only
  • Private render_ methods handle the detail of each section
  • Components handle anything with meaningful variants, logic, or reuse
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def view_template
  render_header
  render_columns
  render_footer
end

private

def render_header
  div(class: "...") do
    h1 { @board.name }
    Breadcrumb() { ... }
  end
end

def render_columns
  @board.columns.each { |col| render_column(col) }
end

This structure means any developer can open a view and immediately understand what it renders, before reading a single implementation detail.