Excerpt

If you’ve ever worked on a Rails app, you’ve probably run into problems with the view layer turning into a mess as the app gets older or the team gets bigger. Rails attempts to tame this problem with solutions like strict locals, but I’ve found them to be cumbersome and view them as a worse implementation of Ruby method definitions.
Fortunately component libraries like Phlex and ViewComponent use Ruby method definitions to create a more sane boundary between application code and views, but using them as controller views hasn’t been straightforward and requires a lot of boilerplate.
That’s where Superview comes in—Superview is a gem that makes it possible to build Rails applications from the ground-up using nothing but components. If a class instance responds to renders_in and has attr_writer

If you’ve ever worked on a Rails app, you’ve probably run into problems with the view layer turning into a mess as the app gets older or the team gets bigger. Rails attempts to tame this problem with solutions like strict locals, but I’ve found them to be cumbersome and view them as a worse implementation of Ruby method definitions.
Fortunately component libraries like Phlex and ViewComponent use Ruby method definitions to create a more sane boundary between application code and views, but using them as controller views hasn’t been straightforward and requires a lot of boilerplate.
That’s where Superview comes in—Superview is a gem that makes it possible to build Rails applications from the ground-up using nothing but components. If a class instance responds to renders_in and has attr_writer methods, Superview can assign the controllers’ instance variables to the component and render it if the class name matches the action name.
## Implicit template rendering in Rails
Before we dive into Superview, let’s understand how good we have it in Rails with view templates. Today a typical Rails blog posts controller action might look like this:
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action { @post = Post.find(params[:id]) }
# Implicitly renders ./app/views/posts/show.html.erb
end
```
Out of the box, Rails is configured to look for views in the ./app/views directory for the requested format. The major accomplishment is the code we didn’t have to write to find and render the template. Can we accomplish the same thing with component views? Let’s find out.
## The tedious way of manually rendering Phlex or ViewComponent views
Phlex and ViewComponent views can be rendered in a Rails controller today, but it requires a bit of boilerplate that starts to feel really repetative as the number of actions grows in your apps.
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action { @post = Post.find(params[:id]) }
def show
respond_to do |format|
# Rendering a component without Superview 👎
format.html { render PostComponent.new(post: @post) }
end
end
end
```
The lines of code start to add up when you multiply these lines of code across each action in your Rails application. There has to be a better way, right?
## Automatically render components with Superview
Since we’ve been spoiled by implicit template rendering in Rails, nobody really wants to write this boilerplate every time they want to render a component. That’s where Superview can help, and just for fun let’s make peoples’ heads explode 🤯 with an example of inline views in a controller.
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Superview::Actions
before_action { @post = Post.find(params[:id]) }
# The `Show` class maps to the `show` action 👍
class Show < ApplicationComponent
attr_writer :post
def view_template
h1 { @post.title }
article { @post.body }
end
end
# The `Edit` class maps to the `edit` action 👍
class Edit < ViewComponent::Base
attr_writer :post
erb_template <<~ERB
<h1>Editing Post</h1>
<form>
<input type="text" value="<%= @post.title %>">
<textarea><%= @post.body %></textarea>
</form>
ERB
end
end
```
With Superview installed, the Show and Edit views will render for their corresponding actions.
### Installing Superview
Of course, you’ll need to install Superview to get the controller above working with Superview::Actions. You can add it to your Rails app by running:
Restart your Rails server and you should be good to go.
### Extracting components into their own view files
I’ve found that inline views are polarizing in Rails. I love them because I can see my view code right next to my controller actions and it feels like Sinatra, but lots of people want their views in the corresponding ./app/views/posts directory. Fortunately for those who prefer to keep their views far, far away from the controller, it’s possible to move your views into their own folder.
Let’s move our inline views from above into the ./app/views/posts directory.
There’s a bit of Rails app configuration we need to do to get these views automatically loading with Zeitwerk, which is the code loader Rails uses to make your application work.
### Add app/views to Rails autoload paths
We need to add the following to our config/application.rb file so Rails autoloader, Zeitwerk, picks up the view files.
```plain text
# ./config/application.rb
module YourApp
class Application < Rails::Application
config.autoload_paths += Rails.root.join("app/views")
end
end
```
### Namespace the views
Zeitwerk will automatically load the files in the ./app/views/posts directory, but only if we namespace the view in the Posts module. Here’s what view files look like, namespaced, in the ./app/views/posts directory.
```plain text
# ./app/views/posts/show.rb
class Posts::Show < ApplicationComponent
attr_writer :post
def view_template
h1 { @post.title }
article { @post.body }
end
end
```
Don’t forget the edit view!
```plain text
# ./app/views/posts/edit.rb
class Posts::Edit < ViewComponent::Base
attr_writer :post
erb_template <<~ERB
<h1>Editing Post</h1>
<form>
<input type="text" value="<%= @post.title %>">
<textarea><%= @post.body %></textarea>
</form>
ERB
end
```
If you’re wondering, “Why Posts and not Post”, it’s because the singular name Post is likely taken by a model. Pluralizing the namespace for views makes it less likely that you’ll run into a situation where a model and view namespace have the same name.
Now there’s just one more thing…
### Include the Posts module in the controller
We need to include the Posts module in the controller so that the view files are loaded by Zeitwerk.
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Superview::Actions
include Posts
before_action { @post = Post.find(params[:id]) }
end
```
The include Posts line loads the Show and Edit constants into the PostsController namespace, which means Superview can resolve them for the show and edit actions. If you have other controllers that need to render these views, you’d include the Posts module in those controllers as well.
## Common “edge” cases
Good abstractions always have escape hatches for situations where the implicit abstraction won’t work, or needs a little help. Let’s look at how a few of those edge cases are handled.
### Multiple formats in an action
For example, let’s say we need to render a JSON format in the same controller; we can use the render method to render the JSON response.
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Superview::Actions
include Posts
before_action { @post = Post.find(params[:id]) }
def show
respond_to do |format|
# The `component` method returns the component class for the action
format.html { render component }
# Renders the `show.json` view in the `./app/views/posts` directory
format.json
end
end
end
```
The component method will return the component class for the action, which is Posts::Show in this case. If you need to render a different component, you can pass the component class to the render method.
### Rendering a different component in an action
One common pattern for the update action is to render the edit view if there’s an error so the user can see form errors and update their input. We can render the edit component in the update action by passing the Edit class to the component method.
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Superview::Actions
include Posts
before_action { @post = Post.find(params[:id]) }
def update
if @post.update(post_params)
redirect_to @post
else
render component Edit
end
end
end
```
### Selectively loading views in the controller
If you have a controller that only needs to render a component for a single action, you can override the component_view method to return the component class for that action.
```plain text
# ./app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Superview::Actions
before_action { @post = Post.find(params[:id]) }
# Set the constant in the controller to the constant
# of the view you wish to render.
Show = Posts::Show
end
```
## Conclusion
Superview makes it possible to build Rails applications from the ground-up using nothing but components. If it responds to renders_in and attr_writer, Superview can assign the controllers’ instance variables to the component and render it if the class name matches the action name.
You can start using Superview in new and existing Rails apps by running:
And then including the Superview::Actions module in your controllers. The module is backwards compatible with existing Rails views, so you can start using components in new controllers and views as you see fit.
Now you have another tool to build more maintainable Rails applications with components and automatically render Phlex and ViewComponents for controller actions with Superview.
## Support this blog 🤗
If you like what you read and want to see more articles like this, please consider using Terminalwire for your web application’s command-line interface. In under 10 minutes you can build a command-line in your favorite language and web framework, deploy it to your server, then stream it to the Terminalwire thin-client that runs on your users desktops. Terminalwire manages the binaries, installation, and updates, so you can focus on building a great CLI experience.