Skip to content

Lesson 4 — Rendering from controllers

In traditional Rails, controllers render views implicitly:

1
2
3
4
5
def index
  @boards = Board.all
  # Rails implicitly renders app/views/boards/index.html.erb
  # with @boards available as an instance variable
end

With Phlex, rendering is explicit. The controller passes data directly to the view object:

1
2
3
def index
  render Views::Boards::Index.new(boards: Board.all)
end

Why explicit rendering matters

No implicit instance variables. In ERB, the controller sets @boards and the view accesses it. The coupling is invisible — rename the variable in the controller and the view silently breaks. In Phlex, the view’s initialize method declares exactly what it needs. If the controller passes the wrong data, you get an immediate Literal::TypeError at the point of instantiation.

The view’s interface is documented in code. Views::Boards::Index.new(boards:) tells you everything about what this view needs. No hunting through the controller for instance variables.

Views are testable in isolation. Because data is passed explicitly, you can instantiate and render a view in a test with known data — no controller or request required.

The render call

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class BoardsController < ApplicationController
  def index
    render Views::Boards::Index.new(boards: current_user.boards)
  end

  def show
    render Views::Boards::Show.new(board: current_user.boards.find(params[:id]))
  end

  def new
    render Views::Boards::New.new(board: Board.new)
  end
end

Rails understands Phlex views as renderables. The render call works exactly as it does with ERB — it handles response codes, content types, and layout wrapping (which is disabled since our Views::Base handles it via around_template).

Redirects and non-HTML responses

Redirects work exactly as normal — they don’t involve views at all:

1
2
3
4
def create
  board = current_user.boards.create!(board_params)
  redirect_to board_path(board), notice: "Board created."
end

For JSON responses, use render json: as usual. Phlex views are only involved when rendering HTML.

Flash messages

Flash messages need to be read somewhere in the layout. Since our layout is a Phlex component, read them there:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Inside Components::Layouts::AppLayout view_template:
body do
  if flash[:notice]
    Alert(message: flash[:notice], variant: :success)
  end
  if flash[:alert]
    Alert(message: flash[:alert], variant: :danger)
  end
  main(class: "container mx-auto px-4 py-8") { yield }
end

Exercise

The lesson showed how Phlex eliminates implicit instance variables. To make that concrete, try recreating the ERB anti-pattern in Phlex and see why it fails.

Create a Views::Home::About view and wire it up to a route:

1
2
# config/routes.rb
get "about", to: "home#about"
1
2
3
4
5
6
7
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def about
    @title = "About KanbanFlow"   # set an instance variable the ERB way
    render Views::Home::About.new  # but don't pass it to the view
  end
end
1
2
3
4
5
6
# app/views/home/about.rb
class Views::Home::About < Views::Base
  def view_template
    h1 { @title }   # try to use the instance variable
  end
end

Visit /about. You will see an empty <h1>@title is nil because Phlex views are plain Ruby objects. The controller’s instance variables are not magically shared with the view.

Now fix it the Phlex way — update the controller to pass title: explicitly and update the view to declare it in initialize. Confirm the heading renders correctly.

Before we do, better check that we’ve included Literal::Properties in our Components::Base class. If it’s not there, add it now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
We're now starting to use prop declarations in our views and components. For this to work, Literal::Properties must be extended into Components::Base. If you haven't already done this, update the file now:
ruby# app/components/base.rb
# frozen_string_literal: true

class Components::Base < Phlex::HTML
  include Phlex::Rails::Helpers::Routes
  extend Literal::Properties              #<< make sure this line is added

  if Rails.env.development?
    def before_template
      comment { "Before #{self.class.name}" }
      super
    end
  end

  private

  def class_names(*classes)
    classes.flatten.compact.reject(&:empty?).join(" ")
  end
end

Since Views::Base inherits from Components::Base, all views automatically get prop as well — no changes needed there.


Solution
1
2
3
4
5
6
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def about
    render Views::Home::About.new(title: "About KanbanFlow")
  end
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Views::Home::About < Views::Base
  prop :title, String

  def view_template
    h1(class: "text-2xl font-bold mb-4") { @title }
    p(class: "text-gray-500") do
      plain "KanbanFlow is a multi-user Kanban board built with Phlex and Rails 8."
    end
  end
end

You might notice that we didn’t set page_title here so it would default to the value in the AppLayout. We can fix this by adding a method to our Views::Base so that we infer the page_title from the class name. We can always override it if we need to.

Update the Views::Base with this new def page_title method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Views::Base < Components::Base
  def cache_store = Rails.cache

  def page_title
    self.class.name
      .sub("Views::", "")
      .gsub("::", " — ")
      .gsub(/([A-Z])/, ' \1')
      .strip
  end

  def around_template
    render Components::Layouts::AppLayout.new(title: page_title) do
      super
    end
  end
end

Now visit the /about page and check the browser tab.

Note: As we organise our Base classes you can see how easy it is to add default functionality to our views and components.